5.6 KiB
5.6 KiB
title, source_type, status, related_branches, related_projects, tags, created, status_label, target_audience, inspiration_url, archive_url
| title | source_type | status | related_branches | related_projects | tags | created | status_label | target_audience | inspiration_url | archive_url | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| blog-topic / skip-locked-outbox-per-aggregate-fifo-gate-2026-06-11 | blog-topic | raw |
|
|
|
2026-06-11 | ready-for-canonical | backend-engineer |
blog-topic: skip-locked-outbox-per-aggregate-fifo-gate-2026-06-11
Layer:
raw/blog-topics/— 채용공고가 아닌 작업·학습·트러블슈팅에서 나온 블로그 글감 원석. canonical 정제 전 raw 후보이며,wiki/blog/직접 생성 근거가 아니다.
Parent / 부모
- raw/branch-notes/feature-domain-event-outbox-contract — outbox relay 구현 중 SKIP LOCKED 와 per-aggregate FIFO 의 충돌을 claim query 의
NOT EXISTS게이트로 해소한 경험 단독 추출.
트리거 / Trigger
- 트리거 유형:
branch-work FOR UPDATE SKIP LOCKED폴링 outbox 는 멀티 인스턴스 claim 경합을 우아하게 풀지만, PostgreSQL/MySQL 공식 문서가 명시하듯 순서를 깬다(inconsistent view). "per-aggregate FIFO 보장" 계약과 정면 충돌 — 1차 구현이 실제로 게이트를 빠뜨려 리뷰에서 잡혔고(head FAILED 인데 tail 이 먼저 발행되는 경로), 수정 과정 자체가 글감.
글감 코어 / Core idea
- 문제: SKIP LOCKED 는 "락 못 잡으면 건너뛴다" — 같은 aggregate 의 이벤트 e1, e2 가 서로 다른 publisher 에 분산 claim 되거나, e1 이 FAILED(backoff 대기) 인 동안 e2 가 먼저 나가면 consumer 가 순서 역전을 본다.
- 해결: claim query 에 상관 서브쿼리 게이트 —
NOT EXISTS (SELECT 1 FROM outbox_event p WHERE p.aggregate_id = o.aggregate_id AND p.occurred_at < o.occurred_at AND p.status <> 'PUBLISHED'). 배치에는 aggregate 당 head 1건만 들어오고, head 가 비-PUBLISHED(FAILED/IN_FLIGHT/DEAD 포함)인 동안 후행은 구조적으로 claim 불가. READ_COMMITTED 스냅숏을 읽는 게이트라 보수적(차단 우위)으로 동작. - 트레이드오프 (strict FIFO): DEAD 가 후행을 영구 차단 → poison event 1건이 aggregate 스트림을 멈춘다. 자동 우회 대신 runbook 수동 처분(재발행
PENDING리셋 vs skipPUBLISHED마킹 — 이벤트 갭 승인 필요)으로 설계. backlog 증가는outbox.pending.sizeP2 alert 가 감지. - 검증: Testcontainers PG 계약 테스트 3종 — ① 2개 Spring context × 1000 rows 동시 claim, 합계 1000·중복 0 (SKIP LOCKED 단일 claim), ② head FAILED/DEAD 시 tail 차단·head PUBLISHED 후 해제 (FIFO 게이트), ③
next_attempt_at을 visibility timeout 으로 재사용한 IN_FLIGHT orphan 재claim. - 부가 발견: 공유 HikariDataSource 를 두 context 에 등록하면 첫 close 가 풀을 닫는다(
setDestroyMethodName("")필요) — raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11.
왜 의미 있나 / Why it matters
- 국내외 outbox 글 대부분이 "SKIP LOCKED 로 폴링하면 된다"에서 멈춘다. ordering 계약과의 충돌과 그 해소(쿼리 레벨 게이트 + strict FIFO 의 운영 비용 명문화 + 계약 테스트로 고정)까지 다루는 글은 드물다.
- fail-open publisher(use case 직발행)와 fail-closed publisher(outbox relay)가 한 코드베이스에 공존해야 하는 이유도 곁들일 수 있는 실전 소재.
글감 / Topic seed
- 한 문장 요지:
FOR UPDATE SKIP LOCKED는 claim 경합을 줄이지만 per-aggregate FIFO 보장과 충돌할 수 있어 head gate가 필요하다. - 예상 제목 후보:
- SKIP LOCKED outbox에서 순서를 지키는 방법
- per-aggregate FIFO를 깨지 않는 outbox claim query
핵심 주장 후보 / Claim candidates
- 사실 후보:
SKIP LOCKED는 잠긴 row를 skip하므로 동일 aggregate의 tail이 먼저 claim될 수 있다.NOT EXISTShead gate는 앞선 미발행 row가 있을 때 tail claim을 막는 방식이다.
- 의견/해석 후보:
- strict FIFO는 poison event가 aggregate stream을 멈추는 운영 비용을 동반한다.
Outline seed
- SKIP LOCKED가 해결하는 문제와 새로 만드는 ordering 문제를 분리한다.
- head gate query로 per-aggregate FIFO를 보강한다.
- DEAD row가 tail을 막는 strict FIFO의 운영 비용과 runbook 필요성을 설명한다.
Canonical 전환 후보 / Canonical extraction candidates
wiki/projects/ca-tmpl/transactional-outbox-pattern.md후보:- SKIP LOCKED vs per-aggregate FIFO gate 글감.
- 필요한 추가 검증:
- branch-note/code 기준 실제 Testcontainers 검증 여부.
Sources / 근거 후보
- raw/branch-notes/feature-domain-event-outbox-contract
- raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11
미해결 / Unknown
- 아직 확인해야 할 사실: 현재 canonical의 구현 없음 기록과 raw seed의 검증 주장 간 차이.
- 과장하면 안 되는 부분: SKIP LOCKED가 ordering을 자동 보장한다고 쓰지 않는다.
Related / 관련
- 관련 branch: raw/branch-notes/feature-domain-event-outbox-contract
- 관련 error: raw/errors/testcontainers-two-context-shared-datasource-close-2026-06-11
Decision / 처리 결정
- 액션:
promote-to-canonical - 이유:
wiki/projects/ca-tmpl/transactional-outbox-pattern.md에 SKIP LOCKED와 per-aggregate FIFO gate 글감으로 반영했다. - 다음 단계: target canonical이 아직
draft이므로blogify전 outbox 구현·Testcontainers 검증 여부를 branch-note/code 기준으로 재확인한다.