usage_event_outbox는 notification 발행 상태를 남기기 위한 테이블이다.
현재 구조의 핵심은 아래와 같다.
- 모든 usage 이벤트를 저장하지 않는다.
- notification 대상 이벤트만 저장한다.
- 저장되는 payload는
EventEnvelope가 아니라NotificationPayloadJSON이다. - 즉시 발행 실패 시
PUBLISH_PENDING이 남는다.
즉 이 테이블은 usage 이벤트 전체 로그가 아니라, notification 발행과 복구를 위한 상태 저장소다.
초기에는 usage 처리 전체를 더 넓게 추적하는 방향도 고려할 수 있었지만, 현재 구조는 notification 대상 이벤트만 저장하는 방향으로 정리되었다.
이렇게 정리한 이유는 아래와 같다.
- usage 도메인에서는 notification 비대상 이벤트 비율이 높을 수 있다.
- 고TPS 환경에서는 모든 이벤트를 DB row로 남기는 방식이 부담이 커진다.
- 실제로 복구가 필요한 지점은 notification 발행 실패 구간이다.
그래서 현재 Outbox는 아래 역할에 집중한다.
- notification 대상 확정
- 즉시 발행 후 상태 추적
- 후속 복구 프로세스 인계
주요 컬럼 의미:
event_id: 원본 usage 이벤트 식별자family_id: 가족 IDcustomer_id: trigger 사용자 IDstatus:PUBLISH_PENDING,SENT,FAILEDpayload_json: 최종NotificationPayloadJSONretry_count,next_retry_at,last_error: 복구 프로세스 상태 관리용
여기서 event_id는 notification 이벤트 id가 아니라, 원본 usage 이벤트 id다.
즉 이 row가 어떤 usage 이벤트에서 파생됐는지를 추적하는 키다.
Outbox에는 EventEnvelope가 아니라 최종 NotificationPayload JSON을 저장한다.
이렇게 한 이유는 아래와 같다.
- 즉시 발행과 복구 발행이 같은 payload를 사용하게 하기 위해서
- 복구 프로세스가 payload를 다시 조립하지 않고 바로 재발행할 수 있게 하기 위해서
- notification의 business payload와 Kafka envelope 생성을 분리하기 위해서
즉 Outbox는 “무엇을 보낼 것인가”를 저장하고, 실제 envelope 생성은 발행 시점에 수행한다.
- notification 발행 대상 확정 완료
- 아직 성공적으로 마감되지 않은 상태
이 상태는 아래 상황을 포함한다.
- 즉시 발행 전
- 즉시 발행 실패 후
- 후속 복구 프로세스 대기 중
- notification Kafka publish 성공이 반영된 상태
- 복구 프로세스가 최종 실패로 마감한 상태
usage 서비스는 notification을 즉시 비동기로 먼저 발행한다.
- 성공 callback이면
SENT - 실패 callback이면
PUBLISH_PENDING유지
즉 usage 서비스는 즉시성을 담당하고, 복구 프로세스는 pending row를 기준으로 후속 발행을 수행한다.
이 구조를 사용하면:
- 정상 케이스에서는 즉시 알림 전파
- 실패 케이스에서는 row 기준 복구 가 가능해진다.
lib-kafka 분류 기준:
IllegalArgumentException->IGNOREKafkaMessageProcessingException->RETRYNonRetryableKafkaMessageProcessingException->DLQ
현재 이 레포는 consumer retry 설정을 별도로 override하지 않는다.
즉 usage 이벤트 처리 도중 발생한 retryable 예외는 consumer 레벨에서 다시 처리된다.
같은 eventId가 다시 들어오면 아래가 다시 수행될 수 있다.
- Redis warmup
- Lua 실행
- DB 정산
- pending notification 재발행 시도
이 레이어는 duplicate와 retry를 엄격히 구분하기보다, 같은 이벤트 재진입을 안전하게 흡수하는 역할을 한다.
복구 프로세스는 PUBLISH_PENDING을 대상으로 아래 순서로 동작한다.
PUBLISH_PENDING조회payload_json역직렬화notification-events재발행- 성공 시
SENT반영 - 필요 시
retry_count,next_retry_at,last_error갱신 - 최종 실패 시
FAILED반영
즉 usage 서비스는 즉시 발행까지, 복구 프로세스는 pending row 후속 발행까지 담당한다.
| 유형 | 처리 방식 |
|---|---|
| invalid payload / 잘못된 family-customer | IGNORE |
| membership lookup, Redis warmup, Lua, DB 정산, Outbox 저장 실패 | RETRY |
| 상태 계약 불일치 / 복구 불가 직렬화 오류 | DLQ |
| 즉시 notification 발행 실패 | PUBLISH_PENDING 유지 |
sequenceDiagram
participant U as Usage Service
participant O as Outbox
participant N as Kafka notification-events
participant B as External Recovery Process
U->>O: PUBLISH_PENDING 저장
U->>N: 즉시 비동기 발행
alt ack success
U->>O: SENT 반영
else ack fail
U->>O: PUBLISH_PENDING 유지
B->>O: PUBLISH_PENDING 조회
B->>N: 재발행
B->>O: SENT 또는 FAILED 반영
end
현재 Outbox 설계의 핵심은 아래 세 가지다.
- notification 대상 이벤트만 저장한다.
- 불필요한 row를 줄인다.
- 고TPS 환경에서 DB hot path 비용을 줄인다.
- payload를 최종 notification 형태로 저장한다.
- 즉시 발행과 복구 발행이 같은 데이터를 사용한다.
- 복구 프로세스가 단순해진다.
- 즉시성과 복구성을 분리한다.
- usage 서비스는 즉시 발행을 담당
- Outbox와 복구 프로세스는 실패 후 후속 발행을 담당