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

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

18 KiB

kind, slug, title, topic, project, status, sourceRevision, rootTreeNode, evidenceCapturedOn, assets, evidence, source
kind slug title topic project status sourceRevision rootTreeNode evidenceCapturedOn assets evidence source
CASE a-validator-checking-the-wrong-datasource validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다 transaction-deadline-and-pool clean-architecture-backend-template 게시 전 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916 case:a-validator-checking-the-wrong-datasource 2026-09-03
key file
a-validator-checking-the-wrong-datasource ../../../final/evidence/rendered/a-validator-checking-the-wrong-datasource.svg
key file
a-validator-checking-the-wrong-datasource-run ../../../final/evidence/rendered/a-validator-checking-the-wrong-datasource-run.svg
../../../final/evidence/raw/a-validator-checking-the-wrong-datasource.txt
../../../final/evidence/raw/a-validator-checking-the-wrong-datasource-run.txt
이 사건의 1차 기록은 두 곳이다. 하나는 `JpaDataSourceProfileValidator` 와 `JpaResolvedDataSourceValidationTest` 두 클래스의 자바독이고, 다른 하나는 저장소 자신의 설계 문서 `docs/superpowers/specs/2026-08-15-five-adapter-runtime-remediation-review-design.md:307` 의 `JPA-INT-002` 항목이다.
root-tree 노드가 가리키는 `final/document.md#a05 §14.5` 는 지금 그 파일에 없다. `final/document.md#a05` 에서 `14.5` 는 0 건이고, 그 문서에 이 검증기와 병렬 네임스페이스가 나오지 않는다.
원본 상류에 없는 것이 둘이다. `validateResolved` 를 부르는 프로덕션 코드가 지금은 하나 있다는 것과, 그 호출자가 발행된 빈을 쓰지 않고 자기 인스턴스를 만든다는 것이다.

validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다

JpaDataSourceProfileValidator 의 자바독에 두 결함이 적혀 있다. 이 검증기가 app.jpa-platform.datasource.* 를 읽었는데 요청을 받는 풀은 spring.datasource.hikari.* 에서 만들어졌다는 것, 그리고 그 병렬 네임스페이스가 어떤 출하 설정에도 없어 두 필드가 늘 널이었다는 것이다. 널에서 던지는 requirePoolBounds 를 부르는 코드가 없어서 애플리케이션은 그대로 기동했다.

관계

  • connection-timeout: 5s가 모든 prod 배포를 시작 실패시켰고 local만 통과했다 둘 다 spring.datasource.hikari.* 를 두고 벌어진 일이다. 저쪽은 그 값을 바인더가 거부했고, 여기는 다른 네임스페이스를 읽느라 그 값을 보지 못했다.
  • 중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다 풀을 만드는 네임스페이스와 검증기가 읽는 네임스페이스가 다르면, 검증기는 배포가 쓰지 않는 값을 보고 통과한다. 어느 쪽이 풀을 만드는지 먼저 확인해야 그 통과가 무엇에 대한 것인지 알 수 있다.
  • @Bean이 있다는 것은 조립 증거가 아니다 이 타입의 빈 정의가 하나 있는데 그것을 주입받는 프로덕션 코드가 없고, 실제 검사는 호출자가 직접 만든 객체가 한다.

문제

검증기는 배포가 실제로 향하는 데이터소스를 봐야 한다.

그런데 상한 값의 출처는 JpaDataSourceSettings 였다. 그것이 바인딩하는 접두는 app.jpa-platform.datasource 이고, 풀을 만드는 접두는 spring.datasource.hikari 다.

결론

자바독은 두 결함이 서로를 상쇄했다고 적는다.

첫째, JpaDataSourceProfileValidator 가 배포가 쓰지 않는 네임스페이스를 읽었다. 둘째, 그 네임스페이스는 출하 YAML 과 환경 키 레지스트리 어디에도 없어 두 필드가 늘 널이었다. requirePoolBounds 가 불렸다면 모든 배포가 기동에 실패했겠지만, 그것을 부르는 코드가 없었다.

지금은 JpaDataSourceSettings 클래스가 없고 설정 파일에서도 0 건이다. src 에 남은 일곱 자리 중 여섯은 자바독이고 하나는 회귀 테스트가 쓰는 검색 리터럴이다. 옛 메서드 이름 셋도 전부 자바독뿐이다.

다만 src 밖이 남았다. infra/jpa/postgres/README.md:18 이 운영자에게 max_connections 를 이제 없는 프로퍼티에 맞춰 잡으라고 지시한다.

validateResolved 는 해석된 데이터소스를 받아 널을 검사하고, 벤더 선택이 PostgreSQL 일 때만 커넥션을 열어 제품과 버전을 본다. 열지 못하면 던지므로 도달할 수 없는 데이터베이스가 첫 질의가 아니라 기동에서 실패한다.

여기서 원본 상류에 없는 것이 나온다.

지금은 그 검사를 부르는 프로덕션 코드가 있다. PersistenceJpaRootAutoConfiguration:113 한 자리이고, 그 자동설정은 ca-skeleton.persistence-jpa.enabled 뒤에 있으며 출하 기본값은 거짓이다. 그 메서드는 발행된 빈을 주입받지 않고 :109 에서 자기 인스턴스를 만드는데, 이유로 적힌 조건은 이미 제거됐다. 이 타입의 빈 정의는 JpaPlatformRuntimeAutoConfiguration:112 하나이고 그것이 JpaPlatformAutoConfiguration:49 에 위임한다. 그 빈은 어디에도 주입되지 않는다.

풀 상한 검사는 의도적으로 여기서 하지 않는다. 다만 HikariPoolConstraintValidator 가 읽는 다섯 키는 타임아웃과 수명과 누수 임계이고, maximum-pool-size 하한은 RuntimeNumericBoundsValidator:22 가 본다. 자바독이 함께 적은 이득 하나는 지금 성립하지 않는다 — HikariCP 는 선언만 testImplementation 이고 runtimeClasspath 에는 전이로 올라온다.

회귀 테스트가 그 부재를 고정한다. 훑을 목록이 비어 있지 않다는 것을 먼저 단언하고 주석을 걷어낸 뒤 검사하되, 훑는 범위는 두 모듈이다. 세 건을 돌려 전부 통과하는 것을 확인했다.

상류 어디에도 등급이 없다. root-tree 노드에도 candidate ledger 에도 설계 문서의 JPA-INT-002 항목에도 없어서, 이 기록도 새로 매기지 않는다. 두 결함은 닫혀 있고 남은 것은 관찰이다.

검증 환경

OpenJDK : 21.0.12 Spring Boot : 4.0.8 확인 방식 : 자바독의 사후 기록 확인, app.jpa-platform.datasource 와 requirePoolBounds 의 잔존 자리 전수, validateResolved 본문과 그 프로덕션 호출자·조건 확인, 이 타입의 빈 정의와 주입처 계수, runtimeClasspath 전개, 회귀 테스트 실행 소스 수정 : x

재현 조건

  1. JpaDataSourceProfileValidator 의 클래스 자바독을 읽는다. 두 결함과 그 상쇄가 적혀 있다.
  2. app.jpa-platform.datasource 를 src 안팎에서 각각 찾고, 설정 파일과 바인딩 클래스 쪽을 따로 센다.
  3. 남은 히트가 주석인지 코드인지 소스 세트별로 가른다.
  4. requirePoolBounds 가 남은 자리를 센다.
  5. validateResolved 가 무엇을 읽고 어떤 조건에서 무엇을 던지는지 본문에서 읽는다.
  6. 그것을 부르는 자리를 main 과 test 로 갈라 세고, 그 자동설정이 붙는 프로퍼티와 출하 기본값을 확인한다.
  7. 그 호출자가 검증기를 주입받는지 직접 만드는지 보고, 직접 만드는 이유로 적힌 조건이 지금도 있는지 확인한다.
  8. 이 타입의 @Bean 정의를 모두 찾고 그것이 무엇에 위임하는지 확인한 뒤, 그 빈을 받는 코드를 센다.
  9. HikariPoolConstraintValidator 가 읽는 키를 나열하고, maximum-pool-size 를 검사하는 코드를 따로 찾는다.
  10. 이 모듈의 HikariCP 선언 구성과 runtimeClasspath 전개 결과를 대조한다.
  11. 회귀 테스트의 단언과 가드와 스캔 범위를 읽고, 그 테스트를 실행해 결과를 읽는다.

본문

JpaDataSourceProfileValidator 의 클래스 자바독에는 이 검증기가 예전에 다른 데이터소스를 검증했다는 내용이 먼저 적혀 있다.

자바독이 적은 두 결함과 그 상쇄

:::evidence key="a-validator-checking-the-wrong-datasource" alt="저장소 루트에서 돌린 정적 검색 출력 158줄. JpaDataSourceProfileValidator 11행부터 19행까지의 자바독이 두 결함과 상쇄를 원문 그대로 적는다. 이어서 app.jpa-platform.datasource 가 src 안에 남아 있는 일곱 자리가 main 다섯과 test 둘로 나오고, src 밖에서 그 문자열을 쓰는 파일 다섯과 줄 수, 그중 infra 의 운영자용 표에 살아 있는 줄이 이어진다. 설정 파일에서는 0 건이고 주석이 아닌 줄은 main 0 test 1 이며 JpaDataSourceSettings 클래스도 0 건이다. requirePoolBounds 가 남은 세 자리는 전부 자바독이다. 그다음 validateResolved 의 본문 50행부터 66행, 그 호출자가 붙는 프로퍼티 조건과 출하 기본값 false, 직접 생성의 이유로 적힌 조건과 그 조건이 지금 제거됐다는 사슬, 호출자가 자기 인스턴스를 만드는 106행부터 114행이 나온다. 이 타입의 빈 정의는 하나이고 JpaPlatformAutoConfiguration 의 @Bean 개수는 0 이며 그 빈을 받는 프로덕션 코드는 0 건이다. 끝으로 HikariPoolConstraintValidator 가 읽는 다섯 키와 그 빈, maximum-pool-size 를 실제로 보는 다른 검증기, HikariCP 가 선언은 testImplementation 인데 runtimeClasspath 에는 올라온다는 출력, 회귀 테스트 세 건의 이름과 그 스캔 범위와 공허한 통과를 막는 단언이 보인다." caption="자바독의 두 결함 · 네임스페이스 잔존 위치와 설정 0 · validateResolved 의 본문과 조건 · 빈 정의 하나와 주입 0 · 풀 키의 실제 소유자 · HikariCP 의 선언과 런타임 · 회귀 테스트 셋 — 158줄 · exit 0" zoom="true" :::

첫 번째 결함으로 자바독이 적는 것은 읽는 대상의 어긋남이다. 상한 값은 JpaDataSourceSettingsapp.jpa-platform.datasource.* 에 바인딩한 설정에서 왔고, 요청을 처리하는 풀은 spring.datasource.hikari.* 에서 만들어졌다. 자바독은 이것을 풀 하나에 설명이 둘인 상태로 적고, 그러면 검증기가 실제로 쓰이지 않는 쪽 설명을 보고 통과할 수 있다고 적는다.

두 번째가 더 나쁘다고 자바독은 적는다. 그 병렬 네임스페이스는 어떤 출하 YAML 에도, 환경 키 레지스트리의 어떤 행에도 없었다. 두 필드가 항상 널이었고 requirePoolBounds 는 널에서 던진다. 무언가 그것을 불렀다면 모든 배포가 기동에 실패했을 것이다.

아무것도 그것을 부르지 않았고, 그래서 아무것도 실패하지 않았다. 자바독은 여기까지를 두 결함이 서로를 상쇄한 것으로 적고, 애플리케이션이 시작된 이유를 두 번째 결함이 첫 번째를 가린 것으로 적는다.

app.jpa-platform.datasource 가 지금 남아 있는 자리

src 안에서 이 문자열을 찾으면 일곱 자리가 나온다. 설정 파일에서는 0 건이고, 그것을 바인딩하던 JpaDataSourceSettings 클래스도 없다.

main 다섯은 전부 자바독이다. 그중 넷은 이 검증기가 아니라 다른 리프의 것으로, NotificationSmtpProviderConfig:48NotificationSmtpSettings:14SmtpProviderRuntimeAssembler:40 이 자기 결정의 이유를 대면서 이 사건을 선례로 인용하고, KafkaMessagingAutoConfiguration:119 도 브로커 주소를 두 번 기술하지 않는 이유로 같은 것을 든다. 나머지 하나가 검증기 자신의 :12 다.

test 둘 중 하나는 자바독이고, 나머지 하나는 JpaResolvedDataSourceValidationTest:57.contains("app.jpa-platform.datasource") — 회귀 테스트가 이 문자열을 찾을 때 쓰는 검색 리터럴이다.

src 밖에도 다섯 파일이 이 문자열을 쓴다. 넷은 설계 문서와 계획 문서지만 하나는 다르다. infra/jpa/postgres/README.md:18 이 운영자용 표에서 max_connectionsapp.jpa-platform.datasource.maximum-pool-size 에 맞춰 잡으라고 지금도 지시한다. 그 프로퍼티는 더 이상 없다.

옛 상한 검사 메서드 이름도 셋 남았는데 역시 전부 자바독이다. 검증기 자신의 :17 과 회귀 테스트의 :26·:29 다. 부를 수 있는 메서드로는 남아 있지 않다.

validateResolved 가 읽는 값과 던지는 조건

validateResolved(DataSource, boolean)DataSource 널 검사를 먼저 하고, 벤더 선택이 PostgreSQL 이 아니면 그대로 돌아온다. 그 자리 주석이 이유를 적는다 — 로컬 개발은 설계상 H2 로 돌고 프로덕션에서 그것을 막는 것은 PersistenceVendorProdSafetyValidator 이며, 여기서도 PostgreSQL 을 요구하면 모든 노트북을 거절하게 된다.

PostgreSQL 이 선택된 배포에서만 커넥션을 열어 versionPolicy.requireStable 에 메타데이터를 넘긴다. 열지 못하면 IllegalStateException 을 던지고, 메시지는 "A context that starts without this check reports healthy and fails on whoever sends the first request." 다.

validateResolved 의 프로덕션 호출자 하나와, 발행된 빈

validateResolved 를 부르는 프로덕션 코드는 PersistenceJpaRootAutoConfiguration:113 하나다. @Bean InitializingBean jpaResolvedDataSourceCheck 가 해석된 DataSourceEnvironment 를 받아, 벤더 프로퍼티를 읽고 그 값이 postgresql 인지를 두 번째 인자로 넘기는 람다를 돌려준다.

이 자동설정은 :57~:60@ConditionalOnProperty(prefix = "ca-skeleton.persistence-jpa", name = "enabled", havingValue = "true") 뒤에 있고 application.yml:364 의 출하 기본값은 false 다. JPA 마스터 스위치를 켠 배포에서만 이 검사가 돈다.

그 메서드는 검증기를 주입받지 않는다. :109~:110 에서 new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy()) 로 자기 인스턴스를 만든다. 이유도 :95~:99 에 적혀 있다. 빈을 내놓는 클래스가 @ConditionalOnBean(DataSource.class) 를 달고 있어 실제 애플리케이션에서 조용히 빠지므로, 그 빈에 의존하면 이 검사도 같은 이유로 사라진다는 것이다.

그 이유는 지금 성립하지 않는다. JpaPlatformRuntimeAutoConfiguration 의 자바독 :57~:58 은 이 클래스가 마스터 스위치를 이미 든 루트 아래에 있어 데이터소스 존재 여부가 파싱 시점에 이미 답해져 있다고 적고, 그 조건은 제거됐다.

이 타입을 빈으로 정의하는 자리는 하나다. JpaPlatformRuntimeAutoConfiguration:112@Bean 이고, 그 메서드가 JpaPlatformAutoConfiguration:49dataSourceProfileValidator() 에 위임한다. 뒤쪽 클래스에는 @Bean 이 하나도 없다 — 자바독 :19app-bootstrap 이 조립을 소유한다는 규칙 때문에 평범한 생성으로 둔다고 적는다.

그 빈을 파라미터나 필드로 받는 프로덕션 코드는 0 건이다. 검사는 도는데, 도는 것은 발행된 빈이 아니라 호출자가 직접 만든 객체다.

풀 크기 상한을 읽는 것은 또 다른 검증기다

자바독은 풀 상한을 여기서 보지 않는 결정을 굵게 적으면서 HikariPoolConstraintValidator 를 지목한다. 그 검증기가 실제로 읽는 다섯 키는 connection-timeout, validation-timeout, keepalive-time, max-lifetime, leak-detection-threshold 이고, RuntimeSafetyConfig:49@Bean 이 그것을 내놓는다.

requirePoolBounds 가 널에서 던지던 maximum-pool-size 는 그쪽이 아니라 RuntimeNumericBoundsValidator:22 가 하한 1 로 본다. minimum-idle:26 이다.

자바독은 이 클래스가 풀 타입 이름을 대지 않게 되어 HikariCP 가 app-bootstrap 의 프로덕션 클래스패스에서 빠진다고도 적는다. 선언은 그렇다 — app-bootstrap/build.gradle:140testImplementation 하나뿐이다. 그런데 runtimeClasspath 를 풀어 보면 com.zaxxer:HikariCP:7.0.2 가 올라와 있다. implementation project(':adapter:outbound:persistence-jpa') 를 타고 spring-boot-starter-data-jpa 가 끌어온 것이라, 자바독이 적은 그 이득은 지금 성립하지 않는다.

회귀 테스트가 공허한 통과까지 막는다

:::evidence key="a-validator-checking-the-wrong-datasource-run" alt="gradle 실행 출력 6줄. PIPESTATUS 로 읽은 gradle 종료 코드 0 이 먼저 찍히고, JpaResolvedDataSourceValidationTest 스위트의 JUnit 결과가 tests 3, failures 0, errors 0, skipped 0 으로 나온다. 이어서 세 테스트가 각각 통과로 표시되는데 도달할 수 없는 데이터베이스가 기동에서 실패하는지, 제품 검사가 벤더 선택을 따르는지, 프로덕션 코드가 병렬 네임스페이스를 바인딩하지 않는지 셋이다." caption="회귀 테스트 세 건 실행 결과 — 6줄 · exit 0" zoom="true" :::

theParallelNamespaceIsGone(:46)이 소스를 훑어 병렬 바인딩이 없다고 단언한다. 그 앞에 단언이 하나 더 있다. 훑은 목록이 비어 있지 않다는 것(:49~:51)이고, 그 단언의 문구가 이유를 적는다 — 아무 소스에도 닿지 못한 스캔은 모든 바인딩을 없다고 보고한다.

:53~:54 의 주석이 왜 주석을 걷어내고 검사하는지도 적는다. 네임스페이스를 퇴역시켰다고 기록한 문장은 결함의 반대이기 때문이다.

다만 이 테스트가 잡을 수 있는 범위는 두 모듈이다. productionSources()(:115~:119)가 app-bootstrapadapter/outbound/persistence-jpa 만 훑는다. 위에서 센 다른 리프 넷은 애초에 스캔 대상 밖이라, 그것들이 이 테스트를 깨뜨리지 않는 이유는 주석 제거만이 아니다.

나머지 둘은 부재가 아니라 지금 동작을 고정한다. anUnreachableDatabaseFailsAtStartup(:71)이 죽은 포트를 가리키는 HikariDataSource 에 대해 커넥션을 열지 못한다는 메시지를 요구하고, theProductCheckFollowsTheVendorSelector(:92)가 H2 를 벤더 선택이 거짓일 때는 통과시키고 참일 때는 거절한다고 단언한다.

돌리면 세 건이 통과한다.

확인하지 못한 것

결함이 있던 시점의 코드를 직접 열어 보지 않았다. 두 결함을 적어 둔 곳은 자바독과 위 설계 문서이고, 지금의 상태만 계수와 실행으로 확인했다.

주입처가 0 건이라는 판정의 근거는 이름 기반 정적 검색이다. 빈 이름으로 찾아 쓰거나 리플렉션으로 꺼내는 경로까지 배제하지는 못했다.

결함이 있던 시점의 코드를 직접 열어 보지 않았다. 두 결함을 적어 둔 곳은 자바독과 위 설계 문서이고, 지금의 상태만 계수와 실행으로 확인했다.