개발 slecs

가맹점 출금 금액 음수 방지 버그 수정

목차

가맹점 출금 화면에서 출금 가능 금액이 음수로 표시되는 버그를 잡았다. merchant-withdraw 뷰 파일 한 개 수정이었고, 공식 라벨을 max(0, ① + ② − ③) 형태로 clamp 명시하는 게 핵심이었음.

단순 패치처럼 보이지만, 이 작업을 제대로 이해하려면 출금 가능 금액이 어떻게 구성되는지부터 봐야 한다.

음수가 생기는 구조

출금 가능 금액의 기본 공식은 대략 이렇다.

출금 가능 금액 = ① 정산 잔액 + ② 예치금 - ③ 출금 대기 중 금액

논리적으로 ③이 ① + ②를 초과할 수 없어야 함. 출금 신청 자체가 잔액 범위 안에서만 가능하니까. 근데 현실은 좀 다르다. 정산 취소, 환불 처리, 상태 전환 타이밍이 맞물리는 순간에 데이터가 일시적으로 비정합 상태가 된다. 출금 신청이 대기 중인 상태에서 그 기반이 된 정산 건이 취소되거나 금액이 조정되면, ③ 항목이 실질 잔액을 초과하는 상황이 생길 수 있음.

백엔드 집계 자체가 틀린 게 아니다. 그 순간의 데이터 스냅샷을 정직하게 내려줬을 뿐이고, 그 값을 화면에 그대로 렌더링하는 게 문제였던 것. 값 자체는 맞더라도 화면에 -3,500원 같은 숫자가 표시되면 사용자 입장에서 시스템을 의심하는 것 외에 다른 선택지가 없다. 결제/정산 도메인에서 숫자 하나가 신뢰를 흔드는 게 얼마나 빠른지는 직접 겪어봐야 안다.

수정은 렌더링 직전 clamp 한 줄이었음.

// 수정 전
const withdrawable = totalSettlement + deposit - pendingWithdraw;

// 수정 후
const withdrawable = Math.max(0, totalSettlement + deposit - pendingWithdraw);

서버 응답은 그대로 두고 프론트에서 처리한 건 의도적인 선택이다. 서버 집계 로직에서 clamp를 넣으면 API 응답 자체가 "음수인데 0을 반환"하는 형태가 되는데, 그러면 나중에 디버깅할 때 실제 상태인지 표현용으로 덮어씌운 건지 구분이 안 된다. 화면에 보여줄 때 clamp하는 게 계층 분리 측면에서 더 깔끔하고, 비즈니스 데이터와 표현 값을 섞지 않는 게 로그 보면서 상태 추적할 때도 훨씬 낫다.

같은 패턴이 어디에 또 있는지

이 버그 수정하고 나서 바로 한 게 비슷한 계산식이 다른 화면에도 있는지 확인하는 거였음. 가맹점 관련 화면이 한두 개가 아니고, 정산 잔액이나 출금 관련 수치를 표시하는 곳이라면 동일한 엣지 케이스에 노출돼 있으니까.

이번엔 해당 뷰 한 곳만 수정이 필요했지만, 확인하는 과정 자체가 가맹점 화면 구조를 다시 한번 정리하는 시간이 됐다. 같은 API 응답에서 어떤 필드를 어느 화면이 쓰는지, 계산 중간값을 컴포넌트 안에서 처리하는지 밖에서 처리하는지. 이걸 파악해두면 다음에 비슷한 이슈가 생겼을 때 어디를 봐야 하는지 빠르게 잡힌다.

버그 수정 때 흘러가는 체크 순서를 정리하면 이렇다.

단계 확인 항목
원인 파악 증상이 아닌 데이터 흐름 추적, 왜 발생하는지
중복 확인 같은 로직이 다른 파일/컴포넌트에 있는지
회귀 방지 수정이 정상 케이스(0, 양수)를 깨지 않는지
재현 검증 실제 화면에서 해당 케이스 재현 후 동작 확인
숫자 교차검증 관련된 다른 화면과 수치 일치 여부

마지막 교차검증이 생각보다 중요하다. 같은 잔액을 다른 경로로 표시하는 화면이 두 개 있을 때, 한쪽만 고치면 두 화면의 숫자가 달라지는 더 이상한 상황이 생긴다. 특히 가맹점 어드민이나 정산 현황처럼 "같은 데이터, 다른 뷰" 구조가 많은 경우엔 반드시 같이 보고 가야 함.

금융 도메인에서 숫자를 다루는 감각

사내 서비스를 만들다 보면 기능 하나가 화면에 버튼 하나 추가하는 걸로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크가 다 엮여 있어서 어느 하나만 빠뜨려도 숫자가 안 맞거나 특정 케이스에서 이상한 화면이 나옴. 이번 버그가 딱 그 지점이었다. 집계는 맞고, 권한도 맞고, 렌더링 한 줄이 문제였는데 그게 사용자한테는 "시스템이 이상하다"로 읽힌다.

결제, 정산, 출금이 엮이는 도메인은 "대충 맞는 것 같다"로 넘어가면 반드시 다시 돌아온다. 이번 음수 버그도 평상시에는 재현이 안 되고 특정 타이밍에서만 나타나는 케이스라 발견 자체가 늦어지기 쉬운 유형이다. 그래서 엣지 케이스 하나를 잡으면 "이 타이밍에서 또 어떤 값이 이상할 수 있을까"를 함께 따져보는 게 습관이 됐음. 귀찮아 보여도 나중에 같은 버그로 다시 오는 시간 비용이 훨씬 크다.

수정 전후로 항상 지키려는 것 중 하나가 수치 메모다. 수정 전 화면 값, 수정 후 화면 값을 같은 케이스로 비교해두면 "고쳤다고 생각했는데 아니었다" 상황을 막을 수 있음. 스크린샷이 아니어도 수치 몇 개만 메모해두는 것만으로도 충분하다.

커밋 단위도 계속 신경 쓰는 부분이다. 이번 수정은 파일 하나라 단순했지만, 조금이라도 복잡해지면 렌더링 수정과 로직 수정을 분리한다. git bisect 돌릴 상황이 생겼을 때 커밋이 논리 단위로 쪼개져 있으면 어디서 깨졌는지 추적하는 시간이 확 줄어들기 때문이다. 커밋 메시지는 "무엇을"보다 "왜"를 담으려 한다. "출금 금액 음수 방지"보다 "정산 조정 타이밍에서 출금 가능 금액이 음수로 표시되는 렌더링 버그 수정"이 3개월 뒤 git log에서 맥락을 복원하는 데 훨씬 낫다. 혼자 기억하는 약어가 아니라 처음 보는 팀원도 맥락을 이해할 수 있게 써야 팀 히스토리로서 의미가 있으니까.

댓글 0

첫 댓글 달아줘.