환불을 음수 거래로 표현할 때 지켜야 할 계약
목차
앞 편에서 취소를 취소일 파일의 음수 행으로 기록하도록 내부 모델을 바꿨습니다. 그런데 그 음수 행을 읽어야 할 반대편 파서는 그대로였습니다. 그래서 정상 환불이 매번 예외 큐에 쌓이고 있었습니다.
한 저장소 안에서 세 문장이 서로 어긋나 있었다
/** 금액 토큰 파싱 — 실패하거나 음수면 null(스킵 신호). */private static Long parseAmount(String raw) { ... return value < 0 ? null : value; // ← 음수를 버립니다}테스트 이름도 "음수 amount는 skip한다(금액 음수 금지)"였습니다. 그런데 같은 패키지의 ExternalRecord 주석은 “환불·차지백은 음수로 온다” 였고, 앞 편에서 제가 고친 InternalRecord는 “취소를 별도 행으로 만든다. 금액은 음수다.” 였습니다.
| 어디에 적혀 있나 | 음수(환불)를 어떻게 다루나 |
|---|---|
InternalRecord: 앞 편에서 고친 것 | 취소를 음수 행으로 쌓는다 |
ExternalRecord 계약 주석 | ”환불·차지백은 음수로 온다” |
PgSettlementCsvParser | 버린다 |
재현
승인 10,000과 환불 -3,000 두 행짜리 파일을 돌렸더니 파싱된 행 1, 건너뛴 행 1이었고 환불 -3,000원이 사라졌습니다.
환불이 있는 날의 대사는 내부 -3,000, 외부 없음(파서가 버림)이 되어 INTERNAL_ONLY 판정이 납니다. 정상 환불이 매번 “사람이 확인해야 할 불일치”로 쌓여 예외 큐가 오염됩니다.
실험용 정산 파일을 돌린 응답에는 이런 값이 있었습니다.
{"external":13,"skipped":3,"matched":4,...}3행이 건너뛰어졌습니다. 그때는 “파서가 요약행을 거르는 기능이 있으니 그거겠지” 하고 “모르는 것” 목록에 적어뒀습니다.
왜 못 봤나 — 한쪽만 봤다
며칠 뒤 열어보니 주문번호가 빈 행(psf-f5~f7)이었습니다. 시드 스크립트가 실패한 슬롯을 그대로 쓴 테스트 데이터 버그였습니다. 그런데 그 버그가 아니었으면 음수 행 문제도 계속 안 보였을 것입니다.
앞 편에서 내부 쪽만 봤습니다. InternalRecord에 마이그레이션과 테스트, 재현까지 붙였지만 외부 파서는 “내가 바꾸지 않은 코드” 였습니다. 그런데 그때 바꾼 것은 한쪽 구현이 아니라 양쪽이 공유하는 모델이었습니다.
ExternalRecord 주석은 이미 맞았습니다. 문서는 맞고 코드가 틀렸는데, 둘을 대조하는 테스트가 없었습니다.
건너뛴 행이 안 보였다
파서는 컬럼 수 부족(요약행), 주문번호 없음, 금액 해석 불가로 행을 건너뛰는데 전부 똑같이 카운트만 합니다. 2026-08-30,,psp-x,500000처럼 50만 원인데 주문번호가 없어도 운영자가 보는 것은 skipped: 3뿐이라, 요약행 3개인지 50만 원짜리 3건인지 알 수 없습니다.
“돈이 모르게 움직이지 않는다”가 대사의 존재 이유인데, 금액이 적힌 채 매칭조차 안 된 행이 숫자 하나로 뭉개졌습니다. 이제 줄 번호·사유·금액을 남기고, 금액이 걸린 채 건너뛴 행을 따로 셉니다.
정산 파일에 금액이 있는데 건너뛴 행 2건 — 원본 확인 필요: [SkippedRow[line=3, reason=주문번호 없음, amount=500000], ...]합계,,,510000도 “금액 있고 주문번호 없는 행”이라 함께 잡히는데, 걸러내려다 그만뒀습니다. 합계,,,510000과 “주문번호가 빠진 51만 원 거래”는 파일만 보고 구분할 수 없으니까요. 놓치는 쪽(진짜 돈)보다 한 번 더 보는 쪽이 낫습니다.
원인 분류표에 빈 칸이 있었다
원인 분류표에 빠진 칸은 내 목록만 봐서는 알 수 없어 밖에서 잣대를 가져왔습니다. 라벨된 대사 예외 데이터셋은 결제사 운영 데이터라 공개된 것이 없어, 여러 PSP 운영 자료가 공통으로 드는 여섯 분류(타이밍 격차, 금액 불일치, FX 변동, 은행 입금 누락, PSP 기록 누락, 중복)와 대조했습니다.
표준 규격은 아니라 “빠진 칸 확인” 용도로만 썼는데, 다섯은 덮고 중복 하나가 비어 있었습니다. 엔진은 이미 중복을 걸러내고 로그도 남기지만 확정하는 사람에게는 고를 어휘가 없어 OTHER로 가고, 그러면 반복돼도 집계에 안 잡힙니다.
DUPLICATE_RECORD를 원인에 추가하고 실행 요약이 중복 행 수를 싣게 했습니다. 매핑표를 테스트가 들고 비는 칸에는 왜 비었는지 쓰게 강제해, 통화가 늘거나 새 원인이 생기면 여기가 먼저 걸립니다.
마무리
양쪽이 공유하는 계약을 바꿨다면 한쪽 구현만 검증하면 안 됩니다. 문서와 코드가 어긋난 것을 문서 쪽이 맞았는데도 못 잡은 이유는, 둘을 대조하는 테스트가 없었기 때문입니다.
참고
- 앞 편: 부분취소가 오자 이미 끝난 결제 대사의 근거가 바뀌었다
- 대사 조회 화면과 원인 제안: 대사 원인을 고르라면서 근거는 11곳에 흩어져 있었다
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.