Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/a-validator-checking-the-wrong-datasource.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.

Follows the import procedure in README.md.

  source/     the originating repository verbatim — 78 documents, 28 SVGs,
              8 manifests, plus .source-revision recording the commit
  final/      the SSOT
    document.md   729 lines written from the 29 experiment documents, not
                  concatenated: what was predicted, what was measured, and
                  where the measurement itself was wrong
    evidence/raw    125 outputs, flattened to <experiment>__<file> because
                    the originals collided (01-baseline.txt appeared three
                    times) and the audit only globs the top level
    evidence/meta   one per raw file; command and exitCode are null and the
                    README says why rather than inventing them
    evidence/browser  22 captures
    assets/       three diagrams through techviz
    .techviz/     their VizSpecs

A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.

Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.

verify-pipeline.py passes. audit-records.py reports no issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 22:51:59 +09:00

159 lines
14 KiB
Plaintext

# 검증기 자바독이 적은 두 결함과 그 상쇄
JpaDataSourceProfileValidator.java:11 * <p>It used to validate a different one. The bounds came from {@code JpaDataSourceSettings}, bound
JpaDataSourceProfileValidator.java:12 * to {@code app.jpa-platform.datasource.*}, while the pool that serves requests is built from
JpaDataSourceProfileValidator.java:13 * {@code spring.datasource.hikari.*} — one pool with two descriptions, and a validator can pass
JpaDataSourceProfileValidator.java:14 * against the description that is not in use.
JpaDataSourceProfileValidator.java:15 *
JpaDataSourceProfileValidator.java:16 * <p>Worse, the parallel namespace appeared in no shipped YAML and no row of the env-key registry,
JpaDataSourceProfileValidator.java:17 * so both of its fields were always null and {@code requirePoolBounds} would have thrown on every
JpaDataSourceProfileValidator.java:18 * deployment. Nothing called it, so nothing failed. Two defects that cancelled each other out, and
JpaDataSourceProfileValidator.java:19 * the reason the application started was the second one hiding the first.
# 그 병렬 네임스페이스가 src 안에 남아 있는 자리
main · NotificationSmtpProviderConfig.java:48 * app.jpa-platform.datasource.*} validated a pool that {@code spring.datasource.hikari.*} had
main · NotificationSmtpSettings.java:14 * paid for — {@code app.jpa-platform.datasource.*} validated a pool that {@code
main · SmtpProviderRuntimeAssembler.java:40 * already paid for elsewhere — {@code app.jpa-platform.datasource.*} validated a pool that {@code
main · JpaDataSourceProfileValidator.java:12 * to {@code app.jpa-platform.datasource.*}, while the pool that serves requests is built from
test · JpaResolvedDataSourceValidationTest.java:22 * <p>{@code JpaDataSourceSettings} bound {@code app.jpa-platform.datasource.*} while the pool that
test · JpaResolvedDataSourceValidationTest.java:57 .filter(path -> withoutJavaComments(read(path)).contains("app.jpa-platform.datasource"))
main · KafkaMessagingAutoConfiguration.java:119 * repository has already paid for in {@code app.jpa-platform.datasource.*}, and the
src 밖에서 이 문자열을 쓰는 파일과 줄 수 :
docs/study/postgresql-jpa-walkthrough.ko.md:1
docs/superpowers/plans/2026-08-15-wave2-module-on-path.md:4
docs/superpowers/plans/evidence/2026-08-15-wave2-decisions.md:3
docs/superpowers/specs/2026-08-15-five-adapter-runtime-remediation-review-design.md:1
infra/jpa/postgres/README.md:1
그중 운영자용 표에 살아 있는 줄 :
infra/jpa/postgres/README.md:18:| `max_connections` | The number `app.jpa-platform.datasource.maximum-pool-size` must be sized against — across every instance, and allowing for
설정 파일(yml·yaml·properties)에서 : 0 건
그중 주석이 아닌 줄 : main 0 건 · test 1 건 (test 쪽은 회귀 테스트의 검색 문자열)
그것을 바인딩하던 JpaDataSourceSettings 클래스 : 0 건
# 옛 상한 검사 메서드 이름이 남아 있는 자리
main · JpaDataSourceProfileValidator.java:17 * so both of its fields were always null and {@code requirePoolBounds} would have thrown on every
test · JpaResolvedDataSourceValidationTest.java:26 * of its fields were always null — and {@code requirePoolBounds} throws on a null. Had anything
test · JpaResolvedDataSourceValidationTest.java:29 * <p>Nothing did. {@code validateStable} and {@code requirePoolBounds} were reachable only from
# 지금 검증기가 읽는 것과 던지는 것
JpaDataSourceProfileValidator.java:50 public void validateResolved(DataSource dataSource, boolean requirePostgreSql) {
JpaDataSourceProfileValidator.java:51 Objects.requireNonNull(dataSource, "dataSource");
JpaDataSourceProfileValidator.java:52 if (!requirePostgreSql) {
JpaDataSourceProfileValidator.java:53 // Local development runs H2 by design, and PersistenceVendorProdSafetyValidator is what keeps
JpaDataSourceProfileValidator.java:54 // that out of production. Demanding PostgreSQL here as well would refuse every laptop.
JpaDataSourceProfileValidator.java:55 return;
JpaDataSourceProfileValidator.java:56 }
JpaDataSourceProfileValidator.java:57 try (Connection connection = dataSource.getConnection()) {
JpaDataSourceProfileValidator.java:58 versionPolicy.requireStable(connection.getMetaData());
JpaDataSourceProfileValidator.java:59 } catch (SQLException unreachable) {
JpaDataSourceProfileValidator.java:60 throw new IllegalStateException(
JpaDataSourceProfileValidator.java:61 "could not open a connection to validate the datasource: "
JpaDataSourceProfileValidator.java:62 + unreachable.getMessage()
JpaDataSourceProfileValidator.java:63 + ". A context that starts without this check reports healthy and fails on whoever "
JpaDataSourceProfileValidator.java:64 + "sends the first request.",
JpaDataSourceProfileValidator.java:65 unreachable);
JpaDataSourceProfileValidator.java:66 }
# validateResolved 를 부르는 자리 전부
main · PersistenceJpaRootAutoConfiguration.java:113 return () -> validator.validateResolved(dataSource, "postgresql".equalsIgnoreCase(vendor));
test · JpaResolvedDataSourceValidationTest.java:82 .validateResolved(pool, true))
test · JpaResolvedDataSourceValidationTest.java:101 assertThatCode(() -> validator.validateResolved(h2, false))
test · JpaResolvedDataSourceValidationTest.java:104 assertThatThrownBy(() -> validator.validateResolved(h2, true))
# 그 호출자가 붙는 조건과 출하 기본값
PersistenceJpaRootAutoConfiguration.java:56 @AutoConfiguration
PersistenceJpaRootAutoConfiguration.java:57 @ConditionalOnProperty(
PersistenceJpaRootAutoConfiguration.java:58 prefix = "ca-skeleton.persistence-jpa",
PersistenceJpaRootAutoConfiguration.java:59 name = "enabled",
PersistenceJpaRootAutoConfiguration.java:60 havingValue = "true")
PersistenceJpaRootAutoConfiguration.java:61 @Import({
application.yml:364: enabled: ${APP_PERSISTENCE_JPA_ENABLED:false}
# 직접 생성의 이유로 적힌 조건과, 그 조건의 현재 상태
PersistenceJpaRootAutoConfiguration.java:95 * {@code @ConditionalOnBean(DataSource.class)} on a plain {@code @Configuration} imported by this
PersistenceJpaRootAutoConfiguration.java:96 * root — a condition evaluated during configuration-class parsing, before the datasource bean
PersistenceJpaRootAutoConfiguration.java:97 * definition is registered. That class therefore drops out silently in the real application,
PersistenceJpaRootAutoConfiguration.java:98 * taking the whole JPA add-on layer with it. Depending on a bean from it would make this check
PersistenceJpaRootAutoConfiguration.java:99 * disappear for the same reason the thing it checks disappeared. The condition-ordering defect is
JpaPlatformRuntimeAutoConfiguration.java:57 * root, which already carries the master switch, so "is there a datasource" has been answered yes
JpaPlatformRuntimeAutoConfiguration.java:58 * by the time it is parsed. A bean here that needs one takes it as a parameter, and a missing
JpaPlatformRuntimeAutoConfiguration.java:59 * datasource with the switch on is then a loud failure — which is the outcome that was wanted,
JpaPlatformRuntimeAutoConfiguration.java:60 * rather than the layer vanishing.
JpaPlatformRuntimeAutoConfiguration.java:61 */
JpaPlatformRuntimeAutoConfiguration.java:62 @org.springframework.context.annotation.Configuration(proxyBeanMethods = false)
JpaPlatformRuntimeAutoConfiguration.java:63 @ConditionalOnClass(DataSource.class)
JpaPlatformRuntimeAutoConfiguration.java:64 @ConditionalOnProperty(prefix = "app.jpa-platform", name = "addons-enabled", matchIfMissing = true)
# 그 호출자가 검증기를 어디서 얻는가
PersistenceJpaRootAutoConfiguration.java:106 @Bean
PersistenceJpaRootAutoConfiguration.java:107 public InitializingBean jpaResolvedDataSourceCheck(
PersistenceJpaRootAutoConfiguration.java:108 DataSource dataSource, Environment environment) {
PersistenceJpaRootAutoConfiguration.java:109 JpaDataSourceProfileValidator validator =
PersistenceJpaRootAutoConfiguration.java:110 new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy());
PersistenceJpaRootAutoConfiguration.java:111 String vendor =
PersistenceJpaRootAutoConfiguration.java:112 environment.getProperty(PersistenceVendorSettings.VENDOR_PROPERTY, "postgresql").trim();
PersistenceJpaRootAutoConfiguration.java:113 return () -> validator.validateResolved(dataSource, "postgresql".equalsIgnoreCase(vendor));
PersistenceJpaRootAutoConfiguration.java:114 }
# 이 타입을 빈으로 정의하는 자리와, 그 빈을 주입받는 코드
JpaPlatformAutoConfiguration 의 @Bean 개수 : 0
JpaPlatformAutoConfiguration.java:19 * <p>Plain construction rather than Spring auto-configuration. This repository's rule is that
JpaPlatformAutoConfiguration.java:20 * {@code app-bootstrap} owns composition, and an adapter leaf that auto-configured itself would
JpaPlatformAutoConfiguration.java:48 /** The datasource product, version, and pool validator. */
JpaPlatformAutoConfiguration.java:49 public JpaDataSourceProfileValidator dataSourceProfileValidator() {
JpaPlatformAutoConfiguration.java:50 return new JpaDataSourceProfileValidator(new PostgreSqlVersionPolicy());
JpaPlatformRuntimeAutoConfiguration.java:111 /** The datasource product, version and pool validator. */
JpaPlatformRuntimeAutoConfiguration.java:112 @Bean
JpaPlatformRuntimeAutoConfiguration.java:113 @ConditionalOnMissingBean
JpaPlatformRuntimeAutoConfiguration.java:114 public JpaDataSourceProfileValidator jpaDataSourceProfileValidator(
JpaPlatformRuntimeAutoConfiguration.java:115 JpaPlatformAutoConfiguration composition) {
JpaPlatformRuntimeAutoConfiguration.java:116 return composition.dataSourceProfileValidator();
JpaPlatformRuntimeAutoConfiguration.java:117 }
파라미터나 필드로 받는 프로덕션 코드 : 0 건
# HikariPoolConstraintValidator 가 읽는 키와 그 빈, 그리고 풀 크기를 보는 다른 검증기
HikariPoolConstraintValidator.java:18 static final String CONNECTION_TIMEOUT_KEY = "spring.datasource.hikari.connection-timeout";
HikariPoolConstraintValidator.java:19 static final String VALIDATION_TIMEOUT_KEY = "spring.datasource.hikari.validation-timeout";
HikariPoolConstraintValidator.java:20 static final String KEEPALIVE_TIME_KEY = "spring.datasource.hikari.keepalive-time";
HikariPoolConstraintValidator.java:21 static final String MAX_LIFETIME_KEY = "spring.datasource.hikari.max-lifetime";
HikariPoolConstraintValidator.java:22 static final String LEAK_DETECTION_KEY = "spring.datasource.hikari.leak-detection-threshold";
RuntimeSafetyConfig.java:49 @Bean
RuntimeSafetyConfig.java:50 HikariPoolConstraintValidator hikariPoolConstraintValidator(Environment environment) {
RuntimeSafetyConfig.java:51 return new HikariPoolConstraintValidator(environment);
maximum-pool-size 와 minimum-idle 을 실제로 검사하는 곳 :
main · RuntimeNumericBoundsValidator.java:22 "spring.datasource.hikari.maximum-pool-size", "APP_DATASOURCE_POOL_MAX_SIZE", 1),
main · RuntimeNumericBoundsValidator.java:26 new Bound("spring.datasource.hikari.minimum-idle", "APP_DATASOURCE_POOL_MIN_IDLE", 0),
app-bootstrap 이 HikariCP 를 선언하는 구성 :
build.gradle:140: testImplementation 'com.zaxxer:HikariCP'
그런데 runtimeClasspath 를 풀어 보면 :
| | | \--- com.zaxxer:HikariCP:7.0.2
+--- com.zaxxer:HikariCP:{strictly 7.0.2} -> 7.0.2 (c)
# 그 부재를 고정하는 회귀 테스트
test · JpaResolvedDataSourceValidationTest.java:45 @DisplayName("no production code binds the parallel namespace")
test · JpaResolvedDataSourceValidationTest.java:46 void theParallelNamespaceIsGone() {
test · JpaResolvedDataSourceValidationTest.java:70 @DisplayName("an unreachable database fails at startup, not at the first query")
test · JpaResolvedDataSourceValidationTest.java:71 void anUnreachableDatabaseFailsAtStartup() {
test · JpaResolvedDataSourceValidationTest.java:91 @DisplayName("the product check applies only where PostgreSQL is the selected vendor")
test · JpaResolvedDataSourceValidationTest.java:92 void theProductCheckFollowsTheVendorSelector() {
그 테스트가 훑는 범위 :
JpaResolvedDataSourceValidationTest.java:115 private static Stream<Path> productionSources() {
JpaResolvedDataSourceValidationTest.java:116 Path root = repositoryRoot().resolve("src");
JpaResolvedDataSourceValidationTest.java:117 return Stream.of("app-bootstrap", "adapter/outbound/persistence-jpa")
JpaResolvedDataSourceValidationTest.java:118 .map(root::resolve)
JpaResolvedDataSourceValidationTest.java:119 .filter(Files::isDirectory)
공허한 통과를 막는 단언과 주석 제거 :
JpaResolvedDataSourceValidationTest.java:47 List<Path> sources = productionSources().toList();
JpaResolvedDataSourceValidationTest.java:48
JpaResolvedDataSourceValidationTest.java:49 assertThat(sources)
JpaResolvedDataSourceValidationTest.java:50 .as("a scan that reached no source would report every binding as absent")
JpaResolvedDataSourceValidationTest.java:51 .isNotEmpty();
JpaResolvedDataSourceValidationTest.java:52
JpaResolvedDataSourceValidationTest.java:53 // Comments are stripped: a sentence recording that the namespace was retired is the opposite of
JpaResolvedDataSourceValidationTest.java:54 // the defect, which was a binder reading a namespace nothing supplied.
JpaResolvedDataSourceValidationTest.java:55 List<String> offenders =
JpaResolvedDataSourceValidationTest.java:56 sources.stream()
JpaResolvedDataSourceValidationTest.java:57 .filter(path -> withoutJavaComments(read(path)).contains("app.jpa-platform.datasource"))
JpaResolvedDataSourceValidationTest.java:58 .map(path -> path.getFileName().toString())
JpaResolvedDataSourceValidationTest.java:59 .sorted()
JpaResolvedDataSourceValidationTest.java:60 .toList();