1168 lines
79 KiB
Markdown
1168 lines
79 KiB
Markdown
# 다섯 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` | 실패 | 추적된 `.env`의 `ddl-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-234`와
|
|
`src/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를 우회한다
|
|
|
|
`CaSkeletonApplication`은 `dev.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가 실제 사례다.
|
|
|
|
- `JpaPlatformRuntimeAutoConfiguration`의 `app.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|DEV|PRODUCTION_INTERNAL|PRODUCTION_PUBLIC` |
|
|
|
|
| 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를 보지 않는다.**
|
|
|
|
`JpaDataSourceSettings`는 `app.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=false`와 `ca-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는 다음과 같다.
|
|
|
|
```text
|
|
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-starter`와
|
|
`adapter-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가 된다. 현재 `bootRun`만 `src/.env`를 직접 파싱하는 구현
|
|
(`app-bootstrap/build.gradle:251-272`)도 환경별 file 선택이 가능한 단일 loader 또는 Spring 표준 config
|
|
import로 교체한다.
|
|
|
|
deployable runtime의 환경 profile cardinality는 정확히 1이다. `docs/registries/env-keys.yaml`의
|
|
`SPRING_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/.env`의 `SPRING_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](https://docs.docker.com/reference/compose-file/merge/)를 기준으로 한다. 빈
|
|
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-smoke`는 `local-graphql|shared-infra|prod-smoke|all-adapters`,
|
|
`object-storage-smoke`는 `shared-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.sh`가 `docker 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-api`를 `aud`에 넣는다.
|
|
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.roles`의 `user`,
|
|
`resource_access.ca-skeleton-api.roles`의 `graphql-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 공식 계약](https://www.keycloak.org/server/importExport)을 따른다.
|
|
|
|
### 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`, `AuthorizationAdapter`의
|
|
`BeanPostProcessorChecker` 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.xml`은 `SPRING_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
|
|
|
|
```bash
|
|
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`에서 읽어 파생한다.
|
|
|
|
```bash
|
|
./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와 별도로 다음을 실행한다.
|
|
|
|
```bash
|
|
./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 자체도 기동 전에 독립 검증한다.
|
|
|
|
```bash
|
|
./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-smoke`도 `config`로 끝내지 않고 TLS DB/app/Keycloak/MinIO를 기동해 `auth-smoke`와
|
|
`object-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로 성공한다.
|
|
- [ ] 전체 `test`와 `check`, 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를 만들 수 있다.
|