Files
llm-wiki/raw/branch-notes/feature-messaging-multibroker-router.md

176 lines
11 KiB
Markdown

---
title: branch / feature-messaging-multibroker-router
source_type: branch-note
status: raw
id: BR-CA-SKELETON-OPERATIONAL-CONTRACT-053
kind: project-work-item
project: ca-skeleton-operational-contract
work_item: WI-CA-SKELETON-OPERATIONAL-CONTRACT-053
inherits: [DEC-CA-SKELETON-OPERATIONAL-CONTRACT-EVENT-BROKER-001@1, DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1]
refines: []
overrides: []
depends_on: [WI-CA-SKELETON-OPERATIONAL-CONTRACT-038]
contract_packet: 1
branch: feature-messaging-multibroker-router
parent_branch:
related_projects: [ca-skeleton]
tags: [branch, ca-skeleton, adapter-outbound, messaging, kafka, spi, extensibility, refactoring]
created: 2026-06-16
target_merge:
status_label: in-progress
contract_packet_sha256: 6d5d448a4d1f5608639023b6bacf14a05e5491283794a7c796714e678b5c3cf3
---
# branch: feature-messaging-multibroker-router
> Layer: `raw/branch-notes/` — 단일 브랜치의 TODO·결정·진행 기록. git 작업은 `develop` 브랜치에서 수행.
<!-- section-id: branch-parent -->
## 부모 (필수)
[[raw/project-notes/ca-skeleton-operational-contract]]
<!-- GENERATED: branch-contract:start -->
<!-- section-id: branch-contract-packet -->
## 브랜치 계약 패킷
- **생성 시 프로젝트 개정**: `1`
- **패킷 스키마**: `contract_packet: 1`
- **완료 조건**: broker 선택·routing·fallback과 core transport-neutrality test가 명시된다
<!-- section-id: inherited-project-decisions -->
### 상속한 프로젝트 결정
| Decision Ref | Project Summary | Branch Application | Source |
|---|---|---|---|
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-EVENT-BROKER-001@1` | core는 broker-agnostic outbox를 제공하고 Kafka는 optional adapter로 둔다 | broker router가 core transport-neutrality와 optional adapter 경계를 유지하도록 적용한다 | [[raw/project-notes/ca-skeleton-operational-contract]] |
| `DEC-CA-SKELETON-OPERATIONAL-CONTRACT-MODULE-LAYOUT-001@1` | Gradle multi-module에서 domain-core·application-core·adapter-*·shared-contract·app-bootstrap·sample-portfolio 책임을 분리한다 | router SPI·adapter·bootstrap wiring의 module ownership에 적용한다 | [[raw/project-notes/ca-skeleton-operational-contract]] |
<!-- section-id: branch-local-decisions -->
### 브랜치 지역 결정
> 기존 branch-local 결정은 아래 `## Decision Evidence Map / 결정-근거 매핑`의 D-row가 소유하며 이 packet에서 복제하지 않는다.
| Decision ID | Decision | Relation | Supporting Claims | Status |
|---|---|---|---|---|
<!-- section-id: declared-overrides -->
### 선언한 예외
| Override ID | Overrides | Reason | Approval | Status |
|---|---|---|---|---|
없음.
<!-- GENERATED: branch-contract:end -->
<!-- section-id: branch-goal -->
## 목표
adapter-outbound 메시징 리뷰에서 "outbox 켜면 Kafka 강제 + 브로커 추가 시 config 편집 필요"가 확인됨(cache 는 '파일만 추가'인데 메시징은 Kafka-binary). 메시징 선택/와이어링을 cache 라우터 패턴으로 이식해 **브로커 추가 = 파일만 추가**(중앙 config·SPI·제너릭 데코레이터 불변)로 만든다. 포트 시그니처·메시지 매핑·fail-open/closed 실패 계약은 보존.
설계: `ca-tmpl/docs/superpowers/specs/2026-06-16-messaging-multibroker-design.md`. 계획: `.../plans/2026-06-16-messaging-multibroker-plan.md`. (둘 다 gitignored)
<!-- section-id: branch-scope -->
## 범위
### 포함 범위
- 신규 SPI `MessageBroker`(brokerId+send) + 제너릭 데코레이터 `OutboundMessagePublisher`(fail-open)·`OutboxMessagePublishAdapter`(fail-closed) + 중앙 `MessagingConfig` + `MessagingSettings`(app.messaging.broker) + 포트별 `DisabledMessagePublisher`/`DisabledOutboxMessagePublisher`.
- Kafka 를 기여자로 전환: `KafkaMessageBroker`(implements MessageBroker), `KafkaAdapterConfig`(@ConditionalOnProperty app.messaging.broker=kafka + @EnableConfigurationProperties), `KafkaAdapterSettings`(enabled 제거, brokers format-only).
- 삭제: `KafkaMessagePublisher`, `KafkaOutboxMessagePublishAdapter`, `Disabled{Message,OutboxMessagePublish}*`(kafka/outbox 위치), `OutboxPublishAdapterConfig`.
- 속성 교체: `app.messaging.kafka.enabled`/`APP_MESSAGING_KAFKA_ENABLED``app.messaging.broker`/`APP_MESSAGING_BROKER` (.env/application.yml/env-keys.yaml). `APP_MESSAGING_KAFKA_BROKERS` 유지.
- 테스트 5개 재작성/갱신.
- 부수: `code-conventions.md` 에 P1/P2(패키지 구조) 규칙 추가; 로거 통합 검토(결론: 분리 유지).
### 제외 범위
- 다중 동시 브로커(per-topic 라우팅) — 단일 활성으로 결정.
- 실제 Kafka SDK — seam(KafkaSender/KafkaMessageBroker) 유지.
- 두 outbound 로거 통합 — 검토 후 의도적 분리 유지(아래 D7).
## 근거
| Source | 정당화하는 결정 |
|---|---|
| cache 멀티백엔드 라우터 (`CacheBackend`/`CacheStoreRouter`/`CacheRouterConfig`, `adapter-outbound`) | SPI+중앙조립+데코레이터 패턴 이식 |
| `OutboxMessagePublishPort` javadoc (application-core) | fail-closed 보존 불변식 |
| `KafkaMessagePublisher` 기존 동작 | fail-open(swallow) 보존 불변식 |
| Spring `@ConditionalOnProperty`/`@ConfigurationPropertiesScan` | 브로커 자기등록 게이팅, settings 전역 바인딩 처리 |
| `./gradlew check` 1254 pass (2026-06-16) | 행위 보존 검증 |
## 결정-근거 매핑
| Decision ID | Decision | 선택 조건 (언제 이 결정 / 언제 대안) | Supporting Claims | Evidence Strength | Open Risk |
|---|---|---|---|---|---|
| D1 | 단일 활성 브로커(`app.messaging.broker=<id>`) | 통상 브로커 1개 / per-topic 다중이면 cache식 bindings | 사용자 결정 2026-06-16 | `user-directed` | per-topic 다중 브로커 필요 시 재설계 |
| D2 | 통합 단일 `MessageBroker` SPI(일반+outbox 공용) | 두 포트가 "선택 브로커로 전송" 공유 | 사용자 결정; 설계 §4 | `user-directed` | 없음(테스트 green) |
| D3 | 환경 플래그 깨끗한 교체 | 스켈레톤(레거시 사용자 없음) / 배포중이면 alias | 사용자 결정; verifyEnvKeys green | `user-directed + verified` | fork 가 옛 키 쓰면 깨짐(스켈레톤이라 무관) |
| D4 | fail-open/closed 를 바인딩 레벨 데코레이터로 분리 보존 | 두 실패 계약이 정반대(swallow vs rethrow) | `OutboxMessagePublishPort` javadoc; `KafkaMessagePublisher` 코드 | `code-evidence + verified` | 없음 |
| D5 | Disabled 를 포트별 2클래스로 분리(통합 1클래스 폐기) | 1클래스가 두 포트 구현 시 `getBean(MessagePublisher)` 모호 | NoUniqueBeanDefinitionException(테스트가 포착) | `verified` (버그→수정→green) | 없음 |
| D6 | P1/P2 패키지 규칙을 code-conventions SSOT 에 성문화 | 관례는 있으나 규칙 부재 시 | 사용자 요청; `adapter-outbound/CLAUDE.md` dominant | `user-directed` | ArchUnit 미강제(문서+리뷰) |
| D7 | 두 outbound 로거 분리 유지(통합 안 함) | 필드셋·로그레벨 정책·SSOT 가 다를 때 | 코드 비교(아래 Claims) | `code-evidence` | 통합 안 해 약간의 형식 중복(2줄) 잔존 |
## 검증해야 할 주장
| Claim | Why uncertain | How to verify | Status |
|---|---|---|---|
| 전 리네임/구조변경이 행위 보존 | 교차모듈 구조 변경 | `./gradlew check` 전체 green | `locally-verified` (1254/1254) |
| fail-open(일반 swallow)·fail-closed(outbox rethrow) 둘 다 보존 | 데코레이터 분리 | OutboundMessagePublisherTest·OutboxMessagePublishAdapterTest green | `locally-verified` |
| 브로커 추가 = 파일만(중앙 불변) | 실제 2번째 브로커 미추가 | RabbitMessageBroker+RabbitAdapterConfig 추가 시 MessagingConfig 무변경 확인 | `needs-confirmation` |
| env 깨끗한 교체가 정합 | 3-way(.env/yaml/registry) | verifyEnvKeys green + 옛 키 grep 0 | `locally-verified` |
| 두 로거 분리가 정당(통합 부적절) | 유사 이름 | 필드셋(operation vs duration_ms/retry_attempt)·로그레벨(WARN-always vs WARN/ERROR)·SSOT(mdc-keys vs metrics.yaml) 상이 확인 | `code-verified` |
| 정식 CA 리뷰 체인 통과 | 인라인 구현 | 커밋 후 ca-architect-sentinel→spec→quality | `needs-confirmation` |
## 검증
- `./gradlew check` → BUILD SUCCESSFUL, **1254 test pass / 0 fail**. verifyEnvKeys·verifyOneTypePerFile·verifyCleanArchitectureDependencies·ArchUnit(Clean+Naming) green.
- 중간 버그: 통합 `DisabledMessaging`(2포트 구현)이 `getBean(MessagePublisher)` 모호성 유발 → 테스트가 포착 → 포트별 2클래스로 분리 후 green.
- 옛 플래그(`APP_MESSAGING_KAFKA_ENABLED`/`messaging.kafka.enabled`) 잔여 grep 0.
## TODO
- [ ] 두 번째 broker adapter 추가 시 중앙 `MessagingConfig` 무변경을 검증한다 — 등급: `needs-confirmation`
- [ ] 정식 CA 리뷰 체인을 실행하고 결과를 기록한다 — 등급: `needs-confirmation`
## 진행 중 메모
- 현 구현 evidence와 잔여 검증 항목은 위 `## 검증 / Verification``## 검증해야 할 주장 / Claims To Verify`가 소유한다.
## 결정 사항
- branch-local 결정의 정본은 `## Decision Evidence Map / 결정-근거 매핑` D1~D7이다.
- project-level 상속 결정은 `## Branch Contract Packet`이 소유한다.
## 구현 가이드
1. `MessageBroker` SPI와 포트별 fail-open/fail-closed decorator 경계를 유지한다.
2. broker adapter는 자기 설정과 조건부 등록을 소유하고 core transport contract를 참조하지 않는다.
3. `app.messaging.broker` 값에 따라 단일 broker를 선택하며 disabled 구현은 포트별 bean으로 유지한다.
4. 전체 test와 environment-key 검증으로 routing·fallback·legacy key 제거를 확인한다.
## 엣지·실패·의존
- **실패 모드**: 한 bean이 두 outbound port를 동시에 구현하면 type 조회가 모호해질 수 있다. 포트별 disabled bean으로 차단한다.
- **의존**: [[raw/branch-notes/feature-domain-event-outbox-contract]]의 fail-closed outbox contract를 보존한다.
- **경계**: per-topic 다중 broker routing은 현재 단일 활성 broker contract 밖이다.
## 마주친 문제
- 통합 `DisabledMessaging``NoUniqueBeanDefinitionException`을 일으켜 포트별 구현으로 분리했다. 재현과 해결 evidence는 `## 검증 / Verification`에 기록돼 있다.
## 관련 일일 노트
- 연결된 일일 노트 없음.
## 완료 후 정리
- 현재 구현·로컬 검증은 완료됐으나 두 번째 broker 확장 검증과 정식 리뷰 체인은 남아 있다.
## 묶음 (파생 raw 문서)
- **raw/errors/**: 후보 1건(보류) — "한 클래스가 두 Spring 포트를 구현하면 `getBean(Type)` 이 NoUniqueBeanDefinitionException; 데코레이터/센티넬은 포트별 1클래스로 분리". 재발 가능 패턴이라 errors 노트화 가치 있음(실행 라운드 후).
- **raw/interviews/**: 후보 1건(보류) — "확장 가능한 어댑터 추상화: 포트만으로 부족하고 선택/와이어링 계층(SPI+라우터+데코레이터)까지 설계해야 '파일만 추가' 확장이 된다".
- **raw/blog-topics/**: 후보 2건(보류):
1. "cache 멀티백엔드 라우터 패턴을 메시징(outbox 포함)에 이식 — Kafka-binary 플래그에서 backend-neutral SPI 로".
2. "fail-open vs fail-closed 를 바인딩 레벨 데코레이터로 분리해 한 SPI 로 두 실패 계약 보존하기".