Files
llm-wiki/raw/branch-notes/feature-integration-adapter-templates.md

48 KiB

title, source_type, status, id, kind, project, work_item, inherits, refines, overrides, depends_on, contract_packet, branch, parent_branch, related_projects, governing_docs, tags, created, target_merge, status_label, contract_packet_sha256
title source_type status id kind project work_item inherits refines overrides depends_on contract_packet branch parent_branch related_projects governing_docs tags created target_merge status_label contract_packet_sha256
branch / feature-integration-adapter-templates branch-note raw BR-CA-SKELETON-OPERATIONAL-CONTRACT-009 project-work-item ca-skeleton-operational-contract WI-CA-SKELETON-OPERATIONAL-CONTRACT-009
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-EVENT-BROKER-001@1
1 feature-integration-adapter-templates
ca-skeleton
raw/project-notes/ca-skeleton-operational-contract
branch
ca-skeleton
adapter
kafka
redis
notification
2026-05-21 in-progress 1442f6124a72b8a5b62b10f02f014af447a26ecf28849e0baa7ca41a87de8703

branch: feature-integration-adapter-templates

Layer: raw/branch-notes/ — Kafka/Redis/Slack/Google Email 같은 선택형 adapter template와 실패 계약을 정의합니다.

[!note] 개정 예정 고지 (2026-07-28) 본 branch 는 owner 를 유지하되 내용을 코드 사실에 맞춰 개정한다. 근거: docs/superpowers/specs/2026-07-28-ca-skeleton-production-capability-feature-decomposition-design.md §5.3.

  • 개정 사유: "optional adapter 는 disabled env 가 기본" 을 capability descriptor 모델로 대체한다. 활성화 SSOT 는 feature-capability-provider-selection-contract 로 이동하고 본 branch 는 소비자가 된다.
  • 참조 설계: ca-tmpl docs/superpowers/specs/2026-07-26-production-capability-platform-design.md §7 (등급 internal-design-doc — 공식 문서 아님)

부모 (필수)

ca-skeleton 은 별도 root branch 없이 project-note 가 SSOT 역할. 본 feature branch 는 project-note 의 운영 계약 중 해당 영역 (§<관련 섹션>) 의 결정/근거/금지 사항을 정제한다.

브랜치 계약 패킷

  • 생성 시 프로젝트 개정: 1
  • 패킷 스키마: contract_packet: 1
  • 완료 조건: optional adapter template가 core broker abstraction을 침범하지 않는다

상속한 프로젝트 결정

Decision Ref Project Summary Branch Application Source
DEC-CA-SKELETON-OPERATIONAL-CONTRACT-EVENT-BROKER-001@1 core는 broker-agnostic outbox를 제공하고 Kafka는 optional adapter로 둔다 Kafka를 포함한 optional adapter의 활성화·격리 template에 적용한다 raw/project-notes/ca-skeleton-operational-contract

브랜치 지역 결정

Decision ID Decision Relation Supporting Claims Status
D1 optional adapter를 disabled-default module로 제공한다 local raw/project-notes/ca-skeleton-operational-contract proposed
D2 ConditionalOnProperty로 bean 등록을 제어한다 local raw/project-notes/ca-skeleton-operational-contract proposed
D3 ArchUnit으로 application의 disabled adapter 의존을 검사한다 local raw/project-notes/ca-skeleton-operational-contract proposed
D4 disabled adapter 호출은 fail-fast 처리한다 local raw/project-notes/ca-skeleton-operational-contract proposed
D5 Java SPI 대안을 채택하지 않는다 local raw/project-notes/ca-skeleton-operational-contract proposed
D6 Profile 기반 adapter toggle을 채택하지 않는다 local raw/project-notes/ca-skeleton-operational-contract proposed
D7 runtime feature flag와 startup adapter toggle을 분리한다 local raw/project-notes/ca-skeleton-operational-contract proposed
D8 plugin architecture는 template 범위에서 제외한다 local raw/project-notes/ca-skeleton-operational-contract proposed
D9 required/optional 분류 owner와 fail-open/closed 정책 owner를 분리한다 local raw/project-notes/ca-skeleton-operational-contract proposed

선언한 예외

Override ID Overrides Reason Approval Status

없음.

목표

선택형 adapter를 모두 기본 dependency로 탑재하면 skeleton이 무거워집니다. 대신 adapter별 실패 계약과 optional template를 제공하여 붙였을 때 같은 방식으로 실패하고 관측되게 합니다.

  • 이슈:
  • PR:

범위

포함 범위

  • Kafka adapter contract 문서.
  • Redis adapter contract 문서.
  • Slack notification adapter contract 문서.
  • Google Email adapter contract 문서.
  • common adapter logging/error contract.
  • optional module 또는 sample 분리 기준.

제외 범위

  • 실제 Kafka/Redis/Slack/Google Email 운영 인프라 구성.
  • provider-specific business workflow.
  • 모든 adapter 기본 활성화.

근거 (필수, 최소 1개+)

본 branch의 결정 근거. 상세 비교는 §외부 근거 / 대안 조사 (있다면) 참조.

Source 정당화하는 결정
raw/official-docs/adapter-spring-boot-autoconfig-custom-starter Spring Boot AutoConfiguration + @ConditionalOnProperty / @ConditionalOnBooleanProperty (3
raw/official-docs/adapter-java-spi-serviceloader META-INF/services; on/off 표현 불가 + DI 미통합 + default constructor 강제
raw/company-tech-blogs/adapter-togglz-ff4j-feature-toggle-library runtime toggle; adapter on/off가 아닌 runtime branching 도구라 시맨틱 차이
raw/official-docs/archunit-conditional-on-property-3-layer-pattern 참조

외부 근거 / 대안 조사 (2026-05-22 — Group G-I: Integration Adapter Templates)

본 branch의 optional module + Spring @ConditionalOnProperty + ArchUnit 3-layer detection + AdapterDisabledException fail-fast 결정에 대한 외부 source.

  • 채택 결정 (Spring Boot AutoConfiguration + @ConditionalOnProperty):
  • 검토한 대안:
  • 비교 핵심: Spring @ConditionalOnProperty는 Layer 1만 공식 cover. Layer 2(ArchUnit noClasses().that().resideInAPackage("..application..").should().dependOnClassesThat().resideInAPackage("..adapters.{disabled}.."))와 Layer 3(AdapterDisabledException)는 ca-tmpl 자체 contract. 보강 후보: ArchUnit source 별도 필요. SPI는 on/off 표현 불가 + DI 미통합으로 ca-tmpl 결정과 정면 충돌. Togglz/FF4J는 startup-time toggle이 아닌 runtime branching이라 시맨틱 다름 — feature flag service와 adapter on/off는 분리 영역.

후속 보강 (2026-05-22): ArchUnit Layer 2의 정적 검사 가능 범위 평가. raw/official-docs/archunit-conditional-on-property-3-layer-pattern 참조.

TODO

TODO drained 2026-05-22 — Kafka/Redis/Slack/Google Email adapter 별 정책, common logging/error contract, optional module vs sample 분리는 "결정 사항" / "Adapter Template Defaults" / "테스트 계약" 표에 반영됨. 잔존 TODO 없음.

Work Item Contract

각 TODO는 아래 판정 단위로 재작성되어야 canonical 승급 가능합니다. TODO가 단순히 기준 작성으로 남아 있으면 이 branch는 완료로 보지 않습니다.

field required rule
Decision yes 구현자가 선택해야 하는 기본값
Allowed yes 허용되는 예외와 조건
Forbidden yes 절대 금지되는 구현/문서 상태
Required registry update conditional error/env/header/log/metric/capability 변경 시 필수
Required contract test yes 계약 위반 시 실패해야 하는 테스트
Failure condition yes review/build에서 실패로 판정할 상태
Canonical extraction target yes wiki/projects 승급 위치

진행 중 메모

  • cache miss는 장애가 아닙니다.
  • notification failure는 core use case 실패 여부를 adapter별로 명시해야 합니다.

결정 사항 (decisions)

  • 2026-05-21: 선택형 adapter는 기본 탑재가 아니라 optional template 기준.
  • 2026-05-22: Kafka/Redis/Slack/Google Email은 기본 dependency가 아니며 disabled env가 기본.
  • 2026-05-22: Kafka retry/DLQ는 background-job branch vocabulary를 소비하고, outbox core는 Kafka를 강제하지 않음.
  • 2026-05-22: adapter 배포 형태는 optional module 기본, sample source set은 문서/fixture 전용일 때만 허용.
  • 2026-05-22: required vs optional dependency 분류 SSOT는 runtime-health-lifecycle-contract. 본 branch는 각 adapter의 fail-open/closed 정책과 enable/disable 메커니즘 owns. 두 branch는 양방향 cross-link.
  • 2026-05-22: disabled adapter detection 메커니즘 = 2-layer 검출.
    • Layer 1 (startup, runtime): Spring @ConditionalOnProperty(name="app.adapter.{adapterName}.enabled", havingValue="true") 적용. flag false 시 adapter bean 등록 X. ApplicationContext에 해당 bean 0개 verify.
    • Layer 2 (build, static): archetype smoke test DisabledAdapterArchitectureTest가 application 시작 시 APP_ADAPTER_{ADAPTER}_ENABLED=false인 상태에서 해당 adapter package의 class import가 use case path에 등장하면 fail. 측정 방법: ArchUnit noClasses().that().resideInAPackage("..application..").should().dependOnClassesThat().resideInAPackage("..adapters.{disabled-adapter}..") (when disabled).
    • Layer 3 (runtime, fail-fast): disabled adapter의 use case path가 invoke되면 AdapterDisabledException throw + log error.code=REQUIRED_ADAPTER_DISABLED (migration-startup의 startup validation과 동일). consumer branches는 본 결정을 consume only. adapter 추가 시 env-keys.yamlAPP_ADAPTER_{NAME}_ENABLED row 추가 필수.
  • 2026-05-22: ArchUnit Layer 2의 정적 검사는 'adapter 후보 클래스가 @ConditionalOnProperty annotation을 가짐'까지만 보장. runtime active 여부는 Layer 3 (AdapterDisabledException)에 위임. (status: needs-confirmation, fitness function 도입 결정 코드 단계 보류)

Adapter Template Defaults

adapter default state owner contract
Kafka disabled optional module outbox + background retry/DLQ
Redis disabled optional module cache consistency
Slack disabled optional module notification failure policy
Google Email disabled optional module notification failure policy
common dependency log/error mapper required foundation registry

결정-근거 매핑

각 결정이 어떤 raw source claim 으로 뒷받침되는지 명시. company-tech-blog 증거는 company-case-study 로 표기하며 공식 best practice 로 승격하지 않음.

Decision ID Decision 선택 조건 (언제 이 결정 / 언제 대안) Supporting Claims Evidence Strength Open Risk
D1 optional adapter (Kafka/Redis/Slack/Google Email) 는 기본 dependency 아님, disabled env 기본, optional module 형태로 배포 adapter 가 선택형 (core use case 가 강제하지 않음) 일 때 이 결정. core 가 강제하는 required adapter (예: DB) 면 disabled-default 적용 안 함 → required 분류는 runtime-health-lifecycle-contract 가 owns (D9) raw/official-docs/adapter-spring-boot-autoconfig-custom-starter.md#SBAC-C1, raw/official-docs/adapter-spring-boot-autoconfig-custom-starter.md#SBAC-C2, raw/official-docs/adapter-spring-boot-autoconfig-custom-starter.md#SBAC-C5 official-vendor-doc (Spring Boot AutoConfiguration + namespace 분리 공식 권고) optional module vs sample source set 의 운영 구분 (배포 artifact 관리 부담)
D2 Layer 1 — Spring @ConditionalOnProperty(name="app.{domain}.{adapter}.enabled", havingValue="true", matchIfMissing=false) adapter bean 등록 제어, ApplicationContext bean count = 0 검증 startup-time 활성/비활성을 boolean property 로 표현할 때 이 결정. runtime 중 동적 toggle (gradual rollout) 이 필요하면 feature flag 영역 (D7 배제 근거 참조) — 다른 메커니즘 raw/official-docs/adapter-spring-boot-autoconfig-custom-starter.md#SBAC-C1, raw/official-docs/adapter-spring-boot-autoconfig-custom-starter.md#SBAC-C3, raw/official-docs/adapter-spring-boot-autoconfig-custom-starter.md#SBAC-C4 official-vendor-doc + official-reference (Spring 공식 — Boolean 시맨틱은 3.5.0+ @ConditionalOnBooleanProperty 권장) 3.5.0 미만 baseline 이면 havingValue="true" 명시 + matchIfMissing=false 정확 표현 필요. ApplicationContext bean count 검증 패턴 자체는 Spring 공식 verification 패턴 아님 (SBAC-C1~C5 Usage Boundaries 참조). ENV_KEY_DRIFT: property 는 app.adapter.{name} 이 아니라 도메인 namespace (app.cache.redis/app.messaging.kafka/app.notification.{slack,google-email}) — §Audit & Findings A1
D3 Layer 2 — ArchUnit noClasses().that().resideInAPackage("..application..").should().dependOnClassesThat().resideInAPackage("..adapters.{disabled}..") 정적 검사 빌드 시점에 disabled adapter package 가 application layer import 경로에 등장하면 fail 시키고 싶을 때 이 결정. 단 'disabled' 는 runtime config 평가라 정적 검사로 완전 보장 불가 → runtime 보장은 Layer 3 (D4) raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C1, raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C4, raw/official-docs/archunit-conditional-on-property-3-layer-pattern.md#AUCP-C5 official-vendor-doc + engineering-blog (AUCP-C5 는 Building Evolutionary Architectures 서적 — engineering-blog 강도) ArchUnit Layer 2 의 정적 검사는 'adapter 후보 클래스가 @ConditionalOnProperty annotation 을 가짐' 까지만 보장 — runtime active 여부는 Layer 3 위임 (branch 자체 명시)
D4 Layer 3 — disabled adapter 의 use case path 가 invoke 되면 AdapterDisabledException throw + fail-fast (silent no-op / timeout 대기 금지) Layer 1(bean 미등록)·Layer 2(정적) 를 우회해 disabled adapter 가 runtime 에 실제 호출되는 경우의 최후 방어선. 정상 경로는 Layer 1 에서 bean 자체가 없어 호출 불가 UNSUPPORTED_DECISION (cited official-doc 중 fail-fast adapter exception 패턴 직접 인용 없음 — ca-tmpl 자체 contract). error code 재사용은 미정REQUIRED_ADAPTER_DISABLEDfeature-migration-startup-contract owns + startup-exit(72) 시맨틱 → runtime 재사용 적정성 검토 필요 (§Audit & Findings A2) n/a raw/branch-notes/feature-migration-startup-contract 와 cross-link 필요 (startup validation 의 동등 패턴). runtime 전용 error code 신규 제안 여부 미결
D5 (대안 비교) Java SPI / ServiceLoader 배제 — on/off 표현 불가 + DI 미통합 + default constructor 강제 N/A (배제된 대안 — 채택된 D2 의 반례) raw/official-docs/adapter-java-spi-serviceloader.md#SPI-C1, raw/official-docs/adapter-java-spi-serviceloader.md#SPI-C2, raw/official-docs/adapter-java-spi-serviceloader.md#SPI-C3, raw/official-docs/adapter-java-spi-serviceloader.md#SPI-C5 official-vendor-doc (Oracle Java Tutorial — classpath 존재 = 활성, property 게이팅 부재) JPMS (Java 9+) provides...with... + Java 9+ provider() static method 통합 시맨틱은 본 SPI source 범위 밖
D6 (대안 비교) Spring @Profile 배제 — boolean 시맨틱 부재, 다중 활성/비활성 표현 복잡 N/A (배제된 대안 — 채택된 D2 의 반례) UNSUPPORTED_DECISION (cited raw 중 @Profile vs @ConditionalOnProperty 정확 비교 source 부재 — raw/official-docs/adapter-spring-boot-autoconfig-custom-starter 의 Usage Boundaries 가 "정확한 우선순위·결합 시맨틱 미증명" 명시). trade-off: 배제 사유는 'profile 은 환경 묶음용, adapter on/off 는 직교 축' 이라는 설계 판단 — 강한 외부 인용 없이 채택 가능 n/a Spring @Profile Javadoc 별도 fetch 필요 (rejected alt — depth-blocking 아님)
D7 (대안 비교) Feature flag library (FF4J / Togglz) 배제 — runtime branching 도구, adapter on/off 와 시맨틱 차이 startup-time on/off 면 D2. runtime gradual rollout / A-B 가 필요하면 feature flag 가 더 적합 — 두 영역 분리 (이 branch scope 밖) raw/company-tech-blogs/adapter-togglz-ff4j-feature-toggle-library.md#TOGGLZ-FF4J-C1, raw/company-tech-blogs/adapter-togglz-ff4j-feature-toggle-library.md#TOGGLZ-FF4J-C2, raw/company-tech-blogs/adapter-togglz-ff4j-feature-toggle-library.md#TOGGLZ-FF4J-C3, raw/company-tech-blogs/adapter-togglz-ff4j-feature-toggle-library.md#TOGGLZ-FF4J-C5 company-case-study (vendor 공식 페이지 — best practice 승격 금지) runtime toggle 자체가 adapter 비활성보다 더 적합한 시나리오 (예: gradual rollout) 가 ca-tmpl 에 등장할 가능성 — feature flag 와 adapter on/off 의 분리 영역 명시 필요
D8 (대안 비교) Plugin architecture (OSGi-style) 배제 — Java 진영 deprecated, ca-tmpl scope 외 N/A (배제된 대안 — 채택된 D2 의 반례) UNSUPPORTED_DECISION (OSGi deprecation 의 1차 official source 미인용 — cited raw 에 OSGi 직접 source 없음). trade-off: 배제 사유는 'classpath modular plugin 은 ca-tmpl 단일 배포 모델과 불일치' 라는 scope 판단 — 외부 인용 없이 채택 가능 n/a Eclipse Foundation OSGi 또는 JBoss Modules official status source 보강 필요 (rejected alt — depth-blocking 아님)
D9 required vs optional dependency 분류 SSOT 는 runtime-health-lifecycle-contract, 본 branch 는 fail-open/closed 정책 owner adapter 가 required (없으면 app 못 뜸) 인지 optional 인지 분류는 D9 가 위임받은 SSOT 가 결정. 본 branch 는 각 optional adapter 가 없을 때 어떻게 실패/degrade 하는지(fail-open vs fail-closed) 만 owns UNSUPPORTED_DECISION (분리 자체는 ca-tmpl 자체 contract — 두 branch 간 cross-link 가정) n/a 양방향 cross-link 확인 + runtime-health branch 의 Decision Evidence Map 와 정합성 검증

구현 가이드

결정 (Decisions) 이 "무엇" 이라면, 본 §는 "어디에 어떻게" 의 사전 명세. 다음 구현자가 되묻지 않고 코드를 작성할 수 있는 수준.

구현 현황 (2026-06-09 ca-tmpl src/ grep 결과): kafka/redis/slack/email adapter 모듈은 src/존재하지 않음 (현존 adapter 모듈 = adapter-identifier·adapter-outbound·adapter-persistence·adapter-web). AdapterDisabledException·@ConditionalOnProperty adapter wiring 도 코드 부재 → 본 § 의 Java 측 명세는 전량 planned. 유일하게 landed 된 것은 docs/registries/env-keys.yaml 의 enable 키 5개 (documented-only — registry row 만 존재).

구현 완료 (2026-06-09, branch feature/integration-adapter-templates): 위 planned 항목 전량 구현 + locally-verified. 패키징 결정: 신규 Gradle 모듈이 아니라 기존 adapter-outbound 모듈의 cache/messaging/notification 패키지에 template 으로 landing (빈 package + .gitkeep 가 이미 그 용도로 존재했고, Gradle matrix·ArchUnit 가 ..adapter.outbound.. 를 이미 커버하므로 신규 모듈 오버헤드 회피). heavy SDK(spring-kafka/lettuce/slack/mail) 미추가 — 각 adapter 는 KafkaSender/RedisClient/SlackClient/GoogleEmailClient integration seam(interface) 만 제공하고 실제 client 는 fork 한 프로젝트가 구현 (§목표/WHY "skeleton 경량 유지"). landed:

  • shared-contract: OperationalError.ADAPTER_DISABLED(INTERNAL/500/retryable=false) + AdapterDisabledException (A2 해소 — startup REQUIRED_ADAPTER_DISABLED 재사용 안 함, runtime 전용 신규 코드 owner=본 branch).
  • adapter-outbound: support/(OutboundCorrelation, OutboundDependencyLogger, OutboundSupportConfig) + adapter 4종 = port + seam + fail-open 구현 + disabled sentinel + @ConditionalOnProperty config (+ Kafka 는 KafkaAdapterSettings brokers 검증). build.gradle 에 spring-boot-autoconfigure+slf4j-api 추가.
  • adapter-web: GlobalExceptionHandlerAdapterDisabledExceptionADAPTER_DISABLED 매핑.
  • app-bootstrap: DisabledAdapterArchitectureTest(Layer 2: 격리 + @Bean gating) 신규, CleanArchitectureTest B7 rule 을 @Configuration factory 제외로 scoping, application.yml app.* block.
  • registries/env: error-codes.yaml ADAPTER_DISABLED row, src/.env 5개 키.
  • 검증: :shared-contract:test·:adapter-outbound:test·:adapter-web:test·:app-bootstrap:test·verifyCleanArchitectureDependencies·verifyEnvKeys·verifyPublicPathSnapshot 모두 PASS. ca-architect-sentinel PASS.

1. Adapter enable/disable env 키 계약 (FACT — env-keys.yaml landed)

Trace: D1 (disabled-default optional) + D2 (Layer 1 boolean property). Supporting: SBAC-C1/SBAC-C2/SBAC-C5. 증거 등급: documented-only (env-keys.yaml row 존재, Java adapter 코드 부재).

  • UNSUPPORTED_IMPL_DECISION: 없음 — 아래 키·기본값·validation·required_test 는 모두 ca-tmpl/docs/registries/env-keys.yaml기존 값 재사용 (invent 아님).
adapter env key (registry SSOT) Spring property default validation required_test (registry) owner_branch
Redis APP_CACHE_REDIS_ENABLED app.cache.redis.enabled false boolean_strict adapter-contract:redis-disabled-default feature-integration-adapter-templates
Kafka APP_MESSAGING_KAFKA_ENABLED app.messaging.kafka.enabled false boolean_strict adapter-contract:kafka-disabled-default feature-integration-adapter-templates
Kafka brokers APP_MESSAGING_KAFKA_BROKERS app.messaging.kafka.brokers null csv_of_host_port_when_kafka_enabled adapter-contract:kafka-brokers-when-enabled feature-integration-adapter-templates
Slack APP_NOTIFICATION_SLACK_ENABLED app.notification.slack.enabled false boolean_strict adapter-contract:slack-disabled-default feature-integration-adapter-templates
Google Email APP_NOTIFICATION_GOOGLE_EMAIL_ENABLED app.notification.google-email.enabled false boolean_strict adapter-contract:google-email-disabled-default feature-integration-adapter-templates

⚠️ 본 branch 의 prose/결정에 등장하는 일반화 패턴 APP_ADAPTER_{NAME}_ENABLED / app.adapter.{name}.enabledregistry 에 landed 된 실제 키와 불일치 (도메인 namespace 사용). 신규 adapter 추가 시에도 APP_ADAPTER_* 가 아니라 도메인 prefix (APP_CACHE_*/APP_MESSAGING_*/APP_NOTIFICATION_*) 를 따른다. → §Audit & Findings A1.

2. Layer 1 — Spring @ConditionalOnProperty bean 게이팅 (planned)

Trace: D2. Supporting: SBAC-C1(ConditionalOnProperty 존재)·SBAC-C3(default 누락 시 미매칭)·SBAC-C4(3.5.0+ Boolean 변형). 증거 등급: planned (코드 부재).

  • UNSUPPORTED_IMPL_DECISION:
    • adapter bean package 명 (dev.caskeleton.adapter.{messaging.kafka|cache.redis|notification.slack|notification.googleemail}) — 코드 미존재, 현존 adapter 모듈 명명 관행(dev.caskeleton.adapter.*) 에서 추정. trade-off: 모듈 경계가 코드로 확정되면 정합 필요.
    • "ApplicationContext bean count = 0 검증" 패턴 — Spring 공식 verification 패턴 아님 (SBAC Usage Boundaries). trade-off: disabled 상태 정합성을 startup 테스트로 직접 assert 하려는 ca-tmpl 자체 선택.

각 adapter auto-config 클래스에 @ConditionalOnProperty(name="app.{domain}.{adapter}.enabled", havingValue="true", matchIfMissing=false) 부착. matchIfMissing=false 명시 의무 — env 누락 시 disabled 가 기본 (D2 Open Risk). Boolean baseline 이 Spring Boot 3.5.0+ 면 @ConditionalOnBooleanProperty 로 치환 가능 (SBAC-C4; baseline 버전은 §Claims To Verify 미확정 항목).

3. Layer 2 — ArchUnit 정적 격리 규칙 (planned)

Trace: D3. Supporting: AUCP-C1·AUCP-C4·AUCP-C5. 증거 등급: planned (rule 코드 부재).

  • UNSUPPORTED_IMPL_DECISION: rule 클래스 명 / 배치 모듈 — app-bootstrap 의 기존 CleanArchitectureTest 패키지 관행에서 추정 (src/app-bootstrap/.../architecture/). trade-off: 실제 ArchUnit suite 배치는 코드 확정 시 정합.
noClasses().that().resideInAPackage("..application..")
  .should().dependOnClassesThat().resideInAPackage("..adapter.{disabled-adapter}..")

정적 검사가 보장하는 범위는 'application layer 가 특정 adapter package 를 import 하지 않음' 까지. 'disabled' 라는 runtime config 조건은 정적으로 완전 평가 불가 (AUCP-C5 Usage Boundaries) → runtime 보장은 §4 (Layer 3).

4. Layer 3 — runtime fail-fast AdapterDisabledException (planned, error code 미정)

Trace: D4 (UNSUPPORTED_DECISION — ca-tmpl 자체 contract). 증거 등급: plannedsrc/ grep 결과 AdapterDisabledException 부재.

  • UNSUPPORTED_IMPL_DECISION:
    • 예외 클래스 명 AdapterDisabledException — 코드 부재, 명명은 임의. trade-off: shared-contract 예외 계층과 정합 필요.
    • error code 재사용 REQUIRED_ADAPTER_DISABLED — 이 코드는 feature-migration-startup-contract owns + startup-exit(72) / INTERNAL 500 시맨틱 (error-codes.yaml L830). runtime invoke-path 예외에 재사용하는 것이 적정한지 미결 → §Audit & Findings A2. trade-off: 재사용 시 코드 1개로 startup·runtime 두 lifecycle 을 표현(혼란) vs 신규 runtime 코드 추가(registry 증식).

정상 경로에서는 Layer 1 이 bean 자체를 등록하지 않으므로 disabled adapter 는 호출 불가. 본 Layer 는 Layer 1·2 를 우회한 호출의 최후 방어선 — silent no-op / timeout 대기 금지, 즉시 throw.

5. Per-adapter 실패 계약 (planned — owner branch 와 분담)

Trace: D9 (본 branch 는 fail-open/closed 정책 owner). 진행 중 메모("cache miss 는 장애 아님", "notification failure 는 adapter별 core 실패 여부 명시") 의 구체화. 증거 등급: planned.

  • 결정 (D9 도출, 본 branch owns): notification adapter (Slack/Google Email) 는 fail-open 기본. notification 은 skeleton 에서 use case 의 부수 효과(side-effect) 로 모델링되므로, 전송 실패가 core use case 의 HTTP 응답을 실패(5xx)로 만들지 않는다 — 실패는 correlationId + 실패 metric 으로 관측되고 응답은 core 결과를 따른다.
  • UNSUPPORTED_IMPL_DECISION:
    • notification fail-open 기본값 자체 — 외부 source 가 prescribe 한 값 아님(설계 판단). trade-off: fail-open 이면 알림 유실이 무음(관측에만 의존) vs fail-closed 면 알림 실패가 핵심 API 에러로 표출되어 사용자 경험 저하. skeleton 은 "알림은 부수효과" 가정을 택함.
    • OUT_OF_BRANCH_SCOPE: notification 이 primary outcome 인 use case(예: "비밀번호 재설정 메일 발송" 자체가 목적) 는 도메인 특화 — 해당 use case 가 전송을 동기 + fail-closed 로 호출하는 결정은 도메인 branch 몫(skeleton 범위 밖). 본 contract 는 default(fail-open)만 owns.
    • correlationId 부착 메커니즘 / PII redaction glob 패턴 — 코드·정책 source 부재. trade-off: 아래는 정책 의도 이며 메커니즘은 구현 시 확정.
adapter enabled 시 실패 정책 fail-open/closed 분담 owner
Kafka publish 실패 시 correlationId 부착 + outbox/retry 로 위임 core use case 는 outbox commit 으로 성공 (fail-open) retry/DLQ vocab → raw/branch-notes/feature-background-job-async-contract, outbox → raw/branch-notes/feature-domain-event-outbox-contract
Redis unavailable 시 cache-miss 로 graceful degrade (INTERNAL 로 뭉개지 금지) fail-open (cache miss = 정상) cache 일관성 → raw/branch-notes/feature-cache-consistency-contract
Slack / Google Email 전송 실패 시 correlationId + 실패 metric 으로 관측, provider body/PII 는 log 미등장 fail-open (기본) — notification 실패 ≠ core use case 실패 (5xx 미승격). primary-outcome use case 의 fail-closed 는 OUT_OF_BRANCH_SCOPE 본 branch owns (default), 도메인별 override 는 도메인 branch

엣지·실패·의존

R4(깊이 게이트) 캡처용. 정상 경로 외 구현 중 부딪힐 실패/엣지/다른 계약 의존.

  • 실패·엣지 경로:
    • env 누락 vs false vs true: @ConditionalOnProperty(matchIfMissing=false) 로 누락=disabled 가 기본. boolean_strict validation 이 true/false 외 값 거부 (registry). 3-case bean count 검증 필요 (§Claims).
    • disabled adapter runtime 호출: Layer 1 우회 시 AdapterDisabledException fail-fast — timeout 대기 금지 (D4).
    • Kafka enabled + brokers 누락: csv_of_host_port_when_kafka_enabled validation 이 startup 에서 차단해야 함 (APP_MESSAGING_KAFKA_BROKERS).
    • Redis unavailable (enabled): cache-miss degrade, 응답 200 유지, INTERNAL 승격 금지.
    • notification provider 실패: PII log 누출 0, core use case 실패 전파 여부 adapter별 명시.
  • 다른 계약 의존:

Audit & Findings

ca-tmpl ground truth (registry/code) 대조에서 발견한 drift. 사용자 작성 결정 영역이므로 자동 rewrite 하지 않고 정합 권고만 기록.

  • A1 — ENV_KEY_DRIFT (APP_ADAPTER_{NAME}_ENABLED → 도메인 namespace):
    • 발견: 본 branch 결정/prose (§결정 사항 disabled adapter detection, D2) 는 일반화 키 APP_ADAPTER_{NAME}_ENABLED / app.adapter.{name}.enabled 를 사용. 그러나 env-keys.yaml실제 landed 된 키는 도메인 namespace — APP_CACHE_REDIS_ENABLED, APP_MESSAGING_KAFKA_ENABLED, APP_NOTIFICATION_SLACK_ENABLED, APP_NOTIFICATION_GOOGLE_EMAIL_ENABLED (모두 owner_branch: feature-integration-adapter-templates).
    • 권고: 구현·@ConditionalOnProperty 는 §구현 가이드 §1 표의 도메인 namespace 키를 SSOT 로 사용. prose 의 APP_ADAPTER_* 일반화는 abstract placeholder 로만 취급하고, 신규 adapter 도 도메인 prefix 를 따른다.
  • A2 — CODE_OWNERSHIP/SEMANTIC drift (REQUIRED_ADAPTER_DISABLED 재사용):
    • 발견: D4/Layer 3 는 runtime invoke-path 예외 로그에 error.code=REQUIRED_ADAPTER_DISABLED 를 적었으나, 이 코드는 error-codes.yaml L830 에서 owner_branch: feature-migration-startup-contract + category INTERNAL/500 + runbook://startup/required-adapter-disabledstartup-time (exit 72, "disabled required adapter 로 app 이 뜨면 실패") 시맨틱.
    • 권고: (1) runtime fail-fast 는 startup validation 과 lifecycle 이 다르므로 startup 코드 재사용은 의미 충돌 가능. (2) 선택지 — startup-only 로 유지하고 runtime 은 별도 코드 신규 제안(owner=본 branch) 하거나, migration-startup branch 와 합의해 코드 의미를 명시적으로 두 lifecycle 로 확장. 결정 전까지 D4 의 error code 는 미정.

검증해야 할 주장

공식 문서나 사례는 근거지만, 내 프로젝트에서의 동작을 자동으로 보장하지 않는다.

Claim Why uncertain How to verify Status
@ConditionalOnProperty(havingValue="true", matchIfMissing=false) 가 ca-tmpl 의 "기본 disabled" 의도를 정확히 표현 SBAC-C3 가 "by default property must be present AND not equal to false" — env 가 누락된 경우 matchIfMissing=false 명시 의무 local 통합 테스트로 (1) env 누락 (2) false (3) true 3-case 에서 bean count 검증 planned
3.5.0+ 에서 @ConditionalOnBooleanProperty 가 동등 시맨틱을 더 명시적으로 표현 SBAC-C4 가 since 3.5.0 — ca-tmpl baseline 의 Spring Boot 버전 확인 필요 gradle/libs.versions.toml 또는 build.gradle.kts 의 Spring Boot 버전 확인 후 적용 needs-confirmation
ArchUnit Layer 2 rule 이 disabled adapter 의 use case path import 를 실제로 catch AUCP-C1 PREDICATE/CONDITION 모델로 가능하지만 - "when disabled" 조건은 runtime config 평가 — ArchUnit 의 정적 검사 한계 (AUCP-C5 Usage Boundaries) APP_ADAPTER_KAFKA_ENABLED=false 상태에서 violating PR 만들어 ArchUnit rule fail 확인 needs-confirmation
Layer 3 AdapterDisabledException + log code REQUIRED_ADAPTER_DISABLED 가 actual runtime 에서 trigger D4 UNSUPPORTED_DECISION — ca-tmpl 자체 contract adapter aspect + exception throw + log assertion 통합 테스트 planned
disabled adapter 가 runtime path 에서 호출 시 fail-fast (timeout 대기 금지) shutdown 정책 (raw/branch-notes/feature-outbound-http-client-baseline) 과 정합 — 적용 시점 확인 필요 shutdown phase 통합 테스트 + thread state assertion needs-confirmation
Kafka publish failure 에 correlationId 가 항상 부착 Kafka adapter contract — correlationId propagation 메커니즘 자체 검증 필요 Kafka producer interceptor + log assertion 통합 테스트 planned
Redis unavailable 시 cache-miss 로 graceful degrade (INTERNAL 으로 뭉개지지 않음) Redis adapter contract — fail-open/closed 정책 명시 필요 Redis container down + cache read 통합 테스트 + 응답 200 OK + cache miss metric 확인 planned
notification provider body/PII 가 log 에 등장하지 않음 Slack/Google Email adapter — payload redaction policy 검증 필요 grep 으로 payload pattern (@gmail.com 등) log 검출 contract test planned
feature flag (FF4J/Togglz) 와 adapter on/off 의 분리 영역 시각화 D7 — runtime toggle vs startup toggle 의 운영 혼동 가능 architecture decision record 작성 + 면접 시 답변 가능한 경계 명시 planned
  • disabled adapter가 runtime path에서 호출되면 실패.
  • Kafka publish failure에 correlationId가 없으면 실패.
  • Redis unavailable이 degrade 가능 여부 없이 INTERNAL로 뭉개지면 실패.
  • notification provider body/PII가 log에 남으면 실패.
  • optional adapter가 core startup에 필수 dependency가 되면 실패.

관심사 커버리지 (coverage-auditor 2026-06-09)

governing doc: raw/project-notes/ca-skeleton-operational-contract (§11 Optional Adapters + §9 Env-driven + §25 SSOT Owner Map + Group G-I). 기준: rules/coverage-gate.md. 판정: Covered (missing 0). 상태: covered-here(이 브랜치 결정) / delegated(다른 owner 브랜치) / missing(아무도 안 맡음 → Blocking).

관심사 상태 owner 심각도 근거
선택형 adapter disabled-default 정책 covered-here D1 (SBAC-C1/C2/C5)
Adapter enable/disable env 키 계약 (5개) covered-here D2 + §구현 가이드 §1 — env-keys.yaml landed (owner=본 branch)
Layer 1 @ConditionalOnProperty bean 게이팅 covered-here D2 + §구현 §2 (planned)
Layer 2 ArchUnit 정적 격리 covered-here D3 + §구현 §3 (AUCP-C1/C4/C5, planned)
Layer 3 runtime fail-fast covered-here D4 + §구현 §4 (planned, error code A2 미결)
fail-open/closed per adapter (Kafka/Redis) covered-here D9 + §구현 §5 (둘 다 fail-open)
fail-open/closed per adapter (Slack/Google Email) covered-here D9 + §구현 §5 — fail-open 기본 결정 완료
common adapter logging/error contract covered-here §범위 In-scope + Adapter Template Defaults common row (MDC dependency key SSOT 는 log-management consume)
optional module vs sample 패키징 기준 covered-here D1 (optional module 기본, sample = 문서/fixture 전용)
required vs optional dependency 분류 SSOT delegated raw/branch-notes/feature-runtime-health-lifecycle-contract OK D9 — 분류 SSOT 위임. cross-link: raw/branch-notes/feature-runtime-health-lifecycle-contract
REQUIRED_ADAPTER_DISABLED error code (startup lifecycle) delegated raw/branch-notes/feature-migration-startup-contract Should-fix error-codes.yaml L830 owner. runtime 재사용 적정성은 §Audit A2 에서 미결 — raw/branch-notes/feature-migration-startup-contract 와 합의 필요
Kafka retry/DLQ vocabulary delegated raw/branch-notes/feature-background-job-async-contract OK §구현 §5 위임 링크 존재
Redis cache endpoint 키 (HOST/PORT) delegated raw/branch-notes/feature-cache-consistency-contract OK env-keys.yaml owner + §엣지 의존 링크 존재

마주친 문제

  • 2026-06-09 B7 ArchUnit 충돌: optional adapter 의 @ConditionalOnProperty @Bean factory method 가 port 타입(MessagePublisher/CacheStore/… — ..adapter.outbound.. 거주)을 반환하자 outbound_adapter_method_returns_only_domain_or_primitives(B7) 가 9건 위반. B7 은 adapter 응답 method 의 external type 누출을 막는 rule 이지 DI factory 가 자기 port 타입을 반환하는 것을 막는 rule 이 아님 → B7 을 @Configuration 클래스 제외로 scoping. 상세: raw/errors/archunit-b7-configuration-bean-factory-return-type-2026-06-09.

  • 2026-06-16 messaging broker-SPI 전환 후 주석 drift 정리: 메시징이 app.messaging.kafka.enabled + 단일 KafkaMessagePublisher 구조에서 app.messaging.broker=<brokerId> + MessageBroker SPI(KafkaMessageBroker) + broker-agnostic 바인딩 데코레이터(OutboundMessagePublisher fail-open / OutboxMessagePublishAdapter fail-closed) + disabled sentinel 쌍(DisabledMessagePublisher/DisabledOutboxMessagePublisher)으로 리팩터된 뒤, JavaDoc/주석이 옛 구조를 가리키는 drift 6건을 정리(코드 동작 무변경, 주석 only). 수정: MessagePublisher(JavaDoc 를 adapter-local fail-open 으로 재서술 — "a use case holds this port" 삭제, use-case-facing durable 경로는 application-core OutboxMessagePublishPort 임을 명시), MessagingConfig·MessagingSettings(깨진 {@link DisabledMessaging} → 실제 Disabled* 쌍), kafka/KafkaSender(KafkaMessagePublisherKafkaMessageBroker + 바인딩 데코레이터), application-core OutboxMessagePublishPort(adapter 클래스명 제거 → "general fail-open messaging publisher" 로 일반화), CleanArchitectureTest 주석 예시(KafkaAdapterConfig#kafkaMessagePublisherMessagingConfig#messagePublisher). 검증: :adapter-outbound:compileJava :application-core:compileJava :app-bootstrap:compileTestJava BUILD SUCCESSFUL.

    • NOTE_DRIFT: 본 노트의 env-key 표(app.messaging.kafka.enabled/APP_MESSAGING_KAFKA_ENABLED, L163-164)와 D2 예시 ENV_KEY_DRIFT 항목(L130)은 broker-SPI 전환 네이밍이라 현재 코드(app.messaging.broker)와 어긋남 — broker-SPI 리팩터(사용자 작업, 본 세션에서 미캡처)가 정합시켜야 할 영역. 본 작업 범위는 코드 주석 only 이므로 노트 표는 자동 rewrite 하지 않음(§Audit & Findings 의 "자동 rewrite 하지 않고 정합 권고만" 정책과 동일).
  • 2026-06-16 OutboundDependencyLoggerFailOpenDependencyLogger 리네임 (책임-명확화 리팩터): 공통 의존성 로거의 이름이 "Outbound*" 라 HTTP 까지 포괄하는 공통 로거로 오독될 소지가 있었음. 실제로는 cache/messaging/notification fail-open optional adapter 전용(WARN, 관측-only)이고 HTTP 경로는 hard failure 를 ERROR 로 올리는 별도 httpclient/OutboundHttpDependencyLogger 임. 두 로거를 합치지 않는다는 판단은 유지(레벨·필드·error-code 정책 상이)하고 이름만 기존 FailOpen* 컨벤션(FailOpenCacheStore/FailOpenNotificationProvider)에 맞춰 변경. 범위: 타입 토큰 17개 .java + @Bean 메서드 outboundDependencyLogger()failOpenDependencyLogger()(타입 주입이라 안전 — resource/Qualifier by-name 참조 0 확인) + 클래스 JavaDoc 도입부 fail-open 강조 + adapter-outbound/CLAUDE.md L38 + LogMaskingPatterns JavaDoc 참조. 동작 무변경. 검증: :adapter-outbound:test 175/175 PASS, :app-bootstrap:compileJava BUILD SUCCESSFUL.

    • 보류 (리뷰 권고/판단대로): ① DependencyLogFields 공통 상수/포매터 추출 — 두 로거의 필드셋·레벨 정책이 달라 효익 적고 리뷰도 "중복 조금이 정책 섞임보다 낫다"며 helper "정도만 고려" 권고 → 보류. ② OutboundHttpClient 의 classify+outcome+log 흐름을 OutboundHttpCallObserver/FailureHandler 로 추출 — 리뷰가 "필수 아님, 과하게 쪼개면 처음 보는 사람이 더 힘듦" 명시 → 보류(스켈레톤 가독성·회귀 위험). ③ TraceContextPropagationInterceptor FORK LANDMINE 주석 docs/runbook 이관 — 해당 경고는 "이 파일을 고쳐 실 tracer 를 붙이는 사람"이 직접 봐야 하는 load-bearing 안전 정보(sampled=00 강제 + 인터셉터가 OTel 계측보다 먼저 등록되어 race 를 이김)라 in-file 유지 권고, 이관 시 누락 위험 → 보류(사용자 확인 시 in-place 압축만 검토).
  • 2026-06-16 cache 패키지 core/ 분리 (하이브리드) + 문서 drift 정리: cache 가 한 폴더에 SPI/router/settings/config/fail-open/exception 다 모여 있어, messaging/notification core/ 컨벤션과 맞춰 공통 계약·정책만 분리. 이동(전부 public → 가시성 변경 0, encapsulation-neutral, httpclient resilience/diagnostics 와 동일 패턴): CacheStore·CacheBackend·CacheBackendException·CacheStoreRouter·FailOpenCacheStorecache/core/; CacheRouterConfig·CacheBindingSettings 는 root 유지; cache/redis/ 불변. import: redis 파일들의 기존 cache.* import 를 cache.core.* 로 path 정정, root CacheRouterConfig 엔 신규 추가, 외부 테스트 3개(OptionalAdapterBeanGatingTest/DisabledAdapterSentinelTest/RedisCacheStoreTest)도 path 정정. adapter-outbound/CLAUDE.md cache 경로 갱신(CacheRouterConfig 만 root 유지).

    • 문서 drift 2건 동시 정리: redis/RedisClient 주석("RedisCacheStore 가 fail-open 적용" → 실제는 중앙 FailOpenCacheStore 데코레이터가 CacheBackendException 을 cache-miss 로 downgrade); core/CacheStore 메서드 javadoc("for the Redis binding" → 모든 backend, 중앙 데코레이터); application.yml optional-adapter 주석("disabled → fail-fast sentinel" 일반화가 cache/notification 엔 부정확 → messaging=Disabled* sentinel bean, cache/notification=router(CacheStoreRouter/RoutingNotifier) unbound fail-fast 로 구분 명시).
    • 검증: cache 스코프 테스트 32/32, CleanArchitectureTest 49/49 PASS, 모듈 컴파일 0 에러. 가드레일 무영향(..adapter.outbound.. 재귀 패턴이 cache.core 자동 커버).

묶음

본 feature branch 는 더 이상 leaf 아님 — 2026-06-09 실 구현으로 errors / interview / blog-topic 파생 자료 누적.

오류 기록 (본 feature 작업 중 발생)

면접 준비 (이 작업에서 나올 수 있는 면접 질문)

  • raw/interviews/optional-adapter-3-layer-disabled-detection-2026-06-09 — startup(@ConditionalOnProperty bean-gating) / build(ArchUnit 정적 격리 + @Bean gating) / runtime(AdapterDisabledException fail-fast) 3계층 disabled-adapter 검출과 각 계층의 보장·한계, fail-open vs fail-closed, runtime 전용 error code 신설(A2) 근거.

Blog topics (이 작업에서 나올 수 있는 글감)

관련 일일 노트

이 브랜치를 작업한 날짜들. 양방향 nav 유지.

  • 2026-06-09 — Layer 1/2/3 + per-adapter fail-open 계약 실 구현 및 locally-verified.
  • 2026-06-16 — messaging broker-SPI 전환 후속 코드 주석 drift 6건 정리(동작 무변경). 위 §마주친 문제 2026-06-16 참조.

완료 후 정리

  • PR 링크:
  • 리뷰 메모:
  • 머지 결과 / 배포 환경:
  • wiki 추출 대상 (verified만, wiki/projects/로만 추출):
    • actually-implemented 항목: 3-layer disabled-adapter 검출(Layer1 @ConditionalOnProperty bean-gating / Layer2 DisabledAdapterArchitectureTest 격리+gating / Layer3 AdapterDisabledException); per-adapter fail-open 계약(Kafka publish→correlationId+outbox 위임, Redis unavailable→cache-miss, Slack/Email→관측+무PII); common OutboundDependencyLogger; A2 runtime 전용 ADAPTER_DISABLED error code.
    • locally-verified 항목: 위 전부 — :shared-contract:test/:adapter-outbound:test/:adapter-web:test/:app-bootstrap:test + verifyCleanArchitectureDependencies/verifyEnvKeys/verifyPublicPathSnapshot PASS, ca-architect-sentinel PASS.
    • prod-verified 항목: (없음 — 미배포)
  • 추출하지 않을 항목 (planned / documented-only / abandoned): 실제 broker/cache/provider 운영 연동(integration seam 구현은 fork 프로젝트 몫 — OUT_OF_BRANCH_SCOPE); primary-outcome notification 의 fail-closed override(도메인 branch 몫).