이 건이 앞선 건과 같은 케이스인지를 확인해 달라는 요청을 받았다. 두 건 다 returns.status 는 COMPLETED 로 돼 있는데 refunds 에 이력이 없는 상태였다.
Table of contents
Open Table of contents
검증 — 숫자로 한 대조
두 건을 나란히 놓고 같은 쿼리로 status 와 COUNT(returns) 와 COUNT(refunds) 를 함께 뽑았다. 세 값이 완전히 같은 패턴으로 나왔다.
비슷해 보인다는 인상이 아니라 숫자로 같다는 것을 확인한 셈이다. 같은 케이스인지를 판정할 때는 비교할 축을 먼저 정하고 그 축의 값을 나란히 놓는 것이 가장 빠르다.
원인 — 우리가 안 한 환불
이 유형은 마켓이 먼저 고객에게 환불하고 나중에 판매자 정산에서 차감하는 방식이다. 우리는 RETURN_COMPLETED 신호를 수집해 내부 상태를 바꾸고 SettlementEvent 를 발행한다.
우리가 직접 환불한 것이 아니므로 환불 이력을 만들지 않는다. 즉 반품이 있고 환불이 없는 것이 이 흐름의 정상 결과이고 버그가 아니었다.
주의 — 단어가 가르는 신뢰
이 건에서는 응대 문구를 어떻게 쓰는지가 중요했다. 두 가지 방법이 있었는데 환불 처리가 누락됐다고 쓰는 것과 환불 이력이 기록되지 않으며 처리는 채널에서 완료됐다고 쓰는 것이다. 뒤쪽을 골랐다. 이유는 앞쪽은 시스템 문제로 들리고 뒤쪽은 설계상 그렇다는 뜻이 돼서다. 대신 문장이 길어져 응대 담당자가 한 번 더 읽어야 하는 것을 감수했다.
같은 사실을 설명하는데도 단어 하나가 받는 쪽의 신뢰를 가른다. 누락이라는 말은 우리가 해야 할 것을 안 했다는 뜻을 담고 있기 때문이다.
조치 — 근본 대응 전의 확인
그러면 이력을 만들어 두면 되지 않겠느냐는 생각이 자연스럽게 나온다. 수집하는 시점에 환불 이력을 자동으로 만드는 것이 근본 대응이다.
그 전에 확인이 필요했는데 refunds 가 SettlementCalculator 에 그대로 들어가기 때문이다. 지금은 없어서 안 들어가는데 만들면 마켓이 이미 차감한 것과 우리 이력이 겹쳐 이중 차감이 될 수 있으므로 이력을 만드는 것이 안전한지를 먼저 확인해야 했다.
배경 — 범위가 알려 준 형식
조회 과정에서 하나가 걸렸는데 받은 주문번호로 조회했더니 결과가 안 나왔다. Hashids 로 인코딩된 식별자로 보고 decode() 하려 했는데 값이 인코딩 범위인 32비트를 넘고 있었다.
즉 우리 식별자가 아니라 외부 주문번호였고 값의 범위가 형식을 알려 준 셈이다. Hashids 의 salt 가 엔티티마다 달라서 잘못 고르면 되돌린 값이 엉뚱해지고 조회 결과가 없어 번호가 틀렸다고 오판하게 되므로 어느 엔티티의 식별자인지를 먼저 확정해야 했다.
정리
- 이력이 없는 것이 정상 흐름의 결과일 수 있다
- 우리가 실행한 것이 아니면 이력을 안 만드는 설계가 있다
- 응대는 누락이 아니라 미기록으로 설명한다
- 단어 하나가 신뢰를 가른다
- 이력을 만드는 대응 전에 정산에 미치는 영향을 확인한다
- 번호가 인코딩 범위를 넘으면 외부 번호다
- 인코딩 설정이 엔티티마다 다르므로 먼저 확정한다
- 같은 케이스인지는 같은 쿼리로 나란히 놓고 숫자로 확인한다