개발 slecs

파트너 낙전 환급 비대칭 버그 수정

목차

낙전 환급 처리에서 비대칭 버그가 터졌다. 파트너 쪽 환급이 특정 조건 조합에서 무제한으로 적용될 수 있는 엣지 케이스였는데, 내부 클래스 3개를 수정해서 막았다. 변경 파일 수만 보면 작아 보이지만 원인 파악하는 데 시간이 꽤 걸렸음.

비대칭이 왜 위험한가

금융·정산 도메인에서 "비대칭"이라는 단어가 버그 설명에 들어오면 일단 긴장해야 한다. 환급(credit)이 발생했을 때 그에 대응하는 차감(debit)이 정확히 연동되지 않거나, 조건 검증 없이 환급을 반복 적용할 수 있는 경로가 열려 있으면 숫자가 한쪽으로만 계속 쌓인다. 화면에서는 정상처럼 보이다가 월말 정산 시점에 차이가 드러나는 류의 버그다.

이번 케이스는 파트너 낙전 환급 로직에서 특정 상태 조합이 엣지 케이스로 남아 있었다. "이미 환급된 건"을 판별하는 조건이 완전하지 않아서, 그 조건을 충족하는 상황을 반복해서 만들면 환급이 중첩 적용될 수 있었음. 무제한이라는 표현이 과장처럼 들릴 수 있지만, 횟수 제한이 없는 경로가 실제로 열려 있었으니 그 표현이 맞다.

비슷한 구조의 버그가 왜 생기는지 생각해보면, 결국 "정상 케이스 중심으로 코드를 짰을 때" 많이 발생한다. 환급 요청이 들어오면 잔액 확인 → 환급 처리 → 상태 업데이트 순서로 짜지만, "이미 처리된 환급 요청이 다시 들어오면?" 혹은 "상태 업데이트 전에 같은 요청이 두 번 오면?"같은 질문을 빠뜨리는 식이다.

// 취약한 패턴 예시 (의사코드)
fun processRefund(partnerId: String, amount: Long) {
    if (hasBalance(partnerId, amount)) {
        applyRefund(partnerId, amount)  // 중복 호출 방어 없음
    }
}

// 수정 방향: 멱등성 확보
fun processRefund(partnerId: String, refundId: String, amount: Long) {
    if (isAlreadyProcessed(refundId)) return  // 선행 차단
    if (hasBalance(partnerId, amount)) {
        applyRefund(partnerId, amount)
        markAsProcessed(refundId)
    }
}

실제 코드는 이것보다 복잡하지만 핵심 구조는 비슷하다. "이미 처리됐는지 먼저 확인한다"는 분기가 없거나, 확인 조건이 특정 파트너 유형에서 달라지는 케이스를 커버하지 못하는 게 원인이었음.

수정 범위와 접근

증상만 픽스하는 게 아니라 왜 발생했는지 원인을 먼저 파악하고 수정했다. 내부 클래스 3개를 건드린 이유는 같은 패턴이 다른 진입 경로에도 있었기 때문이다. 하나만 고치면 그 경로는 막히지만, 유사한 케이스에서 다시 뚫릴 수 있다. 위험한 케이스를 함께 수정하지 않으면 완전한 픽스가 아님.

수정 전에 체크한 것들:

항목 확인 방법
중복 코드 경로 같은 환급 처리 로직이 다른 진입점에서도 호출되는지 추적
회귀 방지 정상 케이스(최초 환급, 부분 환급, 취소 후 환급)가 여전히 동작하는지 수동 확인
경계 조건 amount=0, 이미 처리된 refundId, 잔액이 정확히 0인 케이스
숫자 정합성 환급 전후 합산 금액이 관련 화면 간에 일치하는지 비교

금융 도메인은 경계 조건이 까다롭다. 잔액이 정확히 환급 금액과 같을 때, 소수점 처리 방식이 다른 모듈과 맞지 않을 때, 파트너 유형별로 상태 판단 조건이 달라질 때 같은 것들. 이걸 "대충 비슷하겠지"로 넘기면 나중에 반드시 다시 돌아온다.

검증과 개발 습관

수정 후 버그를 직접 재현해서 정상 동작을 확인했다. 중요한 건 "수정했으니 됐겠지"가 아니라 실제로 문제 케이스를 다시 만들어보는 것이다. 재현 시나리오를 만들 수 없으면 수정도 검증이 안 된다. 숫자 정합성은 관련 화면과 비교해서 맞췄음. 환급 전후 잔액, 환급 이력 건수, 합산 금액이 각 화면에서 일치하는지 대조하는 방식이다.

자동화 테스트가 있으면 좋겠지만, 없는 상태에서 빠르게 검증할 때는 이 방식이 현실적이다. 그리고 이 수동 검증 과정을 거치면서 "이 케이스는 자동화 테스트로 잡았어야 한다"는 감각도 생긴다. 다음 기회에 커버하면 됨.

개발 방식 쪽에서 계속 유지하려는 습관들:

  • 변경 전 현재 동작 수치나 화면 메모 → 수정 후 같은 케이스로 비교
  • 커밋 메시지에 "왜"를 담으려고 노력. "환급 중복 방어 추가"보다 "파트너 낙전 환급 비대칭 케이스 차단 - 이미 처리된 건 판별 조건 누락"
  • 논리적으로 독립된 단위로 커밋 쪼개기. 문제가 생겼을 때 어느 변경에서 깨졌는지 찾는 속도가 확실히 다름

사내 서비스에서 금융 도메인을 다루다 보면, 기능 하나가 단순히 화면 버튼 하나로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크가 전부 엮여 있어서 어느 하나만 빠뜨려도 숫자가 틀리거나 특정 파트너에게 이상한 결과가 나온다. "대충 맞는 것 같다"로 넘어가면 반드시 다시 돌아오고, 그때는 훨씬 더 복잡한 상태에서 디버깅을 시작하게 된다. 이번에 비슷한 패턴을 다 찾아서 함께 막은 게 그나마 잘한 부분이라고 생각함.

댓글 0

첫 댓글 달아줘.