EVD-335 outbox 가 둘이다 — 배선된 것과 어두운 것 측정일 2026-08-31 · revision 21234e38 · workmachine ──────────────────────────────────────────────────────────────────────── 1. 무엇을 다시 쟀나 ──────────────────────────────────────────────────────────────────────── analysis/19 §7.1 과 root-tree 의 case:outbox-chain-behind-an-unsatisfiable-condition 은 "MessagingReliabilityAutoConfiguration 의 @ConditionalOnBean 사슬이 만족되지 않아 outbox/inbox 19개 main 파일 2,818 LOC 가 조용히 비어 있다"로 판정했다. 그 판정은 사실이다. 다시 잰 이유는 스타터의 클래스 javadoc 이 그것을 결함이 아니라 계약으로 서술하기 때문이다. messaging-spring-boot-starter/.../MessagingReliabilityAutoConfiguration.java 클래스 javadoc "Every bean here is conditional on the application having supplied the corresponding repository. The platform cannot provide those: they write inside the application's own transaction, against the application's own datasource, and a default implementation would silently write to the wrong place — or to nowhere at all, which is worse because the outbox would look healthy while nothing was ever staged." 그렇다면 남는 질문은 하나다. 이 저장소의 출하 애플리케이션은 그 계약을 이행하는가. starter 리프 SSOT 도 그 질문을 app-bootstrap 재검증으로 넘겨 두었다 (analysis/messaging/messaging-spring-boot-starter.md:815). ──────────────────────────────────────────────────────────────────────── 2. 답 — 이행하지 않는다. 대신 자기 outbox 를 갖고 있다 ──────────────────────────────────────────────────────────────────────── 이 저장소에는 서로를 모르는 outbox 구현이 둘 있다. [스택 A — 배선되어 출하된다] 포트 application-core/.../application/outbox/ OutboxStorePort · OutboxAppendPort · OutboxMessagePublishPort PublishPendingOutboxEventsUseCase · OutboxBackoffPolicy · OutboxPayloadPolicy 외 15파일 구현 adapter/outbound/persistence-jpa/.../persistence/outbox/OutboxStoreAdapter.java:23-24 @Repository public class OutboxStoreAdapter implements OutboxAppendPort, OutboxStorePort → 스테레오타입이 붙어 있어 컴포넌트 스캔으로 컨텍스트에 들어간다 구동 app-bootstrap/.../bootstrap/outbox/OutboxConfig.java @ConditionalOnProperty(prefix="ca-skeleton.outbox", name="enabled", havingValue="true") OutboxConfig.java:64-65 new PublishPendingOutboxEventsUseCase(...) app-bootstrap/.../bootstrap/outbox/OutboxRelayScheduler.java:34 @Scheduled(fixedDelayString = "${ca-skeleton.outbox.poll-interval:PT5S}") 부속: OutboxMetrics · OutboxLeaderElectionToken · OutboxSettings · OutboxRelayBrokerRequirementValidator [스택 B — 어둡다] 포트 messaging/messaging-reliability-api/.../reliability/OutboxRepository.java 구현 messaging/messaging-outbox-jdbc-postgresql/.../JdbcOutboxRepository.java:48 public final class JdbcOutboxRepository implements OutboxRepository → 스프링 스테레오타입 없음 조립 messaging-spring-boot-starter/.../MessagingReliabilityAutoConfiguration.java:81 @ConditionalOnBean({OutboxRepository.class, OutboxEnvelopeFactory.class}) 협력자 OutboxRelay · OutboxRelayWorker · MessagingOutboxRelayLifecycle · OutboxCleanupJob · OutboxEnvelopeFactory ──────────────────────────────────────────────────────────────────────── 3. 스택 B 가 조립될 수 없음을 확정한 측정 ──────────────────────────────────────────────────────────────────────── $ grep -rn "new JdbcOutboxRepository|new JdbcInboxRepository|new OutboxEnvelopeFactory" \ --include=*.java . | grep "/src/main/" (일치 0) $ grep -rn "Outbox|Inbox" app-bootstrap/src/main --include=*.java | grep -iE "@Bean|new Jdbc|Repository " (일치 0) 타입별 구현 수와 main 참조 파일 수: OutboxRepository 구현 4 (main 1 = JdbcOutboxRepository, 나머지 3은 test) main참조 4 OutboxEnvelopeFactory 구현 0 (final class, 생성자 OutboxEnvelopeFactory(ProducerId)) main참조 2 InboxRepository 구현 3 (main 1) main참조 4 OutboxRelay 구현 0 main참조 2 OutboxRelayWorker 구현 0 main참조 2 OutboxEnvelopeFactory 를 @Bean 으로 만드는 곳은 test 하나뿐이다: messaging-spring-boot-starter/src/test/.../MessagingOutboxRelayLifecycleTest.java:85-86 따라서 81행의 두 조건 중 어느 것도 프로덕션에서 참이 되지 않는다. ──────────────────────────────────────────────────────────────────────── 4. 교정된 판정 ──────────────────────────────────────────────────────────────────────── "만족될 수 없는 조건 뒤에 사슬이 있다"는 서술은 현상으로는 맞지만 원인을 가린다. 정확한 서술은 이것이다. 이 저장소는 outbox 를 두 번 구현했고, 출하하는 것은 application-core 쪽이다. messaging 플랫폼의 outbox(2,818 LOC)는 그 결과로 어둡다. 이 차이가 중요한 이유는 수정 방향이 반대이기 때문이다. "조건이 만족되지 않는다"로 읽으면 → app-bootstrap 에 OutboxRepository 빈을 등록하는 수정 "outbox 가 둘이다"로 읽으면 → 어느 쪽이 정본인지 먼저 결정해야 하는 문제 후자가 맞다. 두 스택은 저장 모델도 다르고(OutboxStorePort vs OutboxRepository), 발행 경로도 다르며(OutboxMessagePublishPort vs OutboxRelay + OutboxEnvelopeFactory), 둘을 동시에 켜면 같은 업무 이벤트가 두 테이블에 적히거나 두 번 발행될 수 있다. 스타터의 javadoc 은 relay 를 자기가 구동하는 이유를 이렇게 적는다 — "nobody running one is a table that fills up behind a business transaction that reported success." 그 경고는 스택 B 의 테이블에 대해서는 무의미하다. 그 테이블에 적는 코드가 배선되어 있지 않으므로 채워질 일도 없다. 즉 스택 B 는 위험하지 않고, 다만 존재하지 않는다. ──────────────────────────────────────────────────────────────────────── 5. 이 측정이 바꾸는 문서 ──────────────────────────────────────────────────────────────────────── analysis/19-messaging-platform.md §7.1 — 원인 서술 보강 analysis/messaging/messaging-spring-boot-starter.md:815 — 열린 항목 종결 root-tree.md case:outbox-chain-behind-an-unsatisfiable-condition — 분류 교체 애플리케이션 소스는 수정하지 않았다.