--- title: Dual-Write Anti-pattern (microservices.io / 일반 정설) source_type: official-doc url: https://microservices.io/patterns/data/application-events.html archive_url: status: raw confidence: medium tags: [ca-outbox-pattern, dual-write, anti-pattern, failure-case, microservices-io, official-doc] related_projects: [ca-skeleton-operational-contract] related_branches: [feature-domain-event-outbox-contract, feature-background-job-async-contract] created: 2026-05-22 last_reviewed: 2026-05-27 --- # Dual-Write Anti-pattern (microservices.io) > Layer: `raw/official-docs/` — microservices.io "Pattern: Application events" 및 "Transactional outbox" 페이지의 problem 섹션 **원문 발췌·출처 기록**. > ca-tmpl 의 SKIP LOCKED outbox 결정에 대한 **대안 3: dual-write (DB 와 broker 에 직접 동시 쓰기)**. **실패 케이스**로 조사 — 왜 outbox 가 필요한가의 negative case. ## Parent / 활용 branch (필수) | Branch | 이 자료가 정당화하는 결정 | |---|---| | [[raw/branch-notes/feature-domain-event-outbox-contract]] | Topic 3 — Outbox Pattern 대안 매트릭스에서 dual-write 를 negative reference 로 채택한 결정의 1차 근거 — 분산 트랜잭션 불가 + 부분 실패 silent divergence | | [[raw/branch-notes/feature-background-job-async-contract]] | Background job 발행에서 `save(); publish();` 직접 호출 패턴이 금지되는 근거 | | [[raw/project-notes/ca-skeleton-operational-contract]] | §18 / §19 의 outbox 채택 정당화 — dual-write 의 명시적 금지 reference | ## 컨텍스트 ca-tmpl 의 SKIP LOCKED outbox 결정에 대한 **대안 3: dual-write (DB 와 broker 에 직접 동시 쓰기)**. **실패 케이스**로 조사 — outbox 패턴 도입의 직접적 동기. dual-write 는 단일 DB + 단일 broker 환경에서도 atomic 보장이 불가능하며, 부분 실패 시 lost event / phantom event 가 발생한다. ## 출처 / Source - 원본 URL: https://microservices.io/patterns/data/application-events.html - 보조 URL: https://microservices.io/patterns/data/transactional-outbox.html (problem 섹션) - 보조: Confluent / Debezium 다수 글에서 동일한 "dual-write 금지" 메시지를 반복 - 아카이브 URL: (미수집) - 저자 / 조직: Chris Richardson — microservices.io - 발행일: rolling docs (페이지 자체에 명시 없음) - 마지막 확인일 (capture): 2026-05-22 - 마지막 재검증 시도: 2026-05-27 - **[2026-05-25 capture]**: user 가 2026-05-22 수집한 인용 원형 유지 (재검증 보류 마커는 2026-05-25 부여된 상태). - **재검증 결과 [2026-05-27 verified attempt]**: - 1차 URL `https://microservices.io/patterns/data/application-events.html` WebFetch 결과 **REDIRECT 상태** — 페이지는 `transactional-outbox.html` 로의 redirect notice 만 남음. 즉 user 가 인용한 "Application events" 본문 자체가 이제 1차 URL 에서 직접 노출되지 않음 (microservices.io 가 페이지를 통합한 것으로 추정). - 보조로 redirect 대상 `transactional-outbox.html` 도 WebFetch 했으나 본 raw 의 3개 quote (Problem / Failure mode / Crash scenario) 는 모두 발췌 결과에서 NOT FOUND. - WebFetch 가 페이지 전체를 노출하지 않을 수 있어 NOT FOUND 가 absence 의 결정적 증거는 아니나, **출처 페이지 자체가 redirect 로 바뀌어** verbatim 위치를 더는 1차 URL 로 가리킬 수 없는 상황. - **재검증 한계 + Strength 정책**: 출처 URL 의 redirect 발생 + 3개 quote 모두 redirect 대상 페이지에서 verbatim NOT FOUND → 본 문서 인용은 모두 `needs-confirmation` Strength **유지** (Strength 상향 없음). wiki 승급 전 다음 중 하나 필요: (a) archive.org 스냅샷으로 원본 "Application events" 페이지 wording 복원 + 인용 위치 확정, (b) 동등한 내용을 명시한 다른 1차 source (Chris Richardson 책 / Confluent / Debezium) 로 cross-reference. ## 핵심 인용 / Key quotes (verbatim claimed, user 수집본 [2026-05-25 capture] — [2026-05-27 verified attempt] 출처 URL redirect + verbatim NOT FOUND) > [§Application events — Problem] "A service often needs to atomically update the database and publish a message/event. It is not viable to use a distributed transaction that spans the database and the message broker." > — [2026-05-27 verified attempt]: 출처 페이지 redirect → `transactional-outbox.html` 에서 NOT FOUND. > [§Application events — Failure mode] "Without 2PC, writing to the database and then publishing to a broker — or vice versa — can result in inconsistency if either step fails." > — [2026-05-27 verified attempt]: redirect 대상 페이지에서 NOT FOUND. > [§Application events — Crash scenario] "Even if both calls succeed individually, a process crash between them leaves the system in an inconsistent state." > — [2026-05-27 verified attempt]: redirect 대상 페이지에서 NOT FOUND. ## Claims Extracted / 추출된 주장 > 본 raw 의 모든 quote 는 2026-05-22 user 수집본이며 2026-05-27 WebFetch 차단으로 verbatim 재확인 보류 → 전체 `needs-confirmation`. | Claim ID | Claim (이 자료가 직접 말하는 것) | Evidence quote | Strength | Applies to | Does not prove | |---|---|---|---|---|---| | DUAL-WRITE-C1 | 서비스는 종종 DB 갱신과 메시지 발행을 atomic 하게 해야 하지만, DB 와 message broker 에 걸친 distributed transaction 사용은 viable 하지 않다 | [§Application events — Problem] "A service often needs to atomically update the database and publish a message/event. It is not viable to use a distributed transaction that spans the database and the message broker." | `needs-confirmation` | DB + broker 를 동시에 다루는 모든 서비스 | "어떤 환경에서도 절대 불가" 는 아님 — 본 인용은 일반적 viability 부정, 일부 broker 의 XA 지원은 별도 검증 | | DUAL-WRITE-C2 | 2PC 없이 DB 에 쓰고 broker 에 발행 (또는 그 반대) 하는 경우, 어느 한쪽이 실패하면 inconsistency 가 발생할 수 있다 | [§Application events — Failure mode] "Without 2PC, writing to the database and then publishing to a broker — or vice versa — can result in inconsistency if either step fails." | `needs-confirmation` | 2PC 없이 DB + broker 를 순차 호출하는 모든 패턴 | inconsistency 의 정확한 형태 (lost event vs phantom event) 분류는 본 인용에 포함되지 않음 | | DUAL-WRITE-C3 | 두 호출이 개별적으로 성공하더라도, 그 사이에 프로세스 크래시가 발생하면 시스템은 inconsistent state 가 된다 | [§Application events — Crash scenario] "Even if both calls succeed individually, a process crash between them leaves the system in an inconsistent state." | `needs-confirmation` | DB commit 과 broker publish 사이의 임의 지점에서 프로세스 종료 가능한 모든 환경 | crash recovery 메커니즘 (retry, compensation) 으로 이를 해결 가능한지 본 인용은 침묵 — outbox 가 그 해결책임은 별도 인용 (transactional outbox 페이지) | ### Strength 정책 본 문서의 모든 claim 은 `needs-confirmation`. 이유: 2026-05-27 재검증 시점에 WebFetch 가 차단되어 user 수집본 (2026-05-22) 의 verbatim 일치 여부를 공식 페이지 대조로 확인 못함. wiki 승급 전 재확인 필수. ## Usage Boundaries / 적용 경계 - **이 자료가 직접 증명하는 것** (재확인 시): - `DUAL-WRITE-C1`: 분산 트랜잭션 (DB + broker 2PC) 의 viability 부정 — outbox 도입의 1차 동기 - `DUAL-WRITE-C2`: 순차 호출 시 부분 실패 = inconsistency - `DUAL-WRITE-C3`: 두 호출 사이의 crash 도 inconsistency 원인 (성공 호출만으로는 안전 불가) - **이 자료가 증명하지 않는 것**: - 모든 broker (Kafka, RabbitMQ, SQS, ...) 가 2PC 를 지원하지 않는다는 절대 명제 (Kafka 는 transaction API 가 있지만 외부 DB 와의 2PC 는 별도 논의) - lost event 와 phantom event 의 명시적 분류 (메모 영역에서 해석 필요) - dual-write 가 모든 시나리오에서 항상 잘못된 선택이라는 일반화 (low-criticality 도메인에서 monitoring 으로 운영 가능한 케이스도 존재 — 본 인용은 silent divergence 위험만 지적) - **내 프로젝트에 적용하려면 추가 확인이 필요한 것**: - ca-tmpl 의 도메인 이벤트가 silent loss 를 허용할 수 없는 critical 도메인인지 확인 (그렇다면 outbox 채택이 정당화) - dual-write 가 잘못이라는 결론 자체는 microservices.io 1차 source 외에 Confluent / Debezium / Stripe 의 동일 메시지로 corroborate 가능 (다중 source 권장) ## 메모 / Notes (내 프로젝트 해석 — 직접 인용 아님) - 적용 시나리오: **권장하지 않음.** 단일 DB + 단일 broker 라도 atomic 보장 불가. - 장점: - 코드가 가장 단순 (`save(); publish();`) - 인프라 추가 없음 - 단점 (failure modes — 본 인용의 일반 inconsistency 명제로부터 도출): - **DB commit 성공 + broker publish 실패** → 외부에는 이벤트 안 감, 상태만 변함 (lost event) - **broker publish 성공 + DB commit rollback** → 외부에는 발생하지 않은 이벤트 발행 (phantom event) - **DB commit 성공 + 프로세스 크래시 → publish 안 됨** (lost event, `DUAL-WRITE-C3` 의 직접 결과) - 분산 트랜잭션 (XA/2PC) 은 broker 측 지원 미흡/성능 문제로 사실상 불가 (`DUAL-WRITE-C1`) - ca-tmpl (SKIP LOCKED polling) 과의 차이: - outbox 는 "이벤트도 DB 에 같이 쓴다" 로 atomic 문제를 회피 - dual-write 는 이 atomic 문제를 그대로 노출 → outbox 도입의 직접적 동기 - 운영 복잡도: 코드는 낮음, 장애 디버깅 비용은 매우 높음 (silent data divergence). - exactly-once / at-least-once 보장 수준: **보장 없음**. lost / phantom 둘 다 가능. - 외부 의존성 추가 여부: 없음 (그러나 그 대가가 신뢰성 손실). - 결론: ca-tmpl 이 dual-write 를 피하고 outbox 를 택한 것은 정설. 이 문서는 "대안"이 아니라 "왜 outbox 를 골랐는가의 negative reference". - 대안 그룹 (Topic 3 — Outbox Pattern, 대안 6종): SKIP LOCKED polling / Debezium CDC / Kafka Connect SMT / **Dual-write [금지]** / Event sourcing / Spring @TransactionalEventListener - 본 source 의 위치: negative reference — Dual-write 금지 ## Related / 관련 - 같은 주제 다른 official-doc: - [[raw/official-docs/outbox-skip-locked-microservices-io]] (baseline: SKIP LOCKED polling — dual-write 의 해결책) - [[raw/official-docs/outbox-debezium-official-docs]] (대안 1: CDC) - [[raw/official-docs/skip-locked-postgres-docs]] (메커니즘) - 같은 주제 company-tech-blog: - [[raw/company-tech-blogs/outbox-woowahan-techblog-pattern]] - 인용하는 branch / project: - [[raw/branch-notes/feature-domain-event-outbox-contract]] - [[raw/branch-notes/feature-background-job-async-contract]] - [[raw/project-notes/ca-skeleton-operational-contract]] - 인용한 wiki 요약: (미작성)