# 그 타입이 약속하는 것 PersistenceVendorSettings.java:1 package dev.caskeleton.adapter.outbound.persistence.config; PersistenceVendorSettings.java:2 PersistenceVendorSettings.java:3 import org.springframework.boot.context.properties.ConfigurationProperties; PersistenceVendorSettings.java:4 PersistenceVendorSettings.java:5 /** PersistenceVendorSettings.java:6 * Selects which RDBMS vendor composition this deployment runs. PersistenceVendorSettings.java:7 * PersistenceVendorSettings.java:8 *
The selector is a property rather than a profile name because the vendor is a property of the PersistenceVendorSettings.java:9 * datastore, not of the environment that happens to use it. A fork that runs PostgreSQL under a PersistenceVendorSettings.java:10 * profile named something other than {@code dev}/{@code prod}, or that wants H2 in a throwaway PersistenceVendorSettings.java:11 * demo, sets this key; it does not have to rename its profiles or edit a condition. PersistenceVendorSettings.java:12 * PersistenceVendorSettings.java:13 *
Binding to an enum is what makes an unknown vendor a startup failure. With a raw string the PersistenceVendorSettings.java:14 * two {@code @ConditionalOnProperty} vendor configurations would both stay off, and the first PersistenceVendorSettings.java:15 * missing SPI bean would surface as a {@code NoSuchBeanDefinitionException} naming {@code PersistenceVendorSettings.java:16 * OutboxClaimRepository} — a symptom several layers away from the misspelled value that caused it. PersistenceVendorSettings.java:17 */ PersistenceVendorSettings.java:18 @ConfigurationProperties(prefix = PersistenceVendorSettings.PREFIX) PersistenceVendorSettings.java:19 public record PersistenceVendorSettings(Vendor vendor) { PersistenceVendorSettings.java:20 PersistenceVendorSettings.java:21 public static final String PREFIX = "ca-skeleton.persistence"; PersistenceVendorSettings.java:22 public static final String VENDOR_PROPERTY = PREFIX + ".vendor"; PersistenceVendorSettings.java:23 PersistenceVendorSettings.java:24 /** The RDBMS vendors this repository composes a persistence adapter for. */ PersistenceVendorSettings.java:25 public enum Vendor { PersistenceVendorSettings.java:26 POSTGRESQL, PersistenceVendorSettings.java:27 H2 PersistenceVendorSettings.java:28 } PersistenceVendorSettings.java:29 PersistenceVendorSettings.java:30 public PersistenceVendorSettings { PersistenceVendorSettings.java:31 // Absent means PostgreSQL: the vendor every deployment before this selector existed ran, so an PersistenceVendorSettings.java:32 // upgrade that does not set the key keeps its datastore. PersistenceVendorSettings.java:33 vendor = vendor == null ? Vendor.POSTGRESQL : vendor; PersistenceVendorSettings.java:34 } PersistenceVendorSettings.java:35 } # 그 타입을 빈으로 등록하는 자리 EnableConfigurationProperties 가 이 타입을 담은 main 줄 : 0 개 ConfigurationPropertiesScan 애너테이션이 붙은 main 자리 (주석 제외) : app-bootstrap · CaSkeletonApplication.java:58 @ConfigurationPropertiesScan( sample-portfolio · SamplePortfolioApplication.java:46 @ConfigurationPropertiesScan(basePackages = "dev.caskeleton") 이 타입이 나오는 자리 전부 : adapter/outbound/persistence-jpa · main · PersistenceVendorSettings.java:18 @ConfigurationProperties(prefix = PersistenceVendorSettings.PREFIX) adapter/outbound/persistence-jpa · main · PersistenceVendorSettings.java:19 public record PersistenceVendorSettings(Vendor vendor) { adapter/outbound/persistence-jpa · main · PersistenceVendorSettings.java:30 public PersistenceVendorSettings { adapter/outbound/persistence-jpa · main · H2PersistenceConfig.java:4 import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings; adapter/outbound/persistence-jpa · main · H2PersistenceConfig.java:42 prefix = PersistenceVendorSettings.PREFIX, adapter/outbound/persistence-jpa · main · PostgreSqlPersistenceConfig.java:5 import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings; adapter/outbound/persistence-jpa · main · PostgreSqlPersistenceConfig.java:29 * PersistenceVendorSettings} back from {@code config} — so the composition root names both vendor adapter/outbound/persistence-jpa · main · PostgreSqlPersistenceConfig.java:33 * unconditional before {@link PersistenceVendorSettings} existed, and a deployment that never sets adapter/outbound/persistence-jpa · main · PostgreSqlPersistenceConfig.java:39 prefix = PersistenceVendorSettings.PREFIX, adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:5 import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings.Vendor; adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:34 .withPropertyValues(PersistenceVendorSettings.VENDOR_PROPERTY + "=" + value) adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:38 assertThat(context.getBean(PersistenceVendorSettings.class).vendor()) adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:47 assertThat(context.getBean(PersistenceVendorSettings.class).vendor()) adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:54 .withPropertyValues(PersistenceVendorSettings.VENDOR_PROPERTY + "=mysql") adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:59 .hasStackTraceContaining(PersistenceVendorSettings.VENDOR_PROPERTY); adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:69 assertThat(condition.prefix()).isEqualTo(PersistenceVendorSettings.PREFIX); adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:83 assertThat(condition.prefix()).isEqualTo(PersistenceVendorSettings.PREFIX); adapter/outbound/persistence-jpa · test · PersistenceVendorSelectionTest.java:92 @EnableConfigurationProperties(PersistenceVendorSettings.class) app-bootstrap · main · PersistenceJpaRootAutoConfiguration.java:4 import dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings; app-bootstrap · main · PersistenceJpaRootAutoConfiguration.java:86 * PersistenceVendorSettings}, because that type is not a registered bean here: the composition app-bootstrap · main · PersistenceJpaRootAutoConfiguration.java:112 environment.getProperty(PersistenceVendorSettings.VENDOR_PROPERTY, "postgresql").trim(); # 두 애플리케이션의 스캔 범위가 다르다 CaSkeletonApplication.java:58 @ConfigurationPropertiesScan( CaSkeletonApplication.java:59 basePackages = { CaSkeletonApplication.java:60 "dev.caskeleton.bootstrap.async", CaSkeletonApplication.java:61 "dev.caskeleton.bootstrap.autoconfigure.fileserver", CaSkeletonApplication.java:62 "dev.caskeleton.bootstrap.autoconfigure.httpclient", CaSkeletonApplication.java:63 "dev.caskeleton.bootstrap.concurrency", CaSkeletonApplication.java:64 "dev.caskeleton.bootstrap.idempotency", CaSkeletonApplication.java:65 "dev.caskeleton.bootstrap.outbox", CaSkeletonApplication.java:66 "dev.caskeleton.bootstrap.redis", CaSkeletonApplication.java:67 "dev.caskeleton.bootstrap.runtime", CaSkeletonApplication.java:68 "dev.caskeleton.bootstrap.security", CaSkeletonApplication.java:69 "dev.caskeleton.bootstrap.settings", CaSkeletonApplication.java:70 "dev.caskeleton.bootstrap.tracing", CaSkeletonApplication.java:71 "dev.caskeleton.adapter.inbound.grpc", CaSkeletonApplication.java:72 "dev.caskeleton.adapter.inbound.web", CaSkeletonApplication.java:73 "dev.caskeleton.adapter.inbound.websocket", CaSkeletonApplication.java:74 "dev.caskeleton.adapter.outbound.cache.redis", CaSkeletonApplication.java:75 "dev.caskeleton.adapter.outbound.fileserver", CaSkeletonApplication.java:76 "dev.caskeleton.adapter.outbound.objectstorage", CaSkeletonApplication.java:77 "dev.caskeleton.application", CaSkeletonApplication.java:78 "dev.caskeleton.domain", CaSkeletonApplication.java:79 "dev.caskeleton.shared" CaSkeletonApplication.java:80 }) SamplePortfolioApplication.java:44 classes = TestEnclosedConfigurationFilter.class) SamplePortfolioApplication.java:45 }) SamplePortfolioApplication.java:46 @ConfigurationPropertiesScan(basePackages = "dev.caskeleton") SamplePortfolioApplication.java:47 public class SamplePortfolioApplication { SamplePortfolioApplication.java:48 sample-portfolio/build.gradle:37 implementation project(':adapter:outbound:persistence-jpa') # 합성 루트가 그 자리에서 무엇을 하는가 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({ PersistenceJpaRootAutoConfiguration.java:62 JpaAdapterComponentsConfig.class, PersistenceJpaRootAutoConfiguration.java:63 PostgreSqlPersistenceConfig.class, PersistenceJpaRootAutoConfiguration.java:64 H2PersistenceConfig.class, PersistenceJpaRootAutoConfiguration.java:65 FileserverJpaPersistenceConfig.class, PersistenceJpaRootAutoConfiguration.java:66 NotificationJpaPersistenceFacade.class, PersistenceJpaRootAutoConfiguration.java:67 MigrationStartupConfig.class, PersistenceJpaRootAutoConfiguration.java:68 JpaPlatformRuntimeAutoConfiguration.class PersistenceJpaRootAutoConfiguration.java:69 }) PersistenceJpaRootAutoConfiguration.java:70 public class PersistenceJpaRootAutoConfiguration { PersistenceJpaRootAutoConfiguration.java:85 *
The vendor is read from the {@link Environment} rather than injected as {@code PersistenceJpaRootAutoConfiguration.java:86 * PersistenceVendorSettings}, because that type is not a registered bean here: the composition PersistenceJpaRootAutoConfiguration.java:87 * root's {@code @ConfigurationPropertiesScan} deliberately excludes the persistence package so an PersistenceJpaRootAutoConfiguration.java:88 * optional capability cannot bind — or reject — its detail settings in a deployment that never PersistenceJpaRootAutoConfiguration.java:89 * switched it on. The default matches {@code PostgreSqlPersistenceConfig}'s {@code matchIfMissing PersistenceJpaRootAutoConfiguration.java:90 * = true}: an unset selector is PostgreSQL, which is the vendor this configuration was PersistenceJpaRootAutoConfiguration.java:91 * unconditional about before the selector existed. PersistenceJpaRootAutoConfiguration.java:92 * PersistenceJpaRootAutoConfiguration.java:93 *
The validator is constructed here rather than injected. It is stateless, and the bean that PersistenceJpaRootAutoConfiguration.java:94 * used to supply it comes from {@code JpaPlatformRuntimeAutoConfiguration}, which carries 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 PersistenceJpaRootAutoConfiguration.java:100 * recorded separately; it is not this task's to fix quietly. PersistenceJpaRootAutoConfiguration.java:101 * PersistenceJpaRootAutoConfiguration.java:102 * @param dataSource the resolved datasource, injected so the database checked is the one in use PersistenceJpaRootAutoConfiguration.java:103 * @param environment the resolved environment, for the vendor selector PersistenceJpaRootAutoConfiguration.java:104 * @return the startup check PersistenceJpaRootAutoConfiguration.java:105 */ 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 } PersistenceJpaRootAutoConfiguration.java:115 } # 그 불린을 받는 쪽 JpaDataSourceProfileValidator.java:44 * vendor. JpaDataSourceProfileValidator.java:45 * JpaDataSourceProfileValidator.java:46 * @param dataSource the resolved datasource JpaDataSourceProfileValidator.java:47 * @param requirePostgreSql whether the vendor selector chose PostgreSQL JpaDataSourceProfileValidator.java:48 * @throws IllegalStateException naming what was unsupported or unreachable JpaDataSourceProfileValidator.java:49 */ 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( # 오타 난 벤더가 무엇을 무너뜨리는가 — 조립 경로 JpaAdapterComponentsConfig.java:55 * what is inside them. They need enabling at all because {@code @ConfigurationPropertiesScan} JpaAdapterComponentsConfig.java:56 * excludes this tree as deliberately as {@code @ComponentScan} does. JpaAdapterComponentsConfig.java:57 */ JpaAdapterComponentsConfig.java:58 @Configuration(proxyBeanMethods = false) JpaAdapterComponentsConfig.java:59 @ComponentScan( JpaAdapterComponentsConfig.java:60 basePackages = { JpaAdapterComponentsConfig.java:61 "dev.caskeleton.adapter.outbound.persistence.audit", JpaAdapterComponentsConfig.java:62 "dev.caskeleton.adapter.outbound.persistence.failure", JpaAdapterComponentsConfig.java:63 "dev.caskeleton.adapter.outbound.persistence.idempotency", JpaAdapterComponentsConfig.java:64 "dev.caskeleton.adapter.outbound.persistence.lock", JpaAdapterComponentsConfig.java:65 "dev.caskeleton.adapter.outbound.persistence.outbox", JpaAdapterComponentsConfig.java:66 "dev.caskeleton.adapter.outbound.persistence.transaction" JpaAdapterComponentsConfig.java:67 }) JpaAdapterComponentsConfig.java:68 public class JpaAdapterComponentsConfig {} OutboxStoreAdapter.java:20 * {@code @Transactional} of their own; {@link #claimBatch} delegates the vendor claim to {@link OutboxStoreAdapter.java:21 * OutboxClaimRepository}. See README "outbox" for the append/claim/no-@Transactional contracts. OutboxStoreAdapter.java:22 */ OutboxStoreAdapter.java:23 @Repository OutboxStoreAdapter.java:24 public class OutboxStoreAdapter implements OutboxAppendPort, OutboxStorePort { OutboxStoreAdapter.java:25 OutboxStoreAdapter.java:26 private final OutboxEventJpaRepository repository; OutboxStoreAdapter.java:27 private final OutboxClaimRepository claimRepository; OutboxStoreAdapter.java:28 OutboxStoreAdapter.java:29 public OutboxStoreAdapter( OutboxStoreAdapter.java:30 OutboxEventJpaRepository repository, OutboxClaimRepository claimRepository) { OutboxStoreAdapter.java:31 this.repository = repository; OutboxStoreAdapter.java:32 this.claimRepository = claimRepository; OutboxStoreAdapter.java:33 } # 그 값을 실제로 넣고 돌린 결과 프로브 : /tmp/probe-vendor/VendorProbe.java · ApplicationContextRunner 세 조립 VendorProbe.java:36 /** OutboxStoreAdapter 는 JpaAdapterComponentsConfig 가 스캔하는 패키지의 @Repository 다. */ VendorProbe.java:37 @Configuration(proxyBeanMethods = false) VendorProbe.java:38 static class OutboxConsumer { VendorProbe.java:39 @Bean OutboxEventJpaRepository events() { return stub(OutboxEventJpaRepository.class); } VendorProbe.java:40 @Bean OutboxStoreAdapter adapter(OutboxEventJpaRepository e, OutboxClaimRepository c) { VendorProbe.java:41 return new OutboxStoreAdapter(e, c); VendorProbe.java:42 } VendorProbe.java:43 } VendorProbe.java:44 VendorProbe.java:45 @Configuration(proxyBeanMethods = false) VendorProbe.java:46 @EnableConfigurationProperties(PersistenceVendorSettings.class) VendorProbe.java:47 static class BoundSettings {} VendorProbe.java:74 public static void main(String[] args) { VendorProbe.java:75 run("두 벤더 설정만 ", "mysql", PostgreSqlPersistenceConfig.class, H2PersistenceConfig.class); VendorProbe.java:76 for (String v : new String[] {"mysql", "postgresq1"}) { VendorProbe.java:77 run("SPI 소비자를 더해", v, VendorProbe.java:78 OutboxConsumer.class, PostgreSqlPersistenceConfig.class, H2PersistenceConfig.class); VendorProbe.java:79 } VendorProbe.java:80 run("그 타입을 켜고 ", "mysql", BoundSettings.class); VendorProbe.java:81 } 두 벤더 설정만 vendor=mysql 기동 실패 : false OutboxClaimRepository 빈 : 0 SPI 소비자를 더해 vendor=mysql 기동 실패 : true ca-skeleton.persistence.vendor 를 지목 : false OutboxClaimRepository 지목 : true 맨 앞 예외 : UnsatisfiedDependencyException: Error creating bean with name 'adapter' defined in VendorProbe$OutboxConsumer: Unsatisfied dependency expressed throug SPI 소비자를 더해 vendor=postgresq1 기동 실패 : true ca-skeleton.persistence.vendor 를 지목 : false OutboxClaimRepository 지목 : true 맨 앞 예외 : UnsatisfiedDependencyException: Error creating bean with name 'adapter' defined in VendorProbe$OutboxConsumer: Unsatisfied dependency expressed throug 그 타입을 켜고 vendor=mysql 기동 실패 : true ca-skeleton.persistence.vendor 를 지목 : true OutboxClaimRepository 지목 : false 맨 앞 예외 : ConfigurationPropertiesBindException: Error creating bean with name 'ca-skeleton.persistence-dev.caskeleton.adapter.outbound.persistence.config.Persis # 시험은 그 타입을 켜고 돌린다 PersistenceVendorSelectionTest.java:28 new ApplicationContextRunner().withUserConfiguration(VendorSettings.class); PersistenceVendorSelectionTest.java:29 PersistenceVendorSelectionTest.java:30 @ParameterizedTest PersistenceVendorSelectionTest.java:31 @ValueSource(strings = {"postgresql", "POSTGRESQL", "h2", "H2"}) PersistenceVendorSelectionTest.java:32 void bindsTheSupportedVendorsCaseInsensitively(String value) { PersistenceVendorSelectionTest.java:33 runner PersistenceVendorSelectionTest.java:34 .withPropertyValues(PersistenceVendorSettings.VENDOR_PROPERTY + "=" + value) PersistenceVendorSelectionTest.java:35 .run( PersistenceVendorSelectionTest.java:36 context -> { PersistenceVendorSelectionTest.java:37 assertThat(context).hasNotFailed(); PersistenceVendorSelectionTest.java:38 assertThat(context.getBean(PersistenceVendorSettings.class).vendor()) PersistenceVendorSelectionTest.java:39 .isEqualTo(Vendor.valueOf(value.toUpperCase(java.util.Locale.ROOT))); PersistenceVendorSelectionTest.java:40 }); PersistenceVendorSelectionTest.java:41 } PersistenceVendorSelectionTest.java:42 PersistenceVendorSelectionTest.java:43 @Test PersistenceVendorSelectionTest.java:44 void defaultsToPostgreSqlWhenTheSelectorIsAbsent() { PersistenceVendorSelectionTest.java:45 runner.run( PersistenceVendorSelectionTest.java:46 context -> PersistenceVendorSelectionTest.java:47 assertThat(context.getBean(PersistenceVendorSettings.class).vendor()) PersistenceVendorSelectionTest.java:48 .isEqualTo(Vendor.POSTGRESQL)); PersistenceVendorSelectionTest.java:49 } PersistenceVendorSelectionTest.java:50 PersistenceVendorSelectionTest.java:51 @Test PersistenceVendorSelectionTest.java:52 void rejectsAnUnknownVendorAtStartupRatherThanComposingNothing() { PersistenceVendorSelectionTest.java:53 runner PersistenceVendorSelectionTest.java:54 .withPropertyValues(PersistenceVendorSettings.VENDOR_PROPERTY + "=mysql") PersistenceVendorSelectionTest.java:55 .run( PersistenceVendorSelectionTest.java:56 context -> { PersistenceVendorSelectionTest.java:57 assertThat(context).hasFailed(); PersistenceVendorSelectionTest.java:58 assertThat(context.getStartupFailure()) PersistenceVendorSelectionTest.java:59 .hasStackTraceContaining(PersistenceVendorSettings.VENDOR_PROPERTY); PersistenceVendorSelectionTest.java:60 }); PersistenceVendorSelectionTest.java:61 } PersistenceVendorSelectionTest.java:62 PersistenceVendorSelectionTest.java:88 .isFalse(); PersistenceVendorSelectionTest.java:89 } PersistenceVendorSelectionTest.java:90 PersistenceVendorSelectionTest.java:91 @Configuration(proxyBeanMethods = false) PersistenceVendorSelectionTest.java:92 @EnableConfigurationProperties(PersistenceVendorSettings.class) PersistenceVendorSelectionTest.java:93 static class VendorSettings {} PersistenceVendorSelectionTest.java:94 }