카카오 메시징 시스템, 경쟁 조건 문제 해결을 위한 아키텍처 여정
KIMS(Kakao Integrated Messaging Service)에서 리포트 누락으로 인해 메시지 상태가 SENT로 고정되는 동시성 문제(Concurrency Issue) 발생
특정 벤더사, 유료 메시지에서 주로 발생했으며, API Server와 Report Server 간의 경쟁 조건(Race Condition)이 원인으로 지목됨
트랜잭션 다이어트(Transaction Diet)를 통해 성능을 개선했지만, 근본적인 문제 해결에는 실패
아웃박스 패턴(Outbox Pattern) 도입을 통해 리포트 누락 문제를 구조적으로 해결하고, Single Writer Principle 적용
트랜잭션 사용의 트레이드오프(Trade-off)를 인지하고, 최종 일관성(Eventual Consistency)을 선택
경쟁 조건(Race Condition) 발생 원인 분석
본문에서는 KIMS(Kakao Integrated Messaging Service)에서 리포트 누락의 원인으로 API Server와 Report Server 간의 경쟁 조건(Race Condition)을 지목한다. 특히, 벤더사 리포트 수신 속도가 빨라지면서, API Server의 DB 영속화(Persistence) 전에 리포트가 도착하는 상황이 발생했다.
API Server: 벤더 API 호출 및 과금 이벤트 발행
Report Server: 리포트 수신 및 상태 업데이트
이러한 상황은 Write 경로와 Read 경로의 타이밍 충돌을 야기했으며, 유료 메시지의 경우 트랜잭션 시간 증가로 인해 문제 발생 가능성이 더욱 높아졌다.
트랜잭션 다이어트(Transaction Diet)와 한계
저자는 트랜잭션 내에서 비동기 과금 이벤트 발행을 분리하여 트랜잭션 다이어트(Transaction Diet)를 시도했다. 이를 통해 트랜잭션 점유 시간을 줄이고, 리포트 누락 건수를 감소시켰다.
@Async, @TransactionalEventListener 활용: 과금 이벤트 발행을 비동기 처리
트랜잭션 축소: 상태 변경 및 DB 영속화(Commit)에 집중
하지만, 트랜잭션 축소는 경쟁 조건 발생 확률을 낮출 뿐, 근본적인 해결책은 아니었다. DB 영속화 시간 자체를 0으로 만들 수 없다는 한계가 존재했다.
트랜잭션 사용의 재고찰
저자는 트랜잭션이 실제로 필요한 보장을 제공하는지 재검토하며, 트랜잭션 사용의 트레이드오프(Trade-offs)를 분석했다. MySQL의 REPEATABLE READ 격리 수준에서 원자성(Atomicity), 읽기 격리(Read Isolation), 쓰기 격리(Write Isolation)가 실제로 필요한지 질문을 던졌다.
원자성: 단일 쓰기 작업이므로 롤백(Rollback) 필요성 낮음
읽기 격리: 벤더 품질 지표는 강한 시점 일관성 요구 X
쓰기 격리: 리포트 수신 타이밍과 충돌, 오히려 문제 발생
결론적으로, 트랜잭션 유지가 불필요하다고 판단하고, 트랜잭션 제거를 결정했다.
아웃박스 패턴(Outbox Pattern) 도입
경쟁 조건 문제를 해결하기 위해, 저자는 아웃박스 패턴(Outbox Pattern)을 도입했다. Report Server는 리포트를 수신하면 즉시 Outbox 테이블에 기록하고, Report Replayer가 Outbox를 기반으로 리포트 적용을 재시도하는 구조이다.
Report Server: 리포트 수신 및 Outbox 기록
Report Replayer: Outbox 기반 리포트 처리 재시도
멱등성 가드(Idempotency Guard)와의 충돌: 두 개의 Writer(Report Server, Report Replayer) 간의 경쟁 발생
이러한 구조는 최종 일관성(Eventual Consistency)을 보장하며, 리포트 누락 문제를 구조적으로 해결했다.
Single Writer Principle 적용
아웃박스 패턴 도입 후, Report Server와 Report Replayer 간의 경쟁 조건이 발생했다. 이를 해결하기 위해 저자는 Single Writer Principle을 적용하여, 동일 데이터에 대한 쓰기를 단일 경로로 직렬화했다.
Report Server: Outbox에 리포트 적재만 수행
Report Replayer: Outbox를 순회하며 리포트 반영 및 과금 이벤트 발행
이러한 설계를 통해 경쟁 조건을 제거하고, 리포트 반영과 과금 이벤트 발행 간의 원자성을 보장했다. 결과적으로, 효율적인 Exactly-once 처리가 가능해졌다.
아키텍처 설계의 교훈
본 사례를 통해 저자는 동시성 문제는 코드의 미세한 조정만으로는 해결될 수 없으며, 경쟁 자체가 발생하지 않는 아키텍처로 전환할 때 근본적으로 제거할 수 있다는 점을 강조한다. 또한, 트랜잭션은 만능 해법이 아니며, 트레이드오프(Trade-offs)를 명확히 인식하고 설계를 해야 함을 강조한다.
트랜잭션: 성능 저하 및 복잡성 증가 가능성
아키텍처 설계: 무엇을 얻고, 무엇을 포기할지 결정
최종 일관성(Eventual Consistency) 선택: 실시간성보다 정확한 결과가 중요
결론적으로, 아키텍처는 끊임없는 선택의 연속이며, 문제 해결을 위해 근본적인 접근 방식을 고민해야 한다.