잃어버린 리포트를 찾아서, 카카오 메시징 시스템 경쟁 조건 제거

헤드라인

도입: 보냈는데 받은 적 없는 편지

KIMS(Kakao Integrated Messaging Service)에서 일부 메시지가 SENT 상태에 머물며 리포트가 “사라진” 것처럼 관측됐다. 로그 분석 결과 특정 벤더사가 약 8ms 만에 리포트를 보내, API 서버의 DB 영속화 전에 리포트가 도착하는 경쟁 조건이 원인이었다. 유료 메시지는 과금 후처리가 하나의 트랜잭션에 묶여 더 두드러졌다.

전개: 트랜잭션 다이어트와 제거

과금 이벤트를 @TransactionalEventListener(AFTER_COMMIT)로 빼서 커밋 시점을 앞당겼고, 해당 경로에서 원자성·읽기/쓰기 격리가 실질 이점을 주지 않는다고 판단해 @Transactional을 제거해 커넥션 점유를 101ms에서 수 ms 수준으로 줄였다. 그럼에도 경쟁 자체를 없애려면 구조 변경이 필요했다.

전환: Transactional Outbox와 Single Writer

리포트를 먼저 Outbox에 적재하고 Report Replayer가 배치로 상태 전이·과금 이벤트를 수행하도록 했다. 초기에는 Report Server와 Replayer가 같은 데이터를 수정하며 멱등성 가드와 충돌해 과금 누락이 늘었고, Single Writer Principle로 “같은 데이터에 대한 쓰기는 하나의 경로만” 두어 Report Server는 Outbox 적재만, Replayer만 상태 전이와 과금 발행을 담당하게 했다.

결말: 즉시성 대신 최종 일관성

즉시성은 희생했으나 리포트·과금 누락을 구조적으로 제거했고, 동시성 문제는 경쟁이 발생하지 않는 아키텍처로 옮길 때만 근본적으로 해소된다는 교훈을 남긴다.