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>
159 lines
14 KiB
Plaintext
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();
|