결제 정산 원장의 컬럼 매핑 버그와 멱등성 오류 수정
목차
결제 정산 원장(system-ledger)에서 버그 두 개를 동시에 잡았다. audit 테이블의 source_history_sn 컬럼 매핑이 엉켜 있었고, 그 여파로 멱등성 키도 잘못된 값을 바라보고 있었다. 변경 파일은 내부 클래스 하나, SQL 매퍼 하나. 건드린 파일 수만 보면 아담한 픽스지만 내부 사정은 좀 달랐다.
컬럼 매핑이 틀리면 무슨 일이 생기나
ORM이나 MyBatis 계열 매퍼를 쓰다 보면 컬럼 이름과 필드 이름이 조금씩 달라지는 경우가 생긴다. 스네이크케이스-카멜케이스 자동 변환이 켜져 있으면 대부분 자동으로 맞아 떨어지지만, 별칭(alias)이 붙거나 서브쿼리로 감싸진 순간부터는 자동 매핑이 제대로 안 먹히는 케이스가 나온다.
이번 케이스가 딱 그랬다. source_history_sn이 쿼리 안에서 집계나 조인을 거치면서 결과 컬럼명이 바뀌었는데, 매퍼 ResultMap은 원래 이름 그대로 물려받고 있었다. 결과적으로 해당 필드는 null로 떨어지거나, 운이 나쁘면 순서가 밀려서 옆 컬럼의 값이 들어왔다. 정산 원장에서 sn이 엉뚱한 값이면 이게 멱등성 키로 흘러갈 때 문제가 커진다. 같은 트랜잭션인데 다른 키로 인식하거나, 반대로 다른 건인데 같은 키로 묶이는 상황이 생길 수 있다.
멱등성 키가 흔들리면 결제 재처리 로직에서 중복 처리 또는 누락 처리가 발생한다. 결제 도메인에서 이 두 가지는 데이터 정합성뿐 아니라 실제 돈과 직결되기 때문에 "나중에 정산하면 맞아떨어지겠지" 식으로 넘길 수 없다.
수정 방식과 파급 범위 점검
SQL 매퍼부터 손댔다. 집계 쿼리에서 source_history_sn에 명시적 alias를 붙이고, ResultMap에서도 그 alias와 매핑 필드를 일치시켰다. 쿼리가 복잡해질수록 암묵적 이름에 의존하는 게 사고의 씨앗이 된다는 걸 다시 한 번 확인한 순간이었다.
<!-- 수정 전: alias 없이 서브쿼리에서 올라와 ResultMap에서 매핑 실패 -->
<result column="source_history_sn" property="sourceHistorySn"/>
<!-- 수정 후: 쿼리에서 alias 명시, ResultMap에서 alias로 연결 -->
<result column="src_hist_sn" property="sourceHistorySn"/>
내부 클래스 쪽은 멱등성 키를 생성하는 로직이었다. 키 생성 시 sourceHistorySn을 읽어오는데, 그게 null로 들어오니 키가 "null" 문자열을 포함하거나 NPE 경계에서 흔들렸다. 여기서 한 가지 결정이 필요했다. 값이 null일 때 예외를 던질 것인가, 아니면 fallback 값을 써서 처리를 계속할 것인가.
정산 원장 맥락에서 sn이 없다는 건 데이터가 온전하지 않다는 뜻이므로, fallback보다 이른 실패(fail-fast)가 맞다고 판단했다. 데이터가 깨진 상태에서 정산을 계속 돌리면 잘못된 원장이 쌓이고 나중에 정산 불일치로 돌아온다.
수정 후에는 비슷한 패턴이 다른 매퍼에도 있는지 훑었다. 같은 audit 테이블을 바라보는 다른 쿼리들, 비슷한 집계 구조를 쓰는 다른 서비스 클래스들. 위험해 보이는 케이스는 같이 고쳤다.
버그 픽스 때 보통 이 순서로 점검한다.
- 같은 로직이 다른 경로(API 엔드포인트, 배치 잡 등)에 복사돼 있는지
- 수정이 기존 정상 케이스를 건드리지 않는지 - 특히 집계 값이 달라지지 않는지
- 해당 화면/API에서 실제로 재현 후 정상 동작 확인
- 숫자가 나오는 다른 화면과 cross-check
엣지 케이스를 한 번에 꼼꼼히 따지는 게 귀찮아 보여도, 같은 버그로 두 번 오는 시간 비용이 훨씬 크다. 특히 결제 도메인은 "일단 배포하고 보자"가 먹히지 않는 영역이다.
작업 후기
사내 서비스를 만들다 보면 기능 하나가 단순히 화면 버튼 하나 추가하는 것으로 끝나지 않는다는 걸 계속 체감한다. SQL 집계, 상태 머신, 예외 처리, 화면 렌더링, 권한 체크가 모두 엮여 있어서 어느 하나만 빠뜨려도 숫자가 맞지 않거나 특정 사용자에게 이상한 화면이 나타난다.
금융/결제 도메인은 숫자 하나가 틀리면 신뢰가 무너질 수 있어서 꼼꼼함이 기본값이어야 한다. "대충 맞는 것 같다"로 넘어가면 반드시 다시 돌아온다.
커밋 습관도 이 작업에서 한 번 더 확인했다. 이번처럼 두 파일만 건드렸어도, SQL 매퍼 수정과 내부 클래스 수정은 논리적으로 다른 단위여서 커밋을 쪼갰다. 나중에 어느 변경에서 회귀가 생겼는지 볼 때 훨씬 좁혀서 찾을 수 있다. 커밋 메시지는 "무엇을"보다 "왜"를 담으려고 하는데, 3개월 후의 나 자신이 git log를 봤을 때 배경을 다시 추적하지 않아도 되게끔.
수정 전에 현재 수치나 동작을 메모해두는 습관도 계속 유지하고 있다. 수정 후 같은 조건으로 비교했을 때 숫자가 달라지면 의도한 변경인지 아닌지를 바로 판단할 수 있어서다. 스크린샷이든 로그 스니펫이든 텍스트 메모든, 비교 기준이 있어야 검증이 된다.
댓글 0
첫 댓글 달아줘.