취소했는데 채널에 반영이 안 된다는 문의를 받았고 order_cancels.status 는 DONE 이었다. CancelService::cancel() 전체를 읽었는데 외부로 보내는 부분이 없었다.
Table of contents
Open Table of contents
배경 — 취소 코드에 없던 전송
취소 로직은 상태를 바꾸고 order_histories 를 남기고 재고를 되돌리는 데서 끝났다. 그 안에는 dispatch() 가 한 줄도 없었다.
그런데 평소에는 외부에 반영되고 있었으므로 다른 어딘가에서 그것을 부른다는 뜻이었다. 동작을 하는 코드를 아무리 읽어도 언제 일어나는지는 안 나오는 상황이었다.
검증 — 호출자를 역추적한 결과
두 가지 방법이 있었는데 취소 흐름을 화면부터 다시 따라 내려가는 것과 외부로 보내는 SendChannelCancelJob 이름으로 grep -rn 해 그것을 만드는 곳을 찾는 것이다. 뒤쪽을 골랐다. 이유는 동작을 하는 코드를 아무리 읽어도 언제 일어나는지는 안 나와서다. 대신 잡 이름이 바뀌었거나 동적으로 만들어지면 검색에 안 걸리는 것을 감수했다. RefundService 파일에서 나왔다.
환불이 완료될 때 외부 취소가 나가는 구조였고 취소 자체가 아니라 그다음 단계가 방아쇠였다. 동작을 하는 코드가 아니라 그 동작을 부르는 곳을 찾는 것이 이런 조사에서 훨씬 빠른 경로였다.
원인 — 방아쇠가 된 다음 단계
문제의 건을 다시 보니 취소는 DONE 이고 외부 연동은 실패인데 refunds 레코드가 아예 없었다. 방아쇠가 될 사건 자체가 일어나지 않은 것이다.
선행 데이터가 없으면 그 뒤의 동작은 실패한 것이 아니라 시작되지 않은 것이다. 실패로 보고 재시도를 걸었으면 아무 변화 없이 시도 횟수만 늘었을 것이다.
조치 — 선행 데이터가 없던 이유
이 주문은 거래가 종료된 곳의 것이라 payment_id 와 seller_id 가 NULL 이었다. 그러면 RefundService 가 진입 조건을 못 넘는다.
즉 이 건은 구조적으로 외부 연동이 불가능한 상태였다. 시스템 안에서 재시도로 풀리는 종류가 아니었으므로 외부 관리 화면에서 직접 처리해야 한다는 것을 회신에 적었다.
주의 — 확정과 추론의 구분
seller_id 가 비어 있는 것이 진입을 막은 원인이라는 것은 내 판단이고 코드로 확정하지는 못했다. 진입 조건을 전부 확인하려면 여러 경로를 봐야 했고 그 시점에는 거기까지 가지 못했다.
그래서 그 부분을 INVESTIGATION.md 에 추론이라고 명시해 적었다. 확정과 추론을 섞어 두면 나중에 이 기록을 근거로 다른 판단을 할 때 그 위에서 틀린다.
정리
- 어떤 동작의 방아쇠가 예상과 다른 곳에 있을 수 있다
- 동작을 하는 코드를 읽어도 언제 일어나는지는 안 나온다
- 그 동작을 부르는 곳을 검색하는 편이 빠르다
- 선행 데이터가 없으면 실패가 아니라 시작되지 않은 것이다
- 그 구분을 안 하면 재시도만 반복하게 된다
- 구조적으로 불가능한 건은 왜 불가능한지를 회신에 적는다
- 대신 무엇을 해야 하는지까지 함께 적는다
- 확정하지 못한 것은 추론이라고 명시한다