12/18

졸용·2025년 12월 18일

TIL

목록 보기
138/144

🔹 outbox/inbox 알아보기

Outbox / Inbox 패턴을 “깊이(depth)” 단계별로 쌓아 올리는 방식으로 정리해봤다.
(이벤트 기반/마이크로서비스에서 “DB 트랜잭션 ↔ 메시지 발행”의 정합성을 맞추는 용도)



🔹 depth.1 이론(왜 필요한가 / 무엇을 해결하나)

🔸 Outbox 패턴

  • 서비스가 DB에 상태 변경(예: 주문 생성)을 한 뒤, 곧바로 Kafka 같은 브로커로 이벤트를 발행하면
    DB 커밋은 성공했는데 메시지 발행은 실패(또는 반대) 같은 “반쪽 성공”이 생길 수 있다.

  • Outbox는 이를 막기 위해,

    1. 업무 데이터 변경
    2. 이벤트 기록(Outbox 테이블 insert)
      같은 DB 트랜잭션으로 묶고,
      이후 별도 프로세스(퍼블리셔)가 Outbox를 읽어 브로커로 발행한다.

즉 “DB가 진실(Source of Truth)이고, 이벤트는 DB에 먼저 적어둔 뒤 안전하게 발행”하는 구조.


🔸 Inbox 패턴

  • 브로커(Kafka)는 “최소 1회(at-least-once)” 전달이 흔해서, 컨슈머는 중복 메시지를 받을 수 있다.

  • Inbox는 컨슈머가 메시지를 처리할 때,

    • 메시지의 고유 식별자(eventId/messageId)를 기준으로
    • 이미 처리했는지(Inbox 테이블/스토어로) 확인하고
    • 중복이면 무시(멱등) 하도록 보장한다.

즉 “수신 측에서 중복 처리를 방지해서 정확히 한 번처럼 보이게” 만드는 구조.



🔹 depth.2 최소 구성요소

🔸 Outbox 최소 구성

  1. Outbox 테이블

    • id(eventId, UUID 등), aggregate_type, aggregate_id, event_type
    • payload(JSON), status(PENDING/SENT/FAILED), created_at
  2. 업무 트랜잭션 안에서 Outbox 레코드 저장

    • 예: 주문 저장 + outbox insert를 같은 트랜잭션으로
  3. Outbox 퍼블리셔(폴링 워커)

    • 주기적으로 status=PENDING 레코드를 조회 → Kafka 발행 → 성공 시 SENT로 업데이트
  4. 재시도 정책(최소)

    • 실패하면 FAILED로 남기고 다음 폴링에서 재시도(또는 backoff)

최소 구성에서는 “폴링 기반”이 가장 단순


🔸 Inbox 최소 구성

  1. Inbox 테이블(Processed Messages)

    • event_id(유니크), consumer_name, processed_at
  2. 컨슘 시 처리 순서

    • 트랜잭션 시작
    • event_id가 Inbox에 이미 있으면 → 종료(중복 무시)
    • 없으면 → 업무 로직 처리(상태 변경) + Inbox insert
    • 커밋

핵심은 “업무 처리”와 “처리완료 기록”을 같은 트랜잭션으로 묶는 것.



🔹 depth.3 도입 시 추가로 신경 써야 하는 것

🔸 이벤트 “키”와 “중복” 설계

  • Inbox가 있으려면 eventId가 반드시 고유해야 하고, 컨슈머가 그걸 신뢰해야 한다.

  • eventId 생성 주체

    • Outbox 레코드 id를 eventId로 쓰는 게 가장 깔끔(발행되는 메시지에 그대로 포함)

🔸 Outbox 퍼블리셔의 동시성/락

  • 퍼블리셔가 2개 이상 떠도 안전해야 함(이중 발행 방지)

  • 방법(선택지)

    • SELECT … FOR UPDATE SKIP LOCKED
    • 상태를 PENDING -> SENDING으로 CAS 업데이트 후 발행
    • 배치 단위로 “claim” 후 처리

🔸 발행 성공 판정과 브로커 ack

  • “Kafka 발행 성공”을 어디까지로 볼지

    • 프로듀서 ack(브로커에 기록 완료)까지 받으면 성공 처리
  • 실패/타임아웃 시: 실제론 기록됐는데 타임아웃일 수 있어 → 중복 발행 가능성을 전제로 Inbox가 필요해짐(서로 보완 관계)


🔸 메시지 순서 보장(aggregate 단위)

  • 주문 같은 aggregate는 “생성→상태변경” 순서가 중요함
  • Kafka를 쓴다면 partition key를 aggregateId(orderId) 로 고정해서 순서가 깨지지 않게

🔸 스키마 진화와 호환성

  • payload(JSON)의 필드가 바뀌면 컨슈머가 깨지기 쉬움

  • 최소 방어:

    • event_type + version 넣기(OrderCreatedV1 같은 네이밍)
    • 컨슈머는 모르는 필드는 무시할 수 있게(유연한 역직렬화)

🔸 장애/모니터링/운영

  • Outbox에 FAILED가 쌓이면 “업무는 됐는데 이벤트가 안 나감” 상태

  • 최소한 필요:

    • FAILED 건수 알람
    • 오래된 PENDING 감지
    • DLQ(Dead Letter) 또는 수동 재처리 도구


🔹 depth.4 확장 시 추천 체크리스트

  • CDC 기반 Outbox (Debezium)
    폴링 대신 DB binlog를 읽어 Kafka로 내보내 성능/지연 개선

  • Exactly-once처럼 보이게 만들기 조합

    • 생산: Outbox(또는 Kafka 트랜잭션)
    • 소비: Inbox + 멱등 업데이트(유니크 제약/업서트)
  • Idempotency Key를 도메인 명령에도 적용

    • “주문 생성 요청” 자체도 중복 호출될 수 있으면 API 레벨에도 idempotency key 추가
  • 리플레이 전략

    • Inbox 보관 기간(TTL) 결정: 영구 보관 vs 일정 기간 후 정리
  • 대용량 최적화

    • outbox/inbox 파티셔닝, 아카이빙, payload 분리(큰 payload는 object storage + pointer)


🔹 현재 내 프로젝트 기준으로 봤을 때

  • order-server

    • 주문 생성 트랜잭션에 Outbox 기록: OrderAfterCreateV1
  • hub-server

    • OrderAfterCreateV1 컨슘 시 Inbox로 중복 제거
    • 허브경로 계산 결과 저장 + Outbox로 HubRouteAfterCreateV1
  • delivery-server

    • HubRouteAfterCreateV1 컨슘 시 Inbox
    • HubDelivery/FirmDelivery 생성(트랜잭션)
profile
꾸준한 공부만이 답이다

0개의 댓글