Files
document-haness/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-outbox-chain-behind-an-unsatisfiable-condition.md
T
DongHyeonkaandClaude Fable 5.1 b25357c48a docs(clean-architecture-backend-template): fold analysis into final and re-select one topic
- analysis/·source-index·state.json 을 final/document.md 제2부·제3부로 접었다. SSOT 는 하나다
- 파일럿 — commit-ambiguity-as-a-result 를 새 기준으로 재선별. 후보 14 → 글감 5
  (PROMOTE 5 · MERGE_INTO 3 · KEEP_IN_SSOT 4 · 보류 2). 기록 5건을 다시 썼고 그림 1개를
  techviz 로 만들었다
- 재선별이 잡은 것: 제1부 §6.2·§11.1 이 자기 §13.2 와 어긋나 있었다(레인을 안 돌렸다 vs
  돌렸다) — 정정. 이미 답이 나와 있던 Question 을 HEAD 재실행 질문으로 다시 세웠다.
  Concept 이 인용한 코드가 SSOT 에 없어 뺐다
- candidateScope·sourceRepository 기록. 나머지 43개 주제는 재선별 대기(PENDING 905)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-07 12:39:20 +09:00

8.1 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, body, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn body assets evidence source
CASE outbox-chain-behind-an-unsatisfiable-condition outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다 assembly-ownership clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:outbox-chain-behind-an-unsatisfiable-condition 2026-09-01 case-outbox-chain-behind-an-unsatisfiable-condition.body.md
key file
outbox-chain-behind-an-unsatisfiable-condition ../../../final/evidence/rendered/outbox-chain-behind-an-unsatisfiable-condition.svg
../../../final/evidence/raw/outbox-chain-behind-an-unsatisfiable-condition.txt
../../../final/evidence/raw/tl-outbox-unsatisfiable-condition.txt
원본 분석 절은 final/document.md#4-3 · final/document.md#a19 §7.1 이다.

outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다

messaging 플랫폼의 outbox 사슬은 조건이 참이 될 수 없어 조립되지 않는다. 원인은 조건 결함이 아니라 이 저장소가 outbox 를 두 번 구현했고 다른 쪽을 출하하기 때문이다.

관계

  • @Bean이 있다는 것은 조립 증거가 아니다 두 스택 중 하나만 컨텍스트에 들어간다는 것이 이 규칙의 사례다.
  • @ConditionalOnBean은 조건이 만족될 수 있는지까지 확인해야 한다 조건이 참이 될 수 없다는 관측이 이 규칙으로 이어진다.
  • 조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다 조립하는 쪽을 읽지 않으면 중복을 조건 결함으로 오진한다.
  • high-water mark가 본 위치를 뜻해서 재전달된 변경이 영구히 사라졌다 같은 신뢰성 계열에서 조용한 실패가 나타난 다른 사례다.

문제

messaging 스타터의 MessagingReliabilityAutoConfiguration 은 outbox 와 inbox 운영 빈을 조건부로 등록한다.

81행 : OutboxRelay 는 OutboxRepository 와 OutboxEnvelopeFactory 빈이 있을 때만 106행 : OutboxRelayWorker 는 OutboxRelay 가 있을 때만 123행 : MessagingOutboxRelayLifecycle 은 OutboxRelayWorker 가 있을 때만 138행 : OutboxCleanupJob 은 OutboxRepository 가 있을 때만 167행 : InboxCleanupJob 은 InboxRepository 가 있을 때만

사슬의 뿌리는 OutboxRepository 와 OutboxEnvelopeFactory 다. 프로덕션 코드 어디에도 그 두 타입의 빈을 만드는 곳이 없다. 따라서 사슬 전체가 조립되지 않고, outbox 와 inbox 리프의 main 파일 19개 2,818 LOC 가 조용히 비어 있다.

여기까지가 앞선 분석의 판정이었고 그것은 사실이다. 다시 잰 이유는 스타터의 클래스 javadoc 이 이 조건들을 결함이 아니라 계약으로 서술하기 때문이다. 플랫폼은 그 저장소를 제공할 수 없다고 명시한다. 애플리케이션 자신의 트랜잭션에서 애플리케이션 자신의 데이터소스에 쓰기 때문이다.

그렇다면 남는 질문은 하나다. 이 저장소의 출하 애플리케이션은 그 계약을 이행하는가.

결론

이행하지 않는다. 대신 자기 outbox 를 갖고 있다.

스택 A 는 배선되어 출하된다.

포트 : application-core 의 outbox 패키지. OutboxStorePort 와 OutboxAppendPort 와 OutboxMessagePublishPort 와 PublishPendingOutboxEventsUseCase 를 포함해 15개 파일 구현 : persistence-jpa 의 OutboxStoreAdapter. @Repository 가 붙어 있어 컴포넌트 스캔으로 컨텍스트에 들어간다 구동 : app-bootstrap 의 OutboxConfig 가 PublishPendingOutboxEventsUseCase 를 직접 생성하고, OutboxRelayScheduler 가 @Scheduled 로 5초 간격 폴링한다

스택 B 는 어둡다.

포트 : messaging-reliability-api 의 OutboxRepository 구현 : JdbcOutboxRepository 2,276 LOC. 스프링 스테레오타입이 없다 조립 : MessagingReliabilityAutoConfiguration 81행의 조건 뒤

측정으로 확정한 것은 이렇다. main 코드에서 JdbcOutboxRepository 나 JdbcInboxRepository 나 OutboxEnvelopeFactory 를 생성하는 곳이 0 이고, app-bootstrap 에서 관련 빈을 만드는 곳도 0 이다. OutboxEnvelopeFactory 를 @Bean 으로 만드는 곳은 스타터의 테스트 하나뿐이다.

이 차이가 중요한 이유는 수정 방향이 반대이기 때문이다. 조건이 만족되지 않는다고 읽으면 app-bootstrap 에 빈을 등록하는 수정이 된다. outbox 가 둘이라고 읽으면 어느 쪽이 정본인지 먼저 정해야 하는 문제가 된다.

후자가 맞다. 두 스택은 저장 모델도 다르고 발행 경로도 다르다. 둘을 동시에 켜면 같은 업무 이벤트가 두 테이블에 적히거나 두 번 발행될 수 있다.

스타터의 javadoc 은 relay 를 자기가 구동하는 이유를 이렇게 적는다. 아무도 relay 를 돌리지 않는 것은 성공을 보고한 업무 트랜잭션 뒤에서 테이블이 차오르는 상황이라는 것이다. 그 경고는 스택 B 의 테이블에는 해당하지 않는다. 그 테이블에 쓰는 코드가 배선되어 있지 않으므로 채워질 일이 없다. 스택 B 는 위험한 것이 아니라 존재하지 않는다.

검증 환경

OpenJDK : 21.0.12 Gradle : 9.0.0 Spring Boot : 4.0.8 확인 방식 : 정적 도달성 전수 확인. 애플리케이션을 부팅하지 않았다 소스 수정 : x

재현 조건

원문은 final/evidence/raw/335-two-outboxes-one-wired.txt 에 있다.

  1. MessagingReliabilityAutoConfiguration 의 조건 사슬과 클래스 javadoc 을 읽는다.
  2. main 소스 전체에서 JdbcOutboxRepository 와 JdbcInboxRepository 와 OutboxEnvelopeFactory 를 생성하는 코드를 찾는다. 일치 0 이다.
  3. app-bootstrap 의 main 에서 outbox 또는 inbox 저장소 빈을 만드는 곳을 찾는다. 일치 0 이다.
  4. application-core 의 outbox 패키지와 persistence-jpa 의 OutboxStoreAdapter 스테레오타입, app-bootstrap 의 OutboxConfig 와 OutboxRelayScheduler 를 읽어 다른 스택이 배선되어 있음을 확인한다.

앞선 사이클의 증거 파일 tl-outbox-unsatisfiable-condition.txt 는 일부 구획에 셸 변수가 전개되지 않은 채 저장돼 있다. 그 구획의 수치는 이번에 다시 쟀다.

본문

이 저장소에는 서로를 모르는 outbox 구현이 둘 있다.

스택 A — 배선되어 출하된다

application-core/.../outbox/ 의 포트와 persistence-jpa@Repository OutboxStoreAdapter, 그리고 app-bootstrapOutboxConfig@Scheduled 로 구동하는 PublishPendingOutboxEventsUseCase 다.

OutboxConfig 참조 위치

:::evidence key="outbox-chain-behind-an-unsatisfiable-condition" alt="코드베이스에서 OutboxConfig 를 검색한 출력 3줄. 이 기록이 세는 참조가 그 출력에 그대로 보인다." caption="OutboxConfig 코드베이스 검색 — 3줄 · exit 0" zoom="true" :::

스택 B — 조건이 프로덕션에서 참이 되지 않는다

messaging-reliability-apiOutboxRepositoryJdbcOutboxRepository(2,276 LOC)와 OutboxRelay 계열이며, MessagingReliabilityAutoConfiguration:81@ConditionalOnBean({OutboxRepository.class, OutboxEnvelopeFactory.class}) 뒤에 있다. JdbcOutboxRepository 에는 스프링 스테레오타입이 없고 그것을 만드는 @Bean 도 main 에 없다.

조건 결함이 아니라 정본이 정해지지 않은 중복이다

스타터의 javadoc 은 이 조건들을 결함이 아니라 계약으로 서술하고 저장소·팩토리 제공을 애플리케이션 책임으로 둔다. 두 스택은 저장 모델과 발행 경로가 다르므로 동시에 켜면 같은 이벤트가 두 번 적히거나 두 번 발행될 수 있다.

확인하지 못한 것

애플리케이션을 부팅해 조건 평가 리포트로 확인하지 않았다. 부팅 한 번이면 두 스택 중 어느 것이 컨텍스트에 들어가는지 직접 관측된다.

두 스택을 동시에 켰을 때 실제로 이중 기록이 일어나는지 재현하지 않았다. 저장 모델이 다르다는 것은 코드로 확인했으나 그 결과를 관측한 것은 아니다.

ConditionEvaluationReport로 미충족 사유를 확인하지 않았다