입금 큐 성공 후 주문 매칭이 자동으로 안 되던 문제 해결
목차
결제대행사에서 입금 통지가 들어오고, 큐도 정상 처리되어 SUCCESS까지 찍혔는데 파트너 화면에는 "미매칭"이 뜨는 상황이었다. 운영팀은 그 화면을 보고 매일 수동으로 매칭 버튼을 누르고 있었고, 나는 그걸 코드 리뷰 중에 우연히 들었다.
"아, 원래 그렇게 하는 거 아닌가요?" 하고 넘어갈 수도 있었는데, 같은 버튼을 반복해서 누르는 운영 행동은 항상 코드 갭을 가리키는 신호다. 일단 파봤다.
어디서 끊겼는지
구조를 따라가 보니 문제는 단순했다. 입금 통지 수신 → 큐 적재 → 큐 처리(SUCCESS 마킹)까지는 연결돼 있었고, 그 다음 "이 입금을 어떤 주문에 붙일지" 결정하는 매칭 단계가 별도 스케줄러나 수동 호출에 의존하고 있었다.
| 단계 | 기존 동작 | 변경 후 |
|---|---|---|
| 입금 통지 수신 | 큐 적재 | 큐 적재 |
| 큐 처리 | SUCCESS 마킹만 | SUCCESS 마킹 + 매칭 트리거 |
| 주문 매칭 | 스케줄러 대기 / 수동 | 큐 커밋 직후 자동 |
스케줄러 주기가 짧으면 그나마 낫지만, 주기가 길거나 스케줄러가 한 번이라도 미끄러지면 그 간격만큼 미매칭이 쌓인다. 매칭 지연이 실시간성 요구가 강한 주문 흐름에서 운영 공수로 전환되어 버린 셈이다.
증상을 다시 정리하면 이렇다.
- 입금 통지는 정상 수신
- 큐 row 상태 SUCCESS
- 파트너 화면에는 "미매칭"으로 표시
- 운영팀이 수동으로 매칭 버튼을 반복 클릭
트랜잭션 경계를 어디서 자를 것인가
큐 SUCCESS 분기 끝에 매칭 호출을 그냥 끼워 넣으면 제일 빠르다. 근데 그렇게 하면 큐 상태 변경과 주문 매칭이 같은 트랜잭션 안에 묶인다. 매칭 로직이 느리거나 외부 의존이 있으면 그 시간 동안 큐 row에 락이 걸린 채로 유지된다. 매칭이 실패하면 큐 SUCCESS 자체도 롤백된다. 이건 원하는 동작이 아니다.
"큐가 끝났다"와 "비즈니스가 끝났다"는 다른 사건이다. 큐 SUCCESS는 인프라 관점에서 "이 메시지는 처리됐다"는 확인이고, 매칭은 도메인 관점에서 "입금을 주문에 연결하는 비즈니스 동작"이다. 둘을 같은 트랜잭션 안에 두면 한쪽 실패가 반대편의 상태도 뒤집어버린다.
그래서 경계를 명시적으로 끊었다.
// 의사 코드
markQueueSuccess(queueId) // 트랜잭션 A
publishMatchEvent(queueId) // A 커밋 후 호출
matchOrder(queueId) // 트랜잭션 B
트랜잭션 A가 커밋되고 나서 매칭 트리거를 날린다. publishMatchEvent는 현재 동기 호출로 유지했지만, 나중에 이벤트 디스패처로 바꿔도 인터페이스는 동일하다. 매칭(트랜잭션 B)이 실패해도 큐 SUCCESS는 살아있어야 하니, 매칭 호출 전체를 try/catch로 감싸고 예외는 로깅만 한 뒤 흘려보냈다. 매칭 누락분은 기존 감시 잡이 다시 주워가도록 기존 구조를 그대로 뒀다.
매칭 로직 자체는 새로 짜지 않았다. 이미 있던 매칭 로직의 진입점만 자동화한 것이라, 실제 변경 범위는 큐 처리 유틸의 SUCCESS 분기 몇 줄이 전부다.
이 패턴을 Outbox Pattern이라고 부르기도 한다. 상태 변경과 후속 이벤트 발행을 같은 트랜잭션에 묶어두면 발행 실패 시 상태도 롤백되고, 반대로 상태 커밋 후 발행 전에 프로세스가 죽으면 이벤트가 유실된다. 완전한 Outbox를 도입하면 이벤트를 DB에 같이 적었다가 별도 릴레이가 발행하는 구조가 되는데, 지금 당장 그 수준의 내구성이 필요한지는 트래픽과 요구사항에 따라 다르다. 지금은 동기 호출 + 재시도 잡 구조로 충분하다고 판단했다.
돌아보며
버그 리포트가 없었다는 게 흥미롭다. 기능 자체는 동작하고 있었고, 그냥 느릴 뿐이었으니까. 운영팀은 그게 원래 프로세스인 줄 알고 적응해버린 상태였다. 코드만 보면 이 갭이 안 보인다. 큐 처리 코드는 SUCCESS를 제대로 마킹하고 있었고, 매칭 로직도 별도로 잘 작동하고 있었다. 문제는 그 사이가 자동으로 이어지지 않는다는 것뿐이었는데, 코드 리뷰로는 잘 안 잡힌다.
운영 행동을 관찰하는 게 이런 케이스를 잡는 가장 빠른 방법이다. "왜 매번 같은 버튼을 누르고 있지?"라는 질문 하나가 시작이었다. 자동화 결함은 불편함으로 존재하다가, 그 불편함이 루틴이 되는 순간 보이지 않게 된다.
남은 숙제도 있다.
- 자동매칭 실패율 메트릭을 별도로 추출해서 알람 연결
- 매칭 실패 사유 코드 표준화 (현재 메시지가 자유 텍스트라 집계가 안 됨)
- 큐 SUCCESS 후 후속 작업이 더 늘어날 경우를 대비해 단발 호출 대신 이벤트 디스패처로 정리하는 리팩터 검토
실패 사유 표준화가 제일 빨리 챙겨야 할 것 같다. 지금은 로그를 눈으로 읽어야 하는 구조라, 매칭 실패가 어떤 패턴으로 발생하는지 집계가 안 된다. 메트릭이 없으면 개선도 없다.
댓글 0
첫 댓글 달아줘.