Files
llm-wiki/raw/blog-topics/skip-locked-outbox-per-aggregate-fifo-gate-2026-06-11.md
T

5.6 KiB
Raw Blame History

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
feature-domain-event-outbox-contract
ca-tmpl
blog-topic
ca-tmpl
outbox
skip-locked
fifo
postgresql
testcontainers
clean-architecture
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 / 부모

트리거 / 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 skip PUBLISHED 마킹 — 이벤트 갭 승인 필요)으로 설계. backlog 증가는 outbox.pending.size P2 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 EXISTS head gate는 앞선 미발행 row가 있을 때 tail claim을 막는 방식이다.
  • 의견/해석 후보:
    • strict FIFO는 poison event가 aggregate stream을 멈추는 운영 비용을 동반한다.

Outline seed

  1. SKIP LOCKED가 해결하는 문제와 새로 만드는 ordering 문제를 분리한다.
  2. head gate query로 per-aggregate FIFO를 보강한다.
  3. 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 / 근거 후보

미해결 / Unknown

  • 아직 확인해야 할 사실: 현재 canonical의 구현 없음 기록과 raw seed의 검증 주장 간 차이.
  • 과장하면 안 되는 부분: SKIP LOCKED가 ordering을 자동 보장한다고 쓰지 않는다.

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 기준으로 재확인한다.