Files
clean-architecture-backend-…/docs/superpowers/specs/2026-08-15-five-adapter-runtime-remediation-review-design.md
T

79 KiB

다섯 Adapter 최종 통합 리뷰 및 개선 설계

  • 기준 일자: 2026-08-15
  • 기준 Git HEAD: 2f5d2fc21954286213c1474d19935f571ef896ea
  • 대상: MongoDB, Messaging, Notification, JPA, GraphQL과 app-bootstrap, 환경 설정, Docker/Keycloak/MinIO smoke 구성, Gradle build logic
  • 판정: CHANGES REQUIRED — 현재 상태는 최종 완료 기준을 충족하지 못함
  • 설계 상태: 단일 배포 artifact + 다섯 master switch 기본 false 방향 승인, 상세 구현 전 written-spec 검토 대기
  • 변경 범위: 이 문서는 리뷰와 개선 설계만 기록한다. production/test/build 코드는 아직 변경하지 않는다.

1. 결정 사항

“다섯 모듈을 모두 포함한다”는 말은 다섯 모듈의 runtime facade가 하나의 app-bootstrap bootJar classpath에 존재한다는 뜻이다. 다섯 모듈이 자동으로 활성화된다는 뜻이 아니다.

최종 구조는 다음 계약을 따른다.

  1. MongoDB, Messaging, Notification, JPA, GraphQL은 각각 명시적인 master env switch를 가진다.
  2. 다섯 master switch의 기본값은 모두 false다.
  3. false는 단순히 endpoint 하나를 숨기는 값이 아니다. 해당 adapter의 detail 설정 바인딩, bean, client, connection pool, background thread, migration, health contributor와 endpoint가 모두 없어야 한다.
  4. true인데 필수 설정이나 실제 provider 구현이 빠진 경우에는 요청 시점이 아니라 startup에 fail-closed한다.
  5. profile은 보안·데이터 안전성의 환경별 제약을 정하고, adapter 활성화 여부는 env switch가 정한다. local이라는 이유만으로 JPA/H2가 암묵적으로 켜지거나 prod라는 이유만으로 특정 adapter가 자동 활성화되지 않는다.
  6. Messaging처럼 내부 leaf가 많은 family는 모든 leaf를 app-bootstrap에 직접 나열하지 않는다. application port를 구현하는 outbound bridge와 Stable runtime facade만 composition root가 본다.

이 방향은 다음 대안과 비교해 선택했다.

대안 장점 문제 판정
provider 문자열이 비었는지를 on/off로 사용 새 key가 적음 master 상태가 불명확하고 relay·worker 같은 종속 기능과 쉽게 모순됨 기각
profile별로 dependency/JAR를 다르게 빌드 artifact가 작음 같은 artifact를 env만 바꿔 배포할 수 없고 검증 조합이 폭증함 기각
단일 artifact + 명시적 master switch 동일 binary를 환경별로 재사용하고 off/on 계약을 자동 검증 가능 구조적 bean gating과 startup dependency validator가 필요 채택

2. 사용자 완료 기준별 현재 판정

완료 기준 현재 판정 핵심 근거
env 외부화와 adapter on/off 실패 Mongo/GraphQL은 shipped runtime에 없고, JPA switch는 전체 adapter를 끄지 못하며, Messaging은 broker 문자열을 switch로 사용한다.
local/dev/prod 분리 부분 구현, 완료 아님 profile YAML은 있으나 base가 profile 누락을 local로 처리하고, local Compose의 PostgreSQL과 실제 local H2 runtime이 다르며 dev Compose merge가 실패한다.
패턴·코드 최적화 부분 구현, 완료 아님 port/adapter, Strategy, typed value, state machine 등 좋은 기반은 있으나 production assembly와 단일 activation authority가 끊겨 있다.
전체 test와 local/dev/prod 기동 실패 전체 test 1건 실패, local/dev 기본 기동 실패, prod는 안전 validator에서 차단되며 Mongo/GraphQL/Notification/Messaging의 실제 on 경로는 기동되지 않았다.
editor/build/runtime warning zero 실패 javac/Gradle deprecation은 현재 재현되지 않았지만 BeanPostProcessor와 Micrometer runtime WARN이 재현되고 IDE 설정은 일부 진단을 전역 숨김 처리한다.
Gradle/settings 비대화 해소 실패 root build 2,767줄, settings 185줄, JPA/Mongo/GraphQL leaf와 evidence script에 반복되는 source set/test-lane/API-surface 기계 코드가 남아 있다.

따라서 현재 ordinary unit test의 대부분이 통과한다는 사실만으로 다섯 adapter가 환경별로 조립되고 운영 가능한 상태라고 결론내릴 수 없다.

3. 검토 범위와 증거 등급

3.1 직접 실행한 검증

명령/행동 결과 증거 범위
./gradlew help --warning-mode all --console=plain --no-daemon 성공 Gradle 9 구성 시 deprecation warning 없음
./gradlew compileJava compileTestJava --warning-mode all --console=plain --no-daemon --continue 성공, 116 tasks javac -Werror, deprecation/unchecked compile warning 없음
./gradlew test --warning-mode all --console=plain --no-daemon --continue 실패, 187 tasks Messaging secret static scan 1건 실패; 뒤쪽 JPA/Mongo/GraphQL ordinary test까지 실행됨
focused secret scan 재실행 동일 실패 flaky가 아닌 deterministic failure
verifyDocumentedLeafCount 성공 현재 scanner가 확인하는 문서만 통과; 검증 사각지대는 별도 발견
local profile 기본 bootRun 실패 messaging off인데 outbox relay가 기본 on이라 startup 거부
local + relay 임시 off 성공, app/readiness HTTP 200 H2 기반 wiring smoke만 증명
local PostgreSQL Compose DB healthy DB service만 증명; local app은 profile상 H2를 사용하므로 결합 증거가 아님
dev profile 기본 bootRun 실패 추적된 .envddl-auto=update와 Flyway 안전 규칙 충돌
dev + relay off + ddl validate 임시 override 성공, PostgreSQL 16.14/Flyway schema v9/Hibernate validate/HTTP 200 임시 보정 후 dev JPA 기동만 증명
prod + relay off + ddl validate 예상된 실패 TLS 없는 JDBC URL을 sslmode=verify-full validator가 exit 71로 거부
local Compose config 성공 app, db 두 service 렌더링
dev Compose config 실패 /var/tmp/heap가 base tmpfs와 dev bind volume에 중복 mount됨

임시 PostgreSQL 컨테이너는 검증 뒤 정지했고 named volume은 보존했다.

3.2 이번 리뷰가 증명하지 않은 것

다음은 아직 실행하지 않았으므로 UNVERIFIED다.

  • 다섯 adapter가 모두 off인 local/dev/prod 실제 bootJar 기동
  • 각 adapter를 하나씩 on으로 한 full-context 기동
  • 다섯 adapter를 동시에 on으로 한 충돌·dependency validation
  • Mongo replica set, Kafka/Rabbit, Notification provider의 실제 round trip
  • GraphQL /graphql에 JWT를 포함한 실제 요청과 policy pipeline
  • Keycloak realm import, token 발급, issuer/audience/role claim 검증
  • MinIO bucket bootstrap과 실제 object round trip
  • prod profile을 만족시키는 TLS PostgreSQL 기반 성공 기동
  • clean check와 모든 strict/release/fault/performance qualification lane
  • 사용자의 현재 IDE Problems 창과 동일한 JDT/IntelliJ 진단 전수

4. 현재 runtime composition 문제

4.1 classpath 존재와 활성화가 일치하지 않는다

src/config/architecture/modules.json:54-72,140-165,193-201,215-234src/app-bootstrap/build.gradle:55-72,152-160을 대조하면 다음과 같다.

대상 app-bootstrap runtime membership main runtime dependency 현재 활성화 의미
JPA 있음 implementation 항상 JPA config가 scan되고 app.jpa-platform.enabled는 부가 platform만 gate
MongoDB 없음 없음 코드의 enabled=true를 설정해도 shipped app에는 class 자체가 없음
Messaging legacy bridge 있음 implementation broker 문자열 blank 여부가 사실상 switch; module graph는 존재
Notification 있음 implementation legacy provider selector와 delivery-platform switch가 병존
GraphQL 없음 qualification test only env로 켤 수 없고, main artifact에는 endpoint가 없음

Mongo와 GraphQL의 empty membership은 과거 “opt-in build-only” 정책으로는 일관되지만, 이번에 확정한 “동일 artifact에서 env switch” 요구와는 양립하지 않는다. Registry membership, allowed dependency, app-bootstrap dependency를 한 변경 단위로 맞춰야 한다.

4.2 broad scan이 master switch를 우회한다

CaSkeletonApplicationdev.caskeleton.adapter 전체를 component scan하고 configuration-properties scan한다(src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java:29-53). 이 구조에서는 leaf가 classpath에 들어온 순간 stereotype과 @ConfigurationProperties가 master auto-configuration 밖에서 발견될 수 있다.

JPA가 실제 사례다.

  • JpaPlatformRuntimeAutoConfigurationapp.jpa-platform.enabled condition은 matchIfMissing=true이고 부가 platform bean만 감싼다 (.../JpaPlatformRuntimeAutoConfiguration.java:42-48).
  • PostgreSQL config는 vendor가 없으면 기본 활성화되고 PersistenceJpaConfig를 import한다 (.../PostgreSqlPersistenceConfig.java:21-31).
  • PersistenceJpaConfig는 entity/repository package를 넓게 scan한다 (.../PersistenceJpaConfig.java:25-70).

따라서 현재 switch를 false로 해도 JPA adapter 전체가 off가 아니다.

GraphQL도 classpath에 추가만 하면 안전하지 않다. GraphQlPlatformAutoConfiguration은 master condition이 없고(.../GraphQlPlatformAutoConfiguration.java:84-86), Spring Boot GraphQL auto-configuration 및 component-scanned controller도 함께 활성화될 수 있다.

여기서 leaf root condition과 component-scan exclusion만 적용해도 충분하다고 가정하면 안 된다. starter와 classpath resource가 유발하는 Spring Boot JPA/Mongo/GraphQL auto-configuration은 leaf root import 밖에서 동작한다. Mongo가 이미 사용하는 MongoOptInAutoConfigurationImportFilter처럼, off일 때 vendor Boot auto-configuration까지 차단하는 composition mechanism을 각 runtime에 두고 exact excluded-class contract를 검증한다. JPA/Flyway/Hibernate, Mongo client/repository, GraphQL schema/web endpoint가 off에서 0개인지 full-context bean/resource inventory로 증명해야 한다. DataSource는 JPA 전용으로 간주하지 않고, JPA·outbox·notification store 등 DB를 요구하는 활성 capability가 하나도 없을 때 composition root가 DataSource/Flyway auto-configuration과 pool 생성을 함께 막는다.

4.3 off 상태의 dependency closure가 안전하지 않다

현재 shipped defaults는 서로 모순된다.

  • app.messaging.broker는 local에서 blank다 (application-local.yml:175-183, src/.env:32-41).
  • ca-skeleton.outbox.relay-enabled는 true다 (application.yml:520-539).
  • validator는 이 조합이 outbox row를 DEAD까지 소진할 수 있어 startup을 거부한다 (OutboxRelayBrokerRequirementValidator.java:47-61).
  • idempotency provider의 base default는 jdbc다(application.yml:348-356).
  • readiness group은 db를 정적으로 포함한다(application.yml:248-278).
  • profile이 없으면 local이 되고 local은 H2/create-drop을 켠다 (application.yml:22-24, application-local.yml:33-64,122-126).

즉 “다섯 adapter 기본 off”를 구현하려면 switch 다섯 개만 추가해서는 안 된다. relay, idempotency, readiness, migration, datasource처럼 adapter를 소비하는 subordinate capability도 같은 dependency graph에서 계산해야 한다.

특히 management.endpoint.health.validate-group-membership=true와 정적 db membership은 JPA-off/all-off에서 존재하지 않는 contributor를 참조할 수 있으므로 all-off 계약과 직접 충돌한다. readiness group은 활성 capability에서 파생하거나, required dependency contributor만 조건부로 추가하되 이름 오타는 여전히 startup에 fail-closed해야 한다.

JPA/DataSource를 소비하는 shipped capability도 closed graph로 관리한다.

consumer JPA-off 계약
outbox/relay capability 전체 off 또는 JPA+Messaging dependency error
JDBC idempotency 기본 disabled; 선택 시 JPA dependency error
multi-instance distributed lock 기본 disabled; 선택 시 JPA dependency error
Notification durable store Notification mode가 요구할 때 JPA dependency error
Fileserver TransactionPort path Fileserver가 off이거나 대체 transaction provider가 없으면 JPA dependency error
migration/env validator, DB readiness/metrics JPA/DB consumer가 없으면 bean/group membership 0

이번 구현이 Fileserver 내부를 재설계한다는 뜻은 아니다. 다섯 adapter all-off를 깨는 composition consumer만 명시적으로 끄거나 dependency error로 만들고, 별도 module hardening은 별도 spec으로 남긴다.

5. 목표 activation contract

5.1 canonical env와 property

외부 운영 계약은 다음 다섯 env key로 고정한다. 모두 registry에 등록하고 inline default를 false로 둔다.

Adapter canonical env Spring property 기본값
JPA APP_PERSISTENCE_JPA_ENABLED ca-skeleton.persistence-jpa.enabled false
MongoDB APP_PERSISTENCE_MONGO_ENABLED ca-skeleton.persistence-mongo.enabled false
Messaging APP_MESSAGING_ENABLED app.messaging.enabled false
Notification APP_NOTIFICATION_PLATFORM_ENABLED ca-skeleton.notification.platform.enabled false
GraphQL APP_GRAPHQL_ENABLED backend.graphql.enabled false

Mongo on-path의 canonical subordinate selector는 다음처럼 별도 등록한다.

env Spring property 계약
APP_PERSISTENCE_MONGO_ACTIVE_PROFILE ca-skeleton.persistence-mongo.active-profile on일 때 nonblank 필수; 정확히 한 profile 선택

이번 shipped Stable 범위는 현재 platform 구현과 일치하는 imperative SYNC 하나로 제한한다. reactive starter/auto-configuration은 production runtime에서 제거하거나 master on 상태에서도 차단하고, reactive Mongo는 별도 설계·port·resource budget·qualification을 갖추기 전까지 지원 대상으로 표시하지 않는다. runtime은 sync client/pool을 정확히 하나만 만들며, 선언된 다른 profile은 구조를 검증하되 secret resolution, socket과 pool 생성은 active profile에만 수행한다.

GraphQL on-path의 deployment mode도 외부 계약으로 등록한다.

env Spring property required-when 허용값
APP_GRAPHQL_DEPLOYMENT_MODE backend.graphql.deployment-mode APP_GRAPHQL_ENABLED=true `LOCAL
runtime environment 허용 GraphQL mode
local LOCAL
dev DEV
prod PRODUCTION_INTERNAL 또는 PRODUCTION_PUBLIC 중 operator가 명시한 하나

TEST는 test-source context에서만 사용하고 STAGING은 별도 runtime environment가 생기기 전 shipped env key에서 허용하지 않는다. 기존 backend.graphql.production 또는 backend.graphql.environment를 GraphQL on 상태에서 사용하거나 새 key와 함께 쓰면 APP_GRAPHQL_DEPLOYMENT_MODE를 이름으로 안내하는 migration error다. master off에서는 이 detail namespace를 bind/validate하지 않는다.

docs/registries/env-keys.yaml이 이름, type, default, required-when, classification, restart policy와 필수 test의 SSOT가 된다. YAML, .env.example, configuration metadata와 문서는 registry에서 검증한다.

현재 activation 역할을 겸하는 다음 key는 subordinate setting으로 내리거나 제거한다.

  • APP_MESSAGING_BROKER: Messaging이 on일 때 선택하는 provider이며 master switch가 아니다.
  • APP_NOTIFICATION_SLACK_PROVIDER, APP_NOTIFICATION_EMAIL_PROVIDER: legacy route/provider selector이며 notification module switch가 아니다.
  • app.jpa-platform.enabled: 전체 JPA adapter switch로 오해되지 않도록 제거하거나 addons-enabled처럼 실제 범위가 드러나는 이름으로 바꾼다.

master scalar는 detail @ConfigurationProperties보다 먼저 strict하게 검사한다. 미설정은 false지만 raw 값은 공백 없는 true|false만 허용하며 대소문자 외 yes, 1, 오타와 빈 값은 silent-off가 아니라 configuration error다. canonical/legacy key가 동시에 있으면 값이 같아도 ambiguity로 거부하고, legacy key만 있으면 replacement를 이름으로 알려주는 migration error를 낸다. JPA의 구 app.jpa-platform.enabled와 broker 문자열 기반 implicit activation 모두 같은 규칙을 따른다. 이 early validator는 detail namespace를 bind하지 않아 off invariant를 깨지 않는다.

5.2 off invariant

master switch가 false인 각 adapter는 full-context test에서 다음을 모두 만족해야 한다.

  • leaf detail @ConfigurationProperties가 bind/validate되지 않는다.
  • leaf가 소유한 production bean 수가 0이다.
  • socket, client, connection pool, session, executor, scheduler, watcher를 만들지 않는다.
  • JPA off에서는 DataSource/HikariDataSource, EntityManagerFactory, Flyway, DB health/metrics도 0개다.
  • migration과 schema validation을 실행하지 않는다.
  • health contributor와 actuator detail을 등록하지 않는다.
  • inbound adapter라면 route/schema/controller를 노출하지 않는다.
  • 잘못된 detail 설정이 환경에 남아 있어도 off 상태의 startup을 막지 않는다.
  • application이 반드시 port bean을 요구한다면 disabled sentinel은 adapter가 아니라 composition root가 제공하고, 실제 호출 시 명확한 ADAPTER_DISABLED로 fail-fast한다.

이 불변식은 각 bean마다 condition을 반복해서 붙이는 방식으로 구현하지 않는다. 각 leaf의 stereotype 자동 발견을 막고, 하나의 root auto-configuration이 master condition을 소유한 뒤 하위 configuration을 import하는 구조적 gating을 사용한다.

이를 위해 CaSkeletonApplication의 broad adapter/property scan을 축소한다. 다섯 optional leaf의 설정은 각 root auto-configuration 내부 @EnableConfigurationProperties에서만 bind한다. 새 bean을 추가했을 때 condition을 빼먹어 자동 활성화되는 구조를 허용하지 않는다.

5.3 on invariant와 dependency validation

master switch가 true이면 다음 규칙을 startup에 검증한다.

조건 startup 계약
JPA on vendor, datasource, migration mode, schema policy와 실제 DB/pool metadata를 검증
Mongo on URI/secret/TLS/topology/schema/profile과 실제 MongoClientSettings 조립을 검증
Messaging on broker id와 production transport bridge가 필수; fake KafkaSender로 기동 증거를 만들지 않음
Notification on + SERVING 최소 한 provider profile과 해당 production assembler, secret, readiness가 필수
Notification on + INGEST_ONLY credential/live runtime 없는 versioned route metadata로 nonempty plan을 freeze해 durable accept/store; dispatch/recovery/reconciliation scheduler는 시작하지 않음
GraphQL on Spring GraphQL endpoint, execution pipeline, security/context resolver와 schema가 실제 request path에 연결
outbox relay on JPA persistence와 Messaging 및 실제 broker가 모두 on이어야 함
idempotency provider jdbc JPA가 on이어야 함; 기본 provider는 disabled로 변경
Notification persistence 현재 구현에서는 JPA on이 필수; 다른 store가 생기기 전 묵시적 fallback 금지

JPA와 Mongo는 서로 다른 application port를 구현하는 경우 동시에 켤 수 있다. 같은 port의 단일 구현을 둘 다 제공하는 경우 bean ordering이나 @Primary로 임의 선택하지 않고 startup에 ambiguity를 거부한다.

6. 모듈별 핵심 blocker와 해결 방향

이 절은 현재 통합 완료를 막는 항목만 다룬다. 각 플랫폼 내부의 전체 correctness/security backlog는 다음 상세 리뷰를 계속 권위 있는 입력으로 사용한다.

  • docs/reviews/2026-08-14-mongodb-module-code-review.md
  • docs/reviews/2026-08-14-messaging-module-code-review.md
  • docs/reviews/2026-08-14-notification-module-code-review.md
  • docs/reviews/2026-08-14-jpa-module-code-review.md
  • docs/reviews/2026-08-14-graphql-module-code-review.md

이전 리뷰의 HEAD와 현재 HEAD가 다르므로 이전 문서의 모든 실패를 현재 실패로 복제하지 않는다. 구현 시 각 finding을 현재 코드에서 다시 재현하고 characterization test를 먼저 추가한다.

6.1 JPA

JPA-INT-001 — whole-adapter switch가 없다.

부가 platform condition, vendor config, entity/repository scan과 Boot JPA/Flyway lifecycle을 하나의 master gate 아래로 옮긴다. JPA off에서 EntityManagerFactory, JPA repository, Hibernate session, JPA migration bean이 존재하지 않는 full-context test를 추가한다.

JPA-INT-002 — validator가 실제 runtime datasource를 보지 않는다.

JpaDataSourceSettingsapp.jpa-platform.datasource.*를 읽지만 실제 pool SSOT는 spring.datasource.hikari.*다 (JpaDataSourceSettings.java:14-19, application.yml:25-50). JpaDataSourceProfileValidator bean은 생성되지만 validateStable을 startup에서 호출하지 않는다 (JpaPlatformRuntimeAutoConfiguration.java:93-99,159-165). 중복 namespace를 제거하고 resolved DataSource/Hikari metadata를 직접 검증한다.

JPA-INT-003 — local H2는 default-off 계약과 분리해야 한다.

H2는 repository/unit test 또는 명시적인 비-release 개발 convenience task로만 사용한다. 사용자 완료 기준에 들어가는 local JPA runtime smoke는 PostgreSQL/Flyway/ddl-auto=validate를 사용해 dev와 같은 vendor semantics를 확인한다. profile 누락이 H2/create-drop으로 이어지지 않게 하고, deployable jar는 profile 누락을 거부한다. 따라서 “local all-off”는 DB 없이 boot하고, “local JPA on”만 Compose PostgreSQL을 요구한다.

JPA-INT-004 — bootstrap의 migration/outbox bean이 JPA master 밖에 있다.

MigrationStartupConfig는 무조건 FlywayMigrationStrategy, datasource env validator와 prod Flyway validator를 만들고(MigrationStartupConfig.java:14-37), OutboxConfig는 relay false여도 settings, validator, RNG, leader token과 OutboxMetrics(OutboxStorePort)를 조립한다(OutboxConfig.java:27-94). JPA off/all-off에서는 migration config 전체가 없어야 하며, outbox config는 outbox.enabled 같은 capability root 아래에서 JPA+Messaging dependency closure를 만족할 때만 진입해야 한다. relay false는 scheduler만 끄는 값이지 outbox capability 전체 off와 같은 의미로 사용하지 않는다.

6.2 MongoDB

MNG-INT-001 — shipped app에서 활성화할 수 없다.

Mongo leaf의 runtime membership과 app-bootstrap allowed/dependency edge를 동시에 추가한다 (modules.json:68-72, app-bootstrap/build.gradle:55-72). 현재 코드의 false default (MongoPersistenceProperties.java:14-22)를 canonical env와 연결한다.

MNG-INT-002 — typed profile/security와 실제 Boot client가 분리되어 있다.

현재 코드와 test가 참조하는 spring.data.mongodb.*는 Spring Boot 4 metadata에서 deprecated error level이며 canonical namespace는 spring.mongodb.*다. deprecated namespace에 새 activation 설계를 얹지 않는다. 실제 Boot client에 platform profile, credential resolver, TLS, Stable API와 pool/timeout 정책은 그 builder에 연결되지 않는다. active profile과 secret reference를 소비하는 client factory/customizer를 한 SSOT로 만들고 real MongoClientSettings를 test한다.

runtime은 active-profile 하나를 명시해 정확히 한 application credential과 한 underlying sync client/pool을 만든다. reactive path는 이번 Stable 범위에서 조립하지 않고, 같은 active profile을 다른 URI/pool로 조용히 복제하지 않는다. profiles map에 존재한다는 이유만으로 사용하지 않는 profile의 secret을 resolve하거나 client를 만들지 않는다.

MNG-INT-003 — startup validation이 probe 부재 시 fail-open한다.

MongoPlatformAutoConfiguration의 startup check는 MongoTopologyProbe bean이 있을 때만 생성된다 (.../MongoPlatformAutoConfiguration.java:137-183). Mongo on에서는 live client로 probe를 만들거나 필수 probe 부재 자체를 startup error로 처리한다.

이 startup check는 application runtime credential, active profile, live schema/version/topology와 활성 capability가 관측된 server capability에 맞는지를 검증한다. admin credential/gateway는 migration 또는 deployment job의 별도 composition이 소유하며 shipped application이 bind하거나 요구하지 않는다. 현재 MongoPlatformAutoConfiguration처럼 admin credential 부재를 runtime validation 전체 누락과 같은 것으로 취급하거나 transactions/change streams를 true, true로 하드코딩하지 않고, 실제 활성 capability flags를 typed settings에서 전달한다 (MongoPlatformAutoConfiguration.java:137-183).

이번 shipped Stable 범위에서 transaction은 typed subordinate switch 기본 false로 두고 on일 때 data-plane credential/client의 실제 replica-set capability를 검증한다. change stream은 별도 CHANGE_STREAM principal/client/pool과 합산 resource budget이 설계·구현되기 전까지 experimental이며 항상 false, 관련 bean/thread 0이다. 현재 replica-set qualification이 server의 change-stream 가능성을 관측하더라도 shipped runtime 지원 증거로 승격하지 않는다. 이후 promotion 시에는 write credential을 재사용하지 않고 별도 credential/client cardinality와 failure/resume-token contract를 새 acceptance로 추가한다.

MNG-INT-004 — 같은 secret reference의 역할 분리가 우회된다.

credential identity hash가 role까지 포함해 같은 secret reference를 서로 다른 credential처럼 보이게 한다. identity hash에서 role을 분리하고 cross-role same-secret regression을 추가한다.

MNG-INT-005 — 현재 activation authority가 세 경로로 갈라져 있다.

기존 ImportFilter, component-scanned MongoPersistenceConfig, auto-imported MongoPlatformAutoConfiguration을 각자 master로 두지 않는다. AutoConfiguration.imports에는 master-gated Mongo root 하나만 두고 root가 persistence/platform child를 import한다. ImportFilter는 off일 때 Boot Mongo vendor auto-configuration을 차단하는 하부 메커니즘으로만 남긴다. root/child/import-filter가 같은 master scalar를 해석하는지, off의 sync/reactive inventory가 모두 0인지, on의 sync client/template/repository는 정확히 하나의 pool을 공유하고 reactive inventory는 0인지 test한다.

6.3 Messaging

MSG-INT-001 — module off와 relay on의 기본 조합이 startup을 깨뜨린다.

APP_MESSAGING_ENABLED=falseca-skeleton.outbox.relay-enabled=false를 shipped default로 맞춘다. relay는 master가 아니라 dependent capability이며, relay on은 JPA+Messaging+broker 조합을 필수로 한다.

MSG-INT-002 — legacy bridge와 신규 messaging platform의 authority가 둘이다.

현재 app은 adapter:outbound:messaging만 사용하고 신규 src/messaging/* leaf는 모두 build-only다. 최종 runtime은 application-owned port를 구현하는 outbound bridge 하나를 유지하고, 그 bridge가 검증된 Stable messaging facade를 사용하게 한다. 현재 실제 application-owned 계약은 OutboxMessagePublishPort이고, legacy generic MessagePublisher는 adapter-local 타입이다. 일반 publish가 application use case에 필요하면 먼저 application port로 정의하며 adapter-local publisher를 core로 유출하지 않는다. MessagingSettings, legacy KafkaSender와 신규 MessagingProperties가 같은 app.messaging namespace를 나눠 갖지 않게 하고, 신규 typed properties를 단일 settings owner로 삼은 뒤 legacy config를 characterization과 함께 제거한다. legacy/new publisher가 동시에 같은 event를 발행하는 dual-write는 금지한다.

runtime edge도 이름만 “facade 사용”으로 남기지 않는다. 목표 graph는 다음과 같다.

app-bootstrap
  -> adapter:outbound:messaging       # application port bridge
  -> messaging runtime starter       # Stable runtime assembly
adapter:outbound:messaging
  -> messaging-core-api              # platform publish contract만
messaging runtime starter
  -> selected platform implementation leaves

이를 위해 registry에 app-bootstrap -> messaging-spring-boot-starteradapter-outbound-messaging -> messaging-core-api edge를 추가한다. 현재 runtime membership gate가 direct project dependency만 비교하는 한 transitive messaging leaf가 실제 bootJar에 들어와도 registry에서 비어 있게 된다. gate를 resolved runtimeClasspath project closure 기준으로 바꾸고 starter와 실제 포함되는 internal leaf 모두에 app-bootstrap membership을 기록한다. 지원하지 않거나 qualification을 통과하지 못한 leaf는 starter dependency와 membership 양쪽에서 제외한다.

MSG-INT-003 — 실제 production sender가 없다.

legacy Kafka config는 project-supplied KafkaSender를 요구하지만 production app에는 구현 bean이 없고 test만 fake를 제공한다. 신규 platform도 production publisher/router/consumer pipeline이 완성되기 전에는 runtime membership을 얻지 않는다. on smoke는 실제 Kafka/Rabbit client로 round trip해야 한다.

MSG-INT-004 — starter auto-configuration이 master gate를 우회한다.

현재 starter의 AutoConfiguration.imports는 core/Kafka/Rabbit/reliability 등 다섯 configuration을 각각 독립 등록하고, 이 child configuration에는 messaging master condition이 없다. imports에는 master-gated root 하나만 남기고 root가 selected provider/reliability child를 import하게 한다. broker client library가 classpath에 있다는 이유로 Kafka와 Rabbit이 동시에 조립되어서는 안 되며, off full-context test는 starter imports와 vendor Boot auto-configuration까지 포함해 bean/client/thread 0을 확인한다.

MSG-INT-005 — 전체 test를 막는 secret scan은 false positive를 분류하지 못한다.

실패 산출물은 credentialId()와 숫자 fencing leaseToken() + 1을 secret concatenation으로 잡는다. scanner는 credentialId()의 trailing () 때문에 safe Id suffix를 인식하지 못한다 (SecretLeakStaticScanTest.java:39-50,146-165). 실제 secret 탐지 능력은 유지하면서 method-call id와 numeric fencing token characterization을 먼저 추가하고 parser를 수정한다. 단순 allowlist로 test를 끄지 않는다.

6.4 Notification

NTF-INT-001 — production provider assembler가 없다.

configuration은 List<ProviderRuntimeAssembler>로 profile을 조립하지만 (NotificationPlatformProviderConfig.java:78-99) production main source에는 구현이 없다. SERVING은 사용할 provider family별 real assembler, secret resolver, timeout/rate/permit/readiness를 구성해야만 켜진다. 지원하지 않는 provider는 문서에서 Stable로 표시하지 않는다.

NTF-INT-002 — mode SSOT가 drift했다.

Java enum은 SERVING|INGEST_ONLY다 (NotificationPlatformMode.java:11-23). env registry는 SERVING|ACCEPT_ONLY를 선언한다 (docs/registries/env-keys.yaml:4266-4277). INGEST_ONLY를 canonical 이름으로 고정하고 YAML, registry, docs와 tests를 맞춘다.

NTF-INT-003 — INGEST_ONLY도 workers를 시작한다.

worker config는 master enabled만 확인하고 background workers와 scheduler를 무조건 start()한다 (NotificationPlatformWorkerConfig.java:135-166). mode별 lifecycle condition을 분리해 INGEST_ONLY에서는 dispatch/recovery/reconciliation thread가 0개임을 검증한다.

NTF-INT-004 — legacy env namespace와 실제 bean condition이 다르다.

registry/YAML은 provider selector를 쓰지만 optional bean test와 실제 config는 slack-webhook.enabled, google-email.enabled, routes.* 문법을 혼용한다. delivery platform을 canonical runtime으로 정하고 legacy R0 selector는 migration shim 뒤에 격리한 뒤 제거한다.

NTF-INT-005 — off 상태와 선택하지 않은 provider의 설정·secret까지 읽을 수 있다.

application의 broad @ConfigurationPropertiesScan 때문에 master가 false여도 notification settings가 bind될 수 있다 (CaSkeletonApplication.java:46-53, NotificationPlatformSettings.java:16-32). 또한 현재 NotificationPlatformSecretsConfig는 활성 provider/mode에서 필요한 secret만 도출하지 않고 하나의 keyring에 여러 provider key를 함께 요구한다 (NotificationPlatformSecretsConfig.java:31-50,59-84,94-124). root gate 밖에서는 settings를 등록하지 않고, on 상태에서도 selected provider capability graph에서 필요한 secret 집합만 계산한다. 사용하지 않는 provider의 빈 secret이 startup을 막거나, 반대로 필요한 secret 누락이 첫 발송까지 숨어서는 안 된다.

현재 NotificationPlatformRuntimeConfig의 import 목록에는 codec/observability/provider/policy/dispatch/worker만 있고 auto-configuration, secrets, registries는 broad scan에 의존한다. callback MVC controller와 security, WebFlux route도 master가 아니라 callbacks.enabled만 본다. 구조적 gating 변경에는 다음 owner graph를 명시적으로 포함한다.

기능 owner/gate
outbound codec, secrets, registries, provider, policy, dispatch, worker notification root + master
callback MVC/WebFlux/security inbound web root + notification master + callbacks subordinate switch
notification JPA facade/store notification master + JPA master + selected persistence mode
Boot/vendor auto-configuration 위 활성 graph에서 필요할 때만 composition root가 허용

mode와 callback/provider에 따른 최소 graph는 다음으로 고정한다.

상태 필요한 secret/bean 금지 항목
master off activation scalar 외 0 모든 notification settings, endpoint, secret, store, worker
INGEST_ONLY durable store, credential 없는 versioned route metadata; provider/contact/callback secret 0, request payload at-rest key는 NTF-INT-007 결정에 따라 0 또는 필수 provider/VAPID/callback/contact secret, callback endpoint, live provider, dispatch/recovery/reconciliation/scheduler/provider-event-replay
SERVING, callbacks off contact encryption/lookup + selected provider credential/request lookup; WebPush일 때만 VAPID callback endpoint/signing/fingerprint/payload protection와 callback replay
SERVING, callbacks on 위 항목 + callback signing/fingerprint/payload protection와 replay 선택하지 않은 provider의 secret/assembler

현재 두 mode 안에서는 INGEST_ONLY + callbacks=true를 invalid startup 조합으로 거부한다. 이 조합이 실제 운영 요구가 되면 callback ingest/replay semantics를 별도 mode/capability로 설계한 뒤 허용한다.

기존 APP_NOTIFICATION_PLATFORM_ENABLED를 canonical master로 재사용하되 현재 verifyNotificationConfiguration이 prefix 집합의 우연한 일치만 보는 것으로 끝내지 않는다. master의 YAML/registry/example 기본값 false를 exact assertion으로 고정하고, legacy provider/route selector가 activation key로 다시 들어오지 않는 negative assertion을 둔다.

NTF-INT-006 — 현재 INGEST_ONLY의 durable accept는 이후 발송 가능한 상태를 보장하지 않는다.

현재 plan writer는 accept 시 provider-specific routing plan을 동결하고 route catalog가 비면 empty plan을 저장한다. 저장된 snapshot은 dispatch 때 그대로 소비되므로, INGEST_ONLY에서 provider/route 없이 받은 row는 나중에 SERVING으로 재기동해도 자동으로 발송 가능해지지 않는다 (CanonicalNotificationPlanWriter.java:82-128, PolicyRoutePlanner.java:50-76, RecipientDeliveryRecord.java:15-24,40-49, NotificationDispatchService.java:298-307).

이번 설계는 accept 후 자동 replan을 하지 않는다. 대신 credential과 live provider bean 없이도 읽을 수 있는 versioned route metadata(provider family/id, channel eligibility, route config version)를 INGEST_ONLY startup에 필수로 하고, accept 시 nonempty immutable plan과 route version을 동결한다. empty route catalog/plan은 startup 또는 accept 경계에서 명시적으로 거부한다. SERVING 전환 시 production assembler registry가 저장된 모든 provider ID/version을 지원하는지 검증하고, 불일치 row를 조용히 재해석하지 않는다. 정책 변경으로 replan이 필요하면 operator 승인·idempotency·audit를 갖춘 별도 backfill/migration으로 수행한다.

qualification은 INGEST_ONLY accept -> process stop -> SERVING restart -> 동일 frozen route로 1회 delivery를 실제 DB/provider fixture에서 검증한다. INGEST_ONLY 단계에서는 provider credential, provider runtime과 모든 worker가 0개여야 한다.

NTF-INT-007 — notification variables payload가 평문으로 저장된다.

현재 accept path는 encoded.variablesPayload()를 request row에 그대로 넣으며 (CanonicalNotificationPlanWriter.java:60-79), PAYLOAD_ENCRYPTION key는 notification request가 아니라 callback raw payload protection에서만 소비된다(AesGcmCallbackPayloadProtection.java:88-97). 따라서 INGEST_ONLY에 사용하지 않는 encryption secret을 억지로 요구해 이 문제를 가리지 않는다.

Notification을 Stable로 승격하기 전에 저장 payload의 sensitivity contract를 결정한다. application-level encryption을 채택하면 codec/port, ciphertext envelope, key ID, rotation/history, 기존 row migration과 query/dispatch 복호화 failure contract를 함께 구현한다. 채택하지 않으면 허용 가능한 variable type을 정적으로 제한하고 DB/storage encryption과 접근통제만으로 충분하다는 threat model과 검증 근거를 남긴다. 어느 쪽도 없이 현재 평문 저장을 production-ready로 승인하지 않는다.

6.5 GraphQL

GQL-INT-001 — shipped app에서 활성화할 수 없고 classpath에 넣으면 자동 활성화될 수 있다.

GraphQL의 empty runtime membership과 qualification-only dependency를 main runtime edge로 바꾸되, 먼저 backend.graphql.enabled=false root condition과 structural scan exclusion을 추가한다.

GQL-INT-002 — 안전 mode가 두 축으로 모순된다.

GraphQlPlatformProperties 기본값은 production=false이면서 environment는 PRODUCTION_PUBLIC이다(GraphQlPlatformProperties.java:32-54). production boolean과 environment enum을 위 backend.graphql.deployment-mode 하나로 합치고 active runtime environment와의 모순을 startup에 거부한다. 현재 익명 principal, allow-by-default authorization과 request protection 일부가 boolean production만 사용하므로 이 모순은 단순 표시 문제가 아니라 production 안전 규칙을 우회할 수 있는 configuration split-brain이다 (GraphQlPlatformStartupValidator.java:33-45, GraphQlPlatformAutoConfiguration.java:302-343,441-459).

GQL-INT-003 — policy object가 실제 endpoint path를 통과해야 한다.

auto-configuration과 startup validator의 존재만으로 cost/authz/cursor/idempotency 정책 적용을 증명하지 않는다. random-port /graphql test에서 정책 위반 요청이 resolver/use case 호출 전에 거부되고 JWT actor/tenant context가 전달되는지를 증명한다.

GQL-INT-004 — 현재 release gate는 shipped JWT composition을 증명하지 않는다.

ConditionalTransportCompositionContractTest는 GraphQL class 존재만 확인하고, GraphQL HTTP qualification은 test-only Basic Auth를 사용한다 (ConditionalTransportCompositionContractTest.java:15-46, GraphqlHttpBoundaryQualificationTest.java:40-59,189-229). graphqlStableTest required-class guard도 현재 CI 경로와 일치하지 않고 같은 HTTP qualification이 ordinary/conditional lane에 중복될 수 있다 (graphql-platform-conventions.gradle:63-100, ci-quality-gates.yml:48-53). app-bootstrap bootJar + 실제 JWT decoder + random-port /graphql을 한 composition lane에서 기동하고, required qualification은 정확히 한 blocking lane이 소유하게 한다.

정본 task는 :app-bootstrap:graphqlRuntimeQualification로 고정한다. 이 task는 bootJar에 의존하고 graphqlRuntimeQualificationTest source set의 dev.caskeleton.bootstrap.graphql.GraphQlJwtRuntimeQualificationTest를 required class로 강제한다. test는 생성된 jar를 child process로 실행하고 같은 tracked realm artifact를 import한 Keycloak container에서 client-credentials token을 받아 실제 HTTP /graphql을 검증한다. JUnit XML은 app-bootstrap/build/test-results/graphqlRuntimeQualification, sanitized process/claim/startup log는 app-bootstrap/build/evidence/graphql-runtime/에 남기며 zero discovery/skip/stale XML을 거부한다.

root conditionalTransportQualification의 기존 GraphQL Basic Auth leg는 이 app-bootstrap task로 교체하고 gRPC/WebSocket leg만 그대로 둔다. GraphqlHttpBoundaryQualificationTest는 module contract test로 남길 수 있지만 release evidence에는 집계하지 않는다. CI quality job은 :adapter:inbound:graphql:graphqlStableTest :app-bootstrap:graphqlRuntimeQualification conditionalTransportQualification을 실행하며 aggregator가 같은 GraphQL task를 두 번 실행하지 않도록 단일 dependency edge를 둔다.

7. 환경 분리와 runtime smoke 설계

7.1 설정 파일 책임

파일/소스 책임
application.yml 공통 typed shape, 다섯 master switch false, subordinate safe defaults
application-local.yml local 안전 제약과 개발 편의값; adapter 자동 활성화 금지
application-dev.yml dev 안전 제약, Flyway/observability shape; address/secret 하드코딩 금지
application-prod.yml prod fail-closed 제약; 실제 secret/endpoint 하드코딩 금지
.env.example 전체 공개 key catalog의 예시, secret은 빈 placeholder/reference
.env.local.example local opt-in 조합 예시
실제 .env* gitignored operator input

application.yml${SPRING_PROFILES_ACTIVE:local} fallback을 제거한다. bootRun convenience가 필요하면 Gradle local task가 명시적으로 local을 전달하고, bootJar/container/IDE의 profile 누락은 startup error가 된다. 현재 bootRunsrc/.env를 직접 파싱하는 구현 (app-bootstrap/build.gradle:251-272)도 환경별 file 선택이 가능한 단일 loader 또는 Spring 표준 config import로 교체한다.

deployable runtime의 환경 profile cardinality는 정확히 1이다. docs/registries/env-keys.yamlSPRING_PROFILES_ACTIVE를 default 없는 enum local|dev|prod로 바꾸고 CSV로 취급하지 않는다. missing, blank, unknown, local,prod 같은 multiple value를 startup에 거부한다. 기능/adapter 선택을 보조 Spring profile로 표현하지 않고 다섯 master switch를 사용한다. test-source context의 test profile은 deployable bootJar acceptance와 분리하며 release artifact가 test로 기동되는 것은 거부한다. 기존 local fallback을 기대하는 EnvProfileMatrixContractTest와 profileless 허용 test를 새 fail-closed contract로 교체한다.

추적된 src/.envSPRING_PROFILES_ACTIVE=local, APP_DATASOURCE_DDL_AUTO=update, local credential은 실제 실행 input으로 사용하지 않는다. example과 local override를 분리해 dev/prod가 local 값을 상속하지 않게 한다.

이 전환과 함께 verifyEnvKeys의 입력 계약도 바꾼다. 현재 task는 src/.env 존재를 강제하고 required placeholder와 그 파일을 직접 비교한다(src/build.gradle:2209-2229,2263-2276). 검증 SSOT를 env registry, profile YAML, .env.example, generated configuration metadata로 옮기고 gitignored operator .env*와 실제 secret 값은 build input에서 제거한다. example 파일이 없어도 통과하거나 실제 secret 파일이 있어야만 통과하는 두 false contract를 모두 막는다.

현재 docker-compose.dev.yml은 개발 편의/JMX 설정만 바꾸고 SPRING_PROFILES_ACTIVE=dev, env source와 DB service를 소유하지 않는다. 따라서 base+dev만 합치면 local/H2 fallback 또는 외부 주소 우연성에 의존한다. dev overlay가 profile과 필요한 infra include 순서를 직접 선언하거나, 하나의 wrapper가 정본 merge 순서를 고정해야 한다. “개발자가 다른 override도 알아서 함께 넣었다”는 결과는 dev 구성의 합격 증거가 아니다.

7.2 Compose 구성

현재 root Compose에는 app만 있고 local override에 PostgreSQL만 있다. Keycloak, realm import, MinIO와 prod smoke file은 없다. dev override는 base tmpfs /var/tmp/heap와 같은 target의 bind mount를 추가해 merge 단계에서 실패한다(docker-compose.yml:53-69, docker-compose.dev.yml:19-21,55-60).

dev에서 heap dump를 host에 보존해야 하므로 Docker Compose 최소 버전을 2.24.4로 문서/CI에 고정하고 dev override가 tmpfs: !override []로 base tmpfs를 교체한 뒤 /var/tmp/heap bind mount를 정확히 한 번 선언한다. !override의 최소 버전과 sequence/unique-resource merge 의미는 Docker Compose merge rules를 기준으로 한다. 빈 sequence가 base sequence를 자동 삭제한다고 가정하지 않고 merged JSON에서 target uniqueness를 검증한다.

목표 구성은 “실제 운영 Compose”와 “로컬 prod 흉내”를 혼동하지 않는다.

구성 목적 최소 service
base/off smoke 다섯 adapter off boot app
local JPA JPA opt-in app, PostgreSQL
local Mongo Mongo opt-in app, Mongo replica set
local Messaging broker round trip app, Kafka 또는 Rabbit; outbox면 PostgreSQL 추가
local Notification ingest durable accept/worker-off smoke app, PostgreSQL
local Notification serving 실제 reference provider smoke app, PostgreSQL, Mailpit/SMTP reference provider
local Notification handoff ingest 재기동 전달 상태 보존 app, PostgreSQL, Mailpit/SMTP reference provider
local GraphQL auth JWT request app, Keycloak + imported realm
shared infra smoke 사용자 요구 infra 확인 PostgreSQL, MinIO, Keycloak
prod-smoke prod validator와 boot 증거 TLS PostgreSQL, Keycloak, 필요한 secret references; 테스트 전용

Compose service는 profile로 선택해 기본 docker compose up이 모든 인프라를 무조건 띄우지 않게 한다. MinIO는 이번 다섯 adapter 중 하나가 아니므로 존재 확인과 object-storage smoke에만 사용하고, 별도 object-storage switch 없이 암묵 활성화하지 않는다.

공통 infra service는 새 docker-compose.infra.yml이 소유하고 environment overlay와 섞지 않는다. app은 항상 모델에 존재하며 다음 profile/service 집합을 config/runtime/compose-profile-contracts.json의 SSOT로 고정한다.

아래 base, infra, local, dev, prod-smoke는 각각 docker-compose.yml, docker-compose.infra.yml, docker-compose.local.yml, docker-compose.dev.yml, docker-compose.prod-smoke.yml을 뜻하며 표에 적힌 순서로 merge한다.

contract lane Compose profile file stack explicit Spring runtime config --services 기대 집합(sorted)
off-local 없음 base+local local app
off-dev 없음 base+dev dev app
off-prod 없음 base+prod-smoke prod app
local-jpa local-jpa base+infra+local local app,db
local-mongo local-mongo base+infra+local local app,mongo,mongo-rs-init
local-messaging local-messaging base+infra+local local app,kafka
local-messaging-outbox local-messaging-outbox base+infra+local local app,db,kafka
local-notification-ingest local-notification-ingest base+infra+local local app,db,notification-smoke
local-notification-serving local-notification-serving base+infra+local local app,db,mailpit,notification-smoke
local-notification-handoff local-notification-handoff base+infra+local local app,db,mailpit,notification-smoke
local-graphql local-graphql base+infra+local local app,auth-smoke,keycloak
shared-infra-local shared-infra base+infra+local local app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke
shared-infra-dev shared-infra base+infra+dev dev app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke
prod-smoke prod-smoke base+infra+prod-smoke prod app,auth-smoke,db,keycloak,minio,minio-init,object-storage-smoke
all-adapters all-adapters base+infra+local local app,auth-smoke,db,kafka,keycloak,mailpit,mongo,mongo-rs-init,notification-smoke

Compose profile은 service 선택 축일 뿐 Spring profile을 암묵 설정하지 않는다. 정본 wrapper가 표의 Spring runtime과 gitignored/generated env input을 app에 명시적으로 주입하고, app activation report의 resolved profile이 표와 정확히 같은지 확인한다. 세 off lane도 각각 환경을 명시하며 profileless Spring boot를 합격 증거로 사용하지 않는다. auth-smokelocal-graphql|shared-infra|prod-smoke|all-adapters, object-storage-smokeshared-infra|prod-smoke에만 속한 one-shot client다. 둘은 up --wait 대상이 아니고 black-box assertion 뒤 성공 종료해야 한다. notification-smoke는 세 local notification profile과 all-adapters에 속한 one-shot HTTP/Mailpit assertion client다. minio-init은 bucket/policy bootstrap owner이고 object-storage-smoke의 round trip을 대신하지 않는다.

scripts/verify-compose-profile-contracts.shdocker compose version --short의 semver, 각 row의 config --services exact set(off lane은 profile option 생략, 나머지는 --profile <name>), rendered app의 exact SPRING_PROFILES_ACTIVE, --profile '*' config --format json 전체 모델과 service별 volume/tmpfs target uniqueness를 검증한다. CI와 local 검증은 이 script만 정본 진입점으로 사용해 사람이 file 순서, Compose profile이나 Spring runtime을 빠뜨리지 않게 한다.

동적 정본 진입점은 scripts/run-compose-runtime-smoke.sh 하나다. 같은 config/runtime/compose-profile-contracts.json의 blocking lane을 읽으며 다음 순서를 내부에서 강제한다.

  1. lane/run-id별 새 COMPOSE_PROJECT_NAME과 mode 0700 임시 디렉터리를 만들고 env/secret 파일은 mode 0600으로 생성한다. 기존 evidence 디렉터리가 있으면 재사용하지 않고 실패한다.
  2. verify-compose-profile-contracts.sh, config, create를 먼저 통과시킨다.
  3. long-running service만 up --wait하고 app health/readiness 및 resolved activation report를 확인한다.
  4. lane에 선언된 auth-smoke, object-storage-smoke, notification-smoke를 각각 run --rm으로 실행하고, JPA lane은 app의 migration/schema/TLS report를 assertion한다. required one-shot이 누락·skip·nonzero면 lane 전체가 실패한다.
  5. src/app-bootstrap/build/evidence/runtime-smoke/<lane>/<run-id>/manifest.json, Compose/service-set digest, activation/health, DB migration/TLS, sanitized Keycloak claim, MinIO round-trip, warning/error summary를 남긴다. raw token, URI credential, secret 값과 rendered secret은 artifact에 쓰지 않는다.
  6. 성공/실패 모두 먼저 sanitized logs와 container exit를 수집한 뒤 trap에서 해당 고유 project에만 down --volumes --remove-orphans를 실행하고 임시 env/secret을 지운다. 다른 project나 named volume은 건드리지 않는다.

--matrix config/runtime/compose-profile-contracts.json은 blocking lane 전부를 zero-discovery/zero-skip로 실행하고, --lane <id>는 같은 계약의 focused 재현만 허용한다. CI가 wrapper 내부 command를 일부 복제해 우회하거나 과거 evidence를 성공으로 집계하지 않는다.

local-notification-handoff는 두 독립 lane을 이어 붙이지 않는 composite stateful lane이다. wrapper는 같은 project, PostgreSQL service와 named volume을 유지한 채 다음 phase를 순서대로 실행한다.

  1. INGEST_ONLY phase env로 DB와 app을 기동하고 credential 없는 Mailpit route metadata로 notification-smoke의 accept request를 저장한다. 저장된 request/route version을 evidence에 기록하고 provider bean/call과 worker/thread가 0인지 activation report로 확인한다.
  2. app만 정상 종료한다. DB/volume은 내리지 않고 같은 project에서 SERVING phase env와 reference provider 설정으로 app을 --force-recreate한 뒤 Mailpit을 ready 상태로 만든다.
  3. 첫 phase의 request ID와 frozen route version으로 notification-smoke를 다시 실행해 Mailpit API의 정확히 한 message, DB terminal state와 같은 route version을 확인한다. 최소 한 dispatch poll window를 더 기다린 뒤 duplicate가 0인지 다시 확인한다.
  4. 두 phase와 중간 app exit가 모두 성공한 뒤에만 공통 evidence 수집과 project teardown으로 진행한다. 첫 phase 뒤 volume을 삭제하거나 두 번째 project로 row를 복사한 결과는 handoff 증거가 아니다.

7.3 Keycloak realm acceptance

infra/keycloak/realms/ca-skeleton-realm.json 같은 재현 가능한 import artifact를 둔다. secret 값은 realm JSON에 넣지 않는다. realm은 ca-skeleton-api를 client authentication과 service account가 켜진 confidential client로 정의하고 standard flow/direct access grant는 끈다. service account에는 realm role user와 client role graphql-query를 부여하며 audience mapper는 ca-skeleton-apiaud에 넣는다. smoke 인증 방식은 선택지 없이 OAuth 2.0 client_credentials 하나로 고정한다.

smoke는 최소 다음을 확인한다.

  1. realm ca-skeleton이 import되었다.
  2. resource client/audience ca-skeleton-api가 존재한다.
  3. application이 사용하는 role과 role/permission claim mapping이 존재한다.
  4. ca-skeleton-api service account의 client credentials로 token을 발급한다.
  5. token의 nonblank sub, 정확한 iss, aud=ca-skeleton-api, realm_access.rolesuser, resource_access.ca-skeleton-api.rolesgraphql-query를 확인한다.
  6. public health는 무인증 성공하고 protected REST/GraphQL endpoint는 유효 token만 성공한다.
  7. 잘못된 realm, audience, expired token은 기대한 safe error contract로 거부된다.

현재 local issuer 기본값 localhost:8081은 host에서는 가능해도 app container 안에서는 app 자신을 가리킨다. JWKS discovery가 lazy라 startup 성공만으로 이 오류가 드러나지 않는다 (application-local.yml:68-76,142-145, JwtDecoderConfig.java:25-58). 이를 한 hostname이 모든 실행 위치에서 우연히 해석된다고 가정하지 않고 lane별 issuer를 외부화한다. bootJar qualification은 Testcontainers의 mapped Keycloak URL 하나를 token endpoint와 app issuer 양쪽에 주입한다. Compose smoke는 app과 one-shot auth-smoke client를 같은 network에 두고 http://keycloak:8080/realms/ca-skeleton을 양쪽에 주입한다. 다른 URL에서 받은 token으로 검증하거나 startup 성공만 보고 합격시키지 않는다.

qualification script가 매 run URL-safe 임의 secret 파일을 mode 0600으로 만들고 Compose/Testcontainers secret로 mount한다. Keycloak wrapper는 /run/secrets/keycloak-graphql-smoke-client-secret을 읽어 process-local KEYCLOAK_GRAPHQL_SMOKE_CLIENT_SECRET으로 export한 뒤 /opt/keycloak/bin/kc.sh start-dev --import-realm을 exec하며, realm artifact에는 ${KEYCLOAK_GRAPHQL_SMOKE_CLIENT_SECRET} reference만 둔다. secret 값은 Git, rendered config, command line, JUnit XML과 sanitized evidence에 남기지 않고 종료 시 파일을 제거한다. test user, password grant와 direct access grant는 만들지 않는다. realm JSON 환경변수 placeholder와 container startup import의 근거는 Keycloak realm import 공식 계약을 따른다.

7.4 MinIO acceptance

MinIO readiness만 보고 성공으로 처리하지 않는다. init service가 test bucket과 최소 policy를 만들고, object-storage qualification이 upload/head/download/delete와 잘못된 credential을 검증한다. prod profile 검증에 local MinIO credential을 재사용하지 않는다.

기존 object-storage qualification은 자체 Testcontainers와 임의 credential을 소유하므로 새 Compose MinIO service의 endpoint/bucket/policy를 검증한 증거가 아니다. Compose smoke는 외부 endpoint를 소비하는 별도 black-box lane으로 두고, 기존 isolated qualification과 이름·artifact를 분리한다. object-storage를 app-bootstrap runtime에 실제 포함할지 여부는 이번 다섯 master switch와 별도 결정이며, 포함하지 않으면 MinIO smoke client도 release test fixture일 뿐 production bean이 아니다.

black-box client owner는 docker-compose.infra.yml의 one-shot object-storage-smoke service와 infra/minio/smoke/ fixture다. client image는 digest로 고정하고 wrapper가 만든 lane 전용 endpoint, bucket과 secret file만 읽는다. known bytes를 임의 object key에 upload하고 HEAD의 size/checksum, download의 byte equality, delete 후 not-found를 순서대로 검증한 다음 의도적으로 잘못된 credential이 거부되는지도 확인한다. 어떤 단계도 skip할 수 없고 결과는 위 runtime-smoke evidence의 minio-roundtrip.json에 secret 없이 기록한다. local/static credential을 prod-smoke에 전달하거나 minio-init 성공을 이 round trip의 대체 증거로 사용하지 않는다.

8. 디자인 패턴 적용 원칙

현재 보존할 패턴은 port/adapter, Strategy/provider registry, typed value object, sealed result algebra, state machine, Null Object 관측 구현이다. 추가 적용은 다음처럼 제한한다.

8.1 적용할 패턴

  • Conditional auto-configuration as Plugin boundary: root condition 하나가 adapter graph 전체를 소유한다.
  • Strategy + registry: broker/provider 선택은 switch가 아니라 closed descriptor와 실제 구현 registry를 조립한다. unknown/duplicate는 startup에 거부한다.
  • Factory/Builder: Mongo client, broker client, notification provider처럼 secret/TLS/pool/lifecycle을 함께 구성해야 하는 resource를 한 factory가 만든다.
  • State machine + fencing: outbox, notification delivery, broker settlement의 durable 전이는 owner/fencing token과 DB compare-and-set으로 보호한다.
  • Typed settings + validator: @Value 문자열 분산과 중복 namespace를 제거하고 resolved runtime object를 검증한다.
  • Decorator: metrics/redaction/retry는 core behavior를 바꾸지 않는 경계 기능에만 쓴다.

8.2 피할 패턴

  • adapter bean마다 같은 @ConditionalOnProperty를 복제하는 방식
  • class 이름이 AutoConfiguration인 plain factory와 실제 auto-configuration의 혼용
  • ObjectProvider 부재를 silently no-op으로 해 production wiring 누락을 숨기는 방식
  • JPA/Mongo 구현 충돌을 @Primary로 우연히 해결하는 방식
  • fake/in-memory implementation을 production runtime fallback으로 제공하는 방식
  • provider별 의미가 다른 release matrix를 하나의 과도한 generic DSL로 합치는 방식
  • build.gradle 내용을 단순히 여러 apply from 파일로 옮기고 중복 모델은 그대로 두는 방식

패턴 수를 늘리는 것이 목표가 아니다. 하나의 activation authority, 하나의 publication authority, 하나의 settings SSOT와 실제 실행 경로를 만드는 것이 목표다.

9. warning/error zero 설계

9.1 현재 확인된 상태

  • Gradle --warning-mode all help와 Java compile은 warning 없이 성공했다.
  • Java compile은 root convention의 -Werror -Xlint:deprecation -Xlint:unchecked를 사용한다 (src/build.gradle:353).
  • local/dev/prod 시도에서 다음 runtime WARN이 재현되었다.
    • RolePermissionPolicy, RolePermissionRegistry, AuthorizationAdapterBeanPostProcessorChecker early-instantiation warning
    • Prometheus registry에 meter가 등록된 뒤 MeterFilter를 추가했다는 warning 2건
    • dev에서는 Flyway converter 관련 BeanPostProcessorChecker warning
  • MetricsContractConfig@PostConstruct에서 registry를 직접 가져와 filter를 설치한다 (MetricsContractConfig.java:17-50). 이 시점이 이미 등록된 meter보다 늦다.
  • SampleMetricsContractConfig에도 같은 late-filter 조립이 복제되어 있어 app 쪽 한 파일만 바꾸면 warning이 남을 수 있다(SampleMetricsContractConfig.java:18-41).
  • logback-spring.xmlSPRING_PROFILES_ACTIVE를 별도로 읽고 기본값을 local로 둔다. 실제 Spring active profile을 CLI로 prod로 바꿔도 .env의 local 값이 남으면 구조화 로그의 profile 필드가 local로 기록되는 drift가 재현되었다(logback-spring.xml:8-9).
  • .vscode/settings.json:7-14는 Spring missing-configuration 진단을 workspace 전체에서 숨긴다.
  • .vscode/jdt-compiler.prefs:21-24는 세 JDT warning category를 rationale과 함께 숨긴다. 빌드와 JDT의 오탐 차이는 인정하되, 새 실제 warning을 가리는 blanket suppression은 허용하지 않는다.

9.2 해결과 acceptance

  1. MeterFilter는 registry 생성 전 적용되는 Boot-supported customizer/configurer로 등록하고 순서를 명시한다.
  2. authorization E2E context에서 infrastructure advisor와 Spring Data projection 후처리기가 AuthorizationPort를 조기 resolve하는 bean creation chain을 최소 context로 재현한다. lookup을 ObjectProvider/supplier로 지연하거나 불필요한 slice auto-configuration을 제외하되, 단순 ROLE_INFRASTRUCTURE 표시로 warning만 숨기지 않는다.
  3. Flyway converter warning은 Boot/Flyway configuration 생성 순서를 최소 reproduction으로 고정하고 framework bug인지 application eager dependency인지 구분한 후 수정한다.
  4. Spring configuration 누락 false positive를 만드는 legacy shim을 structural import로 바꾼 뒤 전역 IDE ignore를 제거하거나 정확한 file/symbol 범위로 좁힌다.
  5. Hikari leak detection 메시지는 메모리 누수와 동일시하지 않는다. 실제 connection leak test와 ThreadLocal/executor lifecycle test를 별도로 실행한다.
  6. boot smoke의 WARN allowlist는 기본 empty다. third-party에서 즉시 제거할 수 없는 warning은 owner, upstream issue, expiry가 있는 registry entry로 구현 중 임시 격리할 수 있지만, 사용자가 별도 예외를 승인하지 않는 한 최종 warning-zero 완료 판정에는 allowlist도 0건이어야 한다.
  7. 최종 증거는 Gradle 성공뿐 아니라 local/dev/prod startup log의 WARN/ERROR 0건을 포함한다. IDE Problems 0건은 같은 JDK/extension/settings를 명시한 export 또는 화면으로 사람이 별도 확인하며 Gradle gate가 IDE 전체를 검증했다고 주장하지 않는다.
  8. 구조화 로그의 profile 필드와 Spring Environment#getActiveProfiles()가 모든 smoke에서 일치해야 한다.

10. Gradle/settings 축소 설계

10.1 현재 측정

파일 줄 수
src/build.gradle 2,767
src/settings.gradle 185
src/app-bootstrap/build.gradle 273
JPA leaf build.gradle 352
Mongo leaf build.gradle 346
Messaging adapter build.gradle 105
Notification leaf build.gradle 39
GraphQL leaf build.gradle 241
src/gradle/jpa-evidence.gradle 930

큰 파일이라는 사실 자체보다 다음 중복이 문제다.

  • custom source set + extendsFrom + Test task + failOnNoDiscoveredTests 반복
  • strict qualification lane 등록과 결과 디렉터리 wiring 반복
  • testkit artifact/source set wiring 반복
  • API surface snapshot renderer/update/verify 기계 코드 반복
  • registry parsing/validation이 settings와 root verification에서 각자 구현됨
  • release evidence task의 공통 manifest/출력 처리 반복
  • dependency exclusion/lock 의도가 module별로 달라 실제 runtime graph와 주석이 drift함

settings.gradle:37-44는 registry가 leaf count의 SSOT라는 프로젝트 정책과 달리 44를 다시 하드코딩한다. verifyDocumentedLeafCount는 현재 root 일부 문서만 검사해 다른 module 문서의 과거 leaf count를 놓친다.

10.2 목표 구조

build-logic included build에 test 가능한 precompiled convention plugin을 둔다.

convention 책임
ca.java-leaf Java 21, encoding, compiler/Error Prone, baseline test
ca.optional-adapter activation metadata와 disabled/on composition contract wiring
ca.strict-test-lane source set/configuration/Test task/fail-closed 공통 기계 코드
ca.testkit-publisher testkit source set와 consumable artifact
ca.api-surface read-only verify와 explicit approved update task
ca.evidence manifest/result schema, deterministic output, no-empty-evidence rule
ca.architecture-registry settings include/mapping, allowed edge와 runtime membership validation
ca.dependency-policy 공통 exclusion/constraint와 resolved graph/lock 검증

최종 파일 책임은 다음과 같다.

  • settings.gradle: plugin management, root name, registry settings plugin 적용만 둔다.
  • root build.gradle: 공통 plugin/version 선언과 architecture-wide lifecycle task만 둔다.
  • leaf build.gradle: plugin, project/external dependency, leaf 고유 semantic lane/matrix만 둔다.
  • provider/vendor별 이미지, scenario, security 요구와 promotion 의미는 각 module registry에 남긴다.
  • convention plugin은 Gradle TestKit으로 malformed registry, empty lane, duplicate task, runtime membership drift를 검증한다.

ca.architecture-registry는 settings와 root가 각각 JSON 규칙을 재구현하지 않도록 included build의 typed parser/validator를 공유한다. registry에 없는 project build.gradle 디렉터리, 존재하지 않는 source path, 중복 ID/path, resolved runtime project closure와 membership drift를 fail-closed한다. 문서 count 검사는 tracked root AGENTS.md/CLAUDE.md와 모든 src/**/CLAUDE.md를 대상으로 숫자 복제를 제거하거나 drift를 실패시키고 check/CI에 연결한다.

dependency convention도 “주석만 제외하고 lockfile에는 남는” 상태를 허용하지 않는다. 예를 들어 Notification build가 YAML dependency 제외를 주장하면서 notification lock/runtime에는 SnakeYAML이 남는 경우(adapter/outbound/notification/build.gradle:21-33, 해당 gradle.lockfile:165)처럼, 실제 configuration별 resolved graph와 lockfile을 함께 검증한다. 단, provider가 실제로 직접 사용하는 dependency까지 공통 plugin에서 일괄 제거하지 않는다.

LOC 숫자를 맞추기 위해 logic을 숨기지 않는다. 완료 조건은 같은 source-set/Test/API-surface 기계 코드가 둘 이상의 leaf에 복사되지 않고, root/settings/leaf가 위 책임만 가지는지로 판단한다.

11. 구현 순서

Wave 0 — red baseline과 증거 고정

  • 현재 full test failure의 scanner characterization test 추가
  • 다섯 switch off full-context bean/resource inventory test 추가
  • local/dev 기본 boot failure와 dev Compose merge failure 재현 test 추가
  • runtime WARN capture test/harness 추가
  • registry membership/classpath mismatch와 존재하지 않는 release task 검증 추가

Wave 1 — activation SSOT와 classpath

  • 다섯 canonical env key와 typed metadata 등록
  • Mongo/GraphQL runtime membership, allowed edge, app dependency 추가
  • Messaging starter의 master-gated off contract를 focused context에서 먼저 고정하되 아직 shipped membership을 부여하지 않음
  • runtime membership gate를 direct dependency가 아닌 resolved project closure 기준으로 교체
  • verifyEnvKeys/notification verifier를 tracked example과 새 master/legacy negative contract에 맞춤
  • broad adapter/property scan 축소
  • 각 leaf root auto-configuration 아래 구조적 gating 적용
  • relay/idempotency/readiness/migration dependency validator 적용
  • all-off local/dev/prod boot를 먼저 green으로 만든다.

Wave 2 — module on-path blocker

  • JPA resolved datasource validation과 whole-adapter gating
  • Mongo client/profile/secret/topology wiring과 credential identity 수정
  • Messaging production bridge와 messaging-runtime-core starter/registry edge를 구현하고 live broker round trip을 통과한 변경 단위에서만 Stable facade/resolved closure membership을 원자적으로 추가; scanner 수정
  • Notification provider assembler, mode SSOT, INGEST_ONLY frozen-route handoff와 worker lifecycle 수정
  • GraphQL master gate, deployment mode와 real endpoint pipeline 연결

모듈별 상세 리뷰의 P0 correctness/security finding이 runtime path에 걸리는 경우 이 wave의 선행조건으로 처리한다. wiring만 연결해 이미 알려진 데이터 유실 경로를 production에 노출하지 않는다.

Wave 3 — 환경과 인프라 smoke

  • tracked example과 실제 env 분리
  • profileless startup fail-closed
  • Compose base/local/dev/prod-smoke 재구성
  • PostgreSQL, Mongo, broker, Mailpit, MinIO, Keycloak service/profile 추가
  • realm import와 token/REST/GraphQL smoke 자동화
  • prod-smoke TLS/secret reference 검증

Wave 4 — runtime warning/IDE error zero

  • MeterFilter 등록 시점 수정
  • BeanPostProcessor early-instantiation dependency 제거
  • Flyway warning root cause 수정
  • IDE blanket suppression 축소/제거
  • startup log warning gate와 resource/thread leak tests 적용

Wave 5 — Gradle build logic 리팩터링

Green인 동작 baseline 위에서 convention plugin을 하나씩 추출한다. 한 단계마다 task graph, dependency graph, test selection과 evidence output이 동일함을 검증한다. settings의 fixed module count와 문서 count 복제를 제거한다.

Wave 6 — 최종 qualification과 문서 동기화

  • 전체 clean check와 모든 blocking qualification 실행
  • all-off, one-on, dependency-error, all-on matrix 실행
  • local/dev/prod smoke와 HTTP/JWT/GraphQL/infra round trip 실행
  • generated configuration metadata와 env/docs drift 검증
  • LLM Wiki branch-note 및 실제 파생 errors/interview/blog raw 문서 캡처

12. 검증 매트릭스

12.1 build/architecture 기본 gate

cd src
./gradlew clean compileJava compileTestJava --warning-mode=fail --no-daemon --console=plain
./gradlew test --warning-mode=fail --no-daemon --console=plain
./gradlew check --warning-mode=fail --no-daemon --console=plain
./gradlew verifyCleanArchitectureDependencies verifyEnvKeys \
  verifyRuntimeModuleMembership verifyPublicPathSnapshot \
  verifyDocumentedLeafCount --console=plain

12.2 focused module gate

정확한 Gradle path는 src/config/architecture/modules.json에서 읽어 파생한다.

./gradlew :adapter:outbound:persistence-jpa:test --console=plain
./gradlew :adapter:outbound:persistence-mongo:test --console=plain
./gradlew :adapter:outbound:messaging:test --console=plain
./gradlew :adapter:outbound:notification:test --console=plain
./gradlew :adapter:inbound:graphql:test --console=plain
./gradlew :adapter:inbound:graphql:graphqlStableTest \
  :app-bootstrap:graphqlRuntimeQualification \
  conditionalTransportQualification --console=plain

Messaging platform은 registry가 가리키는 Stable facade/leaf focused tests와 live broker lane을 별도로 실행한다. ordinary test가 Docker-backed qualification을 대신하지 않는다.

현재 존재하는 persistence blocking lane은 ordinary test와 별도로 다음을 실행한다.

./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate \
  -Pjpa.matrix.versions=16 --console=plain
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate \
  -Pjpa.matrix.versions=17 --console=plain
./gradlew :adapter:outbound:persistence-jpa:jpaPlatformReleaseGate \
  -Pjpa.matrix.versions=18 --console=plain
./gradlew :adapter:outbound:persistence-mongo:mongoStableContractTest \
  :adapter:outbound:persistence-mongo:mongoReplicaSetTest \
  :adapter:outbound:persistence-mongo:mongoFailoverTest \
  :adapter:outbound:persistence-mongo:mongoMigrationTest \
  :adapter:outbound:persistence-mongo:mongoCompatibilityTest \
  :adapter:outbound:persistence-mongo:mongoSecurityIntegrationTest \
  :adapter:outbound:persistence-mongo:mongoPerformanceTest --console=plain

Mongo release registry가 현재 가리키는 mongoShardedTest, mongoAtlasTest, mongoKmsTest와 일부 class는 존재하지 않는다. Advanced capability를 이번 shipped Stable runtime에 포함한다면 task/class와 보호 환경 evidence를 먼저 구현한다. 포함하지 않는다면 registry에서 Stable blocking claim을 제거하고 명시적인 experimental/conditional promotion으로 내린다. task가 없는데 green인 release manifest는 허용하지 않는다.

12.3 activation matrix

Matrix 기대 결과
5개 off 외부 인프라 없이 boot/health 성공, adapter bean/resource/thread/endpoint 0
JPA only PostgreSQL/Flyway/Hibernate validate 성공; Mongo/GraphQL/broker/provider 없음
Mongo only replica set/client/topology/security 성공; JPA entity/repository/pool 없음
Messaging only broker publish/consume 성공; relay false면 DB 불필요
Notification + JPA, INGEST_ONLY nonempty versioned route를 동결해 durable accept, provider credential/call/worker 0; SERVING restart 후 같은 route로 1회 발송
Notification + JPA, SERVING reference provider 실제 delivery/receipt 성공
GraphQL only health schema endpoint와 security/policy pipeline 성공; persistence resolver 없음
relay on, dependency 누락 startup이 누락된 exact switch/provider를 이름으로 거부
JPA+Mongo distinct port면 성공, 같은 port 구현 충돌이면 startup 거부
5개 on 실제 dependency와 endpoint가 모두 ready; silent fallback/duplicate authority 없음

12.4 environment/runtime gate

각 profile은 bootJar/container를 실제로 띄우고 app health와 management readiness를 확인한다. dev/prod를 local override로 성공시킨 결과는 해당 profile의 성공 증거로 인정하지 않는다.

  • local: all-off, JPA, Mongo, broker, notification, GraphQL auth 조합
  • dev: externalized env, PostgreSQL/Flyway, Keycloak realm, MinIO smoke
  • prod-smoke: TLS PostgreSQL, safe JPA settings, secret references, JSON logging, Keycloak token
  • 모든 run: exit code, active profile, resolved adapter activation report, WARN/ERROR count를 artifact로 저장

Compose artifact 자체도 기동 전에 독립 검증한다.

./scripts/verify-compose-profile-contracts.sh
./scripts/run-compose-runtime-smoke.sh \
  --matrix config/runtime/compose-profile-contracts.json

# focused failure reproduction; matrix success를 대신하지 않는다.
./scripts/run-compose-runtime-smoke.sh --lane prod-smoke

matrix는 off-local, off-dev, off-prod, 각 local adapter lane, shared-infra-local, shared-infra-dev, prod-smoke, all-adapters의 정적 model과 runtime flow를 전부 소유한다. 특히 prod-smokeconfig로 끝내지 않고 TLS DB/app/Keycloak/MinIO를 기동해 auth-smokeobject-storage-smoke를 실행한 뒤 evidence를 수집하고 고유 project를 정리한다. 위 script와 contract 파일은 목표 구현 항목이며 현재 HEAD에 존재하거나 통과했다는 기록이 아니다.

13. Definition of Done

다음 조건을 모두 만족하기 전에는 “최종 완료”, “전체 통과”, “production-ready”라고 말하지 않는다.

  • 한 bootJar runtime classpath에 다섯 runtime facade가 존재한다.
  • 다섯 canonical master switch가 env registry/YAML/metadata/docs에 있고 모두 기본 false다.
  • all-off에서 외부 resource 없이 local/dev/prod smoke가 성공한다.
  • 각 adapter off invariant와 on fail-closed contract가 full-context test로 고정된다.
  • Mongo/GraphQL/Messaging Stable facade의 resolved runtime membership와 실제 dependency가 registry와 일치한다.
  • JPA switch가 DataSource/Hikari/entity/repository/Hibernate/Flyway/DB health·metrics graph 전체를 제어한다.
  • Messaging은 실제 broker bridge를 갖고 relay dependency가 일관된다.
  • Notification SERVING은 production assembler로 작동하고 INGEST_ONLY는 worker를 시작하지 않는다.
  • Notification handoff가 같은 project/DB volume에서 INGEST_ONLY accept, app restart, SERVING의 같은 frozen route 정확히 1회 전달과 duplicate 0을 증명한다.
  • GraphQL policy와 JWT context가 실제 /graphql request path에서 실행된다.
  • GraphQL blocking qualification이 bootJar JWT composition을 한 번만 실행하고 class-existence나 test-only Basic Auth를 release 증거로 사용하지 않는다.
  • SPRING_PROFILES_ACTIVE는 정확히 하나의 local|dev|prod이고 profileless/multiple/unknown deploy가 거부된다.
  • GraphQL on이면 environment에 허용된 APP_GRAPHQL_DEPLOYMENT_MODE 하나가 명시되고 legacy boolean/enum split-brain이 거부된다.
  • local/dev/prod profile과 env example/secret source가 분리되고 실제 secret 파일은 추적·렌더링·evidence 대상에서 제외된다.
  • Compose 최소 버전, profile별 exact service set, 전체 merged model과 mount target uniqueness가 정본 script로 검증된다.
  • runtime-smoke wrapper가 local/dev/prod blocking lane에서 create, up --wait, required one-shot, sanitized evidence와 고유 project teardown을 zero-skip로 수행한다.
  • PostgreSQL, Mongo, broker, MinIO, Keycloak/realm smoke evidence가 생성된다.
  • Keycloak realm은 client credentials 전용 service account, audience와 role claim을 제공하고 실제 JWT protected REST/GraphQL request가 lane별 동일 issuer URL로 성공한다.
  • 전체 testcheck, architecture/env/public-path/strict qualification이 모두 성공한다.
  • Gradle/Javac/Checkstyle/SpotBugs/runtime startup에 error/warning과 warning allowlist가 0건이고, 동일 toolchain의 IDE Problems 0건을 별도 확인했다.
  • 실제 active profile과 구조화 로그 profile 필드가 일치하고 late MeterFilter warning이 0건이다.
  • Gradle convention plugin 추출 뒤 task selection, dependency graph와 evidence 의미가 유지된다.
  • 세부 모듈 리뷰의 runtime 경로상 P0 blocker가 닫히거나 명시적으로 해당 capability가 계속 비활성/experimental 상태다.
  • 변경 파일, 명령, 결과, 미실행/차단과 증거 등급이 LLM Wiki branch-note에 캡처된다.

14. 남은 위험과 구현 경계

이 문서는 “기본 off인 조립 가능한 artifact”의 통합 설계를 승인한다. 다섯 대형 플랫폼의 모든 내부 알고리즘을 production-ready로 승인하지 않는다. 특히 Mongo transaction/cursor, Messaging outbox/inbox와 broker settlement, Notification durable state machine, JPA notification schema/lease, GraphQL execution-policy 연결은 기존 상세 리뷰의 P0/P1을 현재 HEAD에서 다시 확인해야 한다.

Gradle 리팩터링은 runtime 수정과 같은 diff에서 한꺼번에 수행하지 않는다. 먼저 동작과 환경 matrix를 green으로 만들고, 그 증거를 보존하는 characterization 위에서 build logic을 이동한다. 그렇지 않으면 task가 사라지거나 실행하지 않았는데도 “정리 후 green”으로 보이는 false evidence를 만들 수 있다.