3.9 KiB
title, source_type, status, related_branches, related_projects, tags, created
| title | source_type | status | related_branches | related_projects | tags | created | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| interview / transactional-outbox-skip-locked-implementation-2026-06-11 | interview-prep | raw |
|
|
|
2026-06-11 |
interview: transactional outbox (SKIP LOCKED polling) 구현
Layer:
raw/interviews/— feature-domain-event-outbox-contract 실구현(Phase C2)에서 정직하게 나올 수 있는 질문.
Parent / 부모
예상 질문 / Q&A
-
Q. 왜 outbox 인가? 그냥 트랜잭션 커밋 후 publish 하면 안 되나? A. dual-write 문제. DB 커밋과 broker publish 는 원자적으로 묶을 수 없다(2PC 비현실적). 커밋 후 publish 전에 프로세스가 죽으면 이벤트 유실. outbox 는 상태 변경과 같은 트랜잭션으로 outbox row 를 insert 하고(if-and-only-if commit), 별도 relay 가 폴링해 발행한다. ca-tmpl 에서는
OutboxAppendPort.append를 use case 의tx.inWrite안에서 호출하고, rollback 시 row 가 없음을 계약 테스트(OutboxAppendTransactionalContractTest)로 고정했다. -
Q. 멀티 인스턴스에서 같은 이벤트를 두 publisher 가 잡지 않는 보장은? A. 별도 leader election 인프라(Redis/ZooKeeper) 없이 DB row-level claim:
SELECT ... FOR UPDATE SKIP LOCKED. 락을 못 잡는 row 는 대기 없이 skip 되므로 두 인스턴스가 서로 다른 row 를 가져간다. 계약 테스트로 2개 Spring context 가 1000 row 를 나눠 발행해 합계 1000·중복 0 을 단언했다.StartupSafetyValidator는 multi-instance 모드에서outboxLeaderElectionbean 존재를 기동 시 강제한다. -
Q. per-aggregate 순서(FIFO)는 어떻게 보장하나? SKIP LOCKED 는 순서를 깨지 않나? A. 깬다(공식 문서가 inconsistent view 명시). 그래서 claim query 에 게이트를 넣었다:
NOT EXISTS (같은 aggregate 의 더 이른 occurred_at row 가 PUBLISHED 가 아닌 상태)— 배치에는 aggregate 당 head 1건만 들어온다. head 가 FAILED/IN_FLIGHT/DEAD 인 동안 후행은 차단(strict FIFO). 트레이드오프: poison event 1건이 그 aggregate 스트림을 멈춤 → DEAD runbook 의 수동 처분(재발행 또는 skip)으로 해제. global ordering 은 보장하지 않는다고 명시. -
Q. publisher 가 claim 후 죽으면(IN_FLIGHT orphan)? A. 별도 컬럼 없이
next_attempt_at을 visibility timeout 으로 재사용: claim 시IN_FLIGHT + next_attempt_at = now + PT5M. 만료된 IN_FLIGHT 는 재claim 가능. publish 직후 상태 갱신 전 crash 면 재발행되므로 at-least-once — consumer 의 idempotencyKey dedupe 가 흡수(계약 테스트: 동일 key 5회 전달 → 1회 처리). -
Q. 발행 실패 분류는? A. 일시 실패 →
OUTBOX_PUBLISH_FAILED(TRANSIENT_DEPENDENCY) + FAILED + 지수 backoff(30s × 2^(n-1) + full jitter), max attempts 3 소진 →OUTBOX_DEAD_LETTER(INTERNAL) + DEAD + runbook. 분류 코드·메트릭(outbox.publisher.published.total{outcome},outbox.pending.size{status},outbox.publisher.lag)은 registry 기존 값 재사용. -
Q. 기존 KafkaMessagePublisher 가 fail-open(실패 삼킴)인데 relay 가 그걸 쓰면? A. 못 쓴다 — relay 는 실패를 봐야 FAILED/DEAD 상태머신을 돌린다. 그래서 fail-closed 전용 포트(
OutboxMessagePublishPort)를 application-core 에 정의하고 adapter-outbound 에서KafkaSenderseam 에 직결해 예외를 전파시켰다. fail-open 경로는 "use case 직발행 + outbox 가 durability 담당" 시나리오용이라 공존이 맞다. -
Q. claim 트랜잭션 isolation 은? A.
READ_COMMITTED명시 pin (vendor default 금지 — MySQL InnoDB 는 REPEATABLE READ). claim 은 짧고 단일 배치 단위라 SERIALIZABLE 불필요. publish 는 claim 트랜잭션 밖에서 수행.