Spring Boot Docs Overview에 작성되어 있는 6가지 기능은 다음과 같다.
- Create stand-alone Spring applications
- Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
- Provide opinionated 'starter' dependencies to simplify your build configuration
- Automatically configure Spring and 3rd party libraries whenever possible
- Provide production-ready features such as metrics, health checks, and externalized configuration
- Absolutely no code generation and no requirement for XML configuration
나는 위의 선배님들이 고생하여 만들어 놓으신 6가지 기능들 덕분에 보다 편리하게 현재 개발을 하고 있다.
그저 사용하고 싶은 기술의 의존성을 추가하고 yml
에 환경설정 몇가지만 하고 그저 기동만 시키면 알아서 실행되지 않는가? 사실 프로젝트 상황에 따라 추가적인 설정을 추가하여 사용하긴한다.
이렇게 yml
에 환경설정만 추가하여 Bean
들이 설정되는 방식이 바로 AutoConfiguration
이다!!!
지금부터는 Spring Data JPA
의존성 추가시 어떻게 DataSource
가 생성되는지를 알아보도록 하겠다.
우선 시작은 @SpringBootApplication
으로 부터 시작된다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
@SpringBootApplication
에는 @EnableAutoConfiguration
를 포함하고 있다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
@EnableAutoConfiguration
에서는 AutoConfigurationImportSelector
를 @Import
하고 있으며 해당 클래스가 AutoConfiguration
의 시작 지점이다.
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
...
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
...
AutoConfigurationImportSelector.getAutoConfigurationEntry
실행 결과에 현재 어플리케이션에 설정될 AutoConfiguration
클래스들의 목록이 생성되며 그 중에는 DataSourceAutoConfiguration
도 포함되어있는 것을 확인 할 수 있다.
우선 AutoConfiguration
클래스의 내용을 보기 이전에 @Conditional
에 대한 이해가 필요하다. 이는 Bean
을 조건에 해당 하는 경우에만 등록 할 수 있도록 하는 방식이다.
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceCheckpointRestoreConfiguration.class })
public class DataSourceAutoConfiguration {
...
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration {
@Bean
@ConditionalOnMissingBean(JdbcConnectionDetails.class)
PropertiesJdbcConnectionDetails jdbcConnectionDetails(DataSourceProperties properties) {
return new PropertiesJdbcConnectionDetails(properties);
}
}
...
결과적으로는 위의 메소드를 통해서 DataSource
가 생성된다. 위의 내용을 이해하자면 다음과 같다.
@AutoConfiguration(before = SqlInitializationAutoConfiguration.class)
: 해당 AutoConfiguration
를 수행하기 이전에 SqlInitializationAutoConfiguration
를 수행한다.@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
: DataSource.class
, EmbeddedDatabaseType.class
두 클래스가 모두 classpath에 존재해야 한다.@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
: io.r2dbc.spi.ConnectionFactory
타입의 Bean
이 없어야 한다.@Conditional(PooledDataSourceCondition.class)
: PooledDataSourceCondition
의 조건을 만족해야 한다.@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
: DataSource
, XADataSource
타입의 Bean
이 없어야 한다.위의 조건들이 만족하게 되면 PropertiesJdbcConnectionDetails
Bean
이 생성되게 되며 이후 @Import
에 의해서 선언된 클래스들의 Bean
등록이 이루어진다.
abstract class DataSourceConfiguration {
...
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnClass({HikariDataSource.class})
@ConditionalOnMissingBean({DataSource.class})
@ConditionalOnProperty(
name = {"spring.datasource.type"},
havingValue = "com.zaxxer.hikari.HikariDataSource",
matchIfMissing = true
)
static class Hikari {
Hikari() {
}
@Bean
static HikariJdbcConnectionDetailsBeanPostProcessor jdbcConnectionDetailsHikariBeanPostProcessor(ObjectProvider<JdbcConnectionDetails> connectionDetailsProvider) {
return new HikariJdbcConnectionDetailsBeanPostProcessor(connectionDetailsProvider);
}
@Bean
@ConfigurationProperties(
prefix = "spring.datasource.hikari"
)
HikariDataSource dataSource(DataSourceProperties properties, JdbcConnectionDetails connectionDetails) {
HikariDataSource dataSource = (HikariDataSource)DataSourceConfiguration.createDataSource(connectionDetails, HikariDataSource.class, properties.getClassLoader());
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
}
...
@ConditionalOnClass({HikariDataSource.class})
: HikariDataSource.class
이 classpath에 존재해야 한다.@ConditionalOnMissingBean({DataSource.class})
: DataSource
타입의 Bean
이 없어야 한다. @ConditionalOnProperty( name = {"spring.datasource.type"}, havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true )
: spring.datasource.type
값이 com.zaxxer.hikari.HikariDataSource
으로 일치하거나 값이 설정이 안되어야한다.이렇게 HikariDataSource
가 Bean
으로 등록되는 것이다!!!
그렇다면 이제는 우리가 만든 설정들에 AutoConfiguration
를 적용해보도록 하겠다.
설정은 간단하게 JPA
를 활용하여 두개의 DataSource
를 생성하는 설정을 예제로 사용하겠다. 해당 설정은 다음 글를 통해서 다루었다.
@AutoConfiguration
@EnableJpaRepositories(
basePackages = {"io.velog.youmakemesmile.autoconfigure.primary"},
entityManagerFactoryRef = "primaryEntityManagerFactory",
transactionManagerRef = "primaryTransactionManager"
)
@EnableTransactionManagement
public class PrimaryAutoConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource1")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource1.hikari")
public DataSource primaryDataSource(@Qualifier("primaryDataSourceProperties") DataSourceProperties dataSourceProperties) {
return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@Primary
@ConfigurationProperties(prefix = "spring.datasource1.jpa.hibernate")
public HibernateProperties primaryHibernateProperties() {
return new HibernateProperties();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource1.jpa")
@Primary
public JpaProperties primaryJpaProperties() {
return new JpaProperties();
}
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean primaryEntityManagerFactory(@Qualifier("primaryDataSource") DataSource dataSource,
@Qualifier("primaryJpaProperties") JpaProperties jpaProperties,
@Qualifier("primaryHibernateProperties") HibernateProperties hibernateProperties) {
EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(jpaProperties, hibernateProperties);
return builder.dataSource(dataSource).packages("io.velog.youmakemesmile.autoconfigure.primary").build();
}
private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
JpaVendorAdapter jpaVendorAdapter = createJpaVendorAdapter(jpaProperties);
Map<String, Object> jpaProperties1 = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings().ddlAuto(hibernateProperties::getDdlAuto).hibernatePropertiesCustomizers(new ArrayList<>()));
return new EntityManagerFactoryBuilder(jpaVendorAdapter, jpaProperties1, null);
}
private JpaVendorAdapter createJpaVendorAdapter(JpaProperties properties) {
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setShowSql(properties.isShowSql());
if (properties.getDatabase() != null) {
adapter.setDatabase(properties.getDatabase());
}
if (properties.getDatabasePlatform() != null) {
adapter.setDatabasePlatform(properties.getDatabasePlatform());
}
adapter.setGenerateDdl(properties.isGenerateDdl());
return adapter;
}
@Bean
public JpaTransactionManager primaryTransactionManager(@Qualifier("primaryEntityManagerFactory") EntityManagerFactory primaryEntityManagerFactory) {
return new JpaTransactionManager(primaryEntityManagerFactory);
}
@Bean
@Primary
public ChainedTransactionManager transactionManager(@Qualifier("primaryTransactionManager") JpaTransactionManager primaryTransactionManager, @Qualifier("secondTransactionManager") JpaTransactionManager secondTransactionManager) {
return new ChainedTransactionManager(secondTransactionManager, primaryTransactionManager);
}
}
@AutoConfiguration
@EnableJpaRepositories(
basePackages = {"io.velog.youmakemesmile.autoconfigure.second"},
entityManagerFactoryRef = "secondEntityManagerFactory",
transactionManagerRef = "secondTransactionManager"
)
//@EntityScan(basePackages = {"io.velog.youmakemesmile.autoconfigure.second"})
public class SecondAutoConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource2")
public DataSourceProperties secondDataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource2.hikari")
public DataSource secondDataSource(@Qualifier("secondDataSourceProperties") DataSourceProperties dataSourceProperties) {
return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource2.jpa.hibernate")
public HibernateProperties secondHibernateProperties() {
return new HibernateProperties();
}
@Bean
@ConfigurationProperties(prefix = "spring.datasource2.jpa")
@Primary
public JpaProperties secondJpaProperties() {
return new JpaProperties();
}
@Primary
@Bean
public LocalContainerEntityManagerFactoryBean secondEntityManagerFactory(@Qualifier("secondDataSource") DataSource dataSource,
@Qualifier("secondJpaProperties") JpaProperties jpaProperties,
@Qualifier("secondHibernateProperties") HibernateProperties hibernateProperties) {
EntityManagerFactoryBuilder builder = createEntityManagerFactoryBuilder(jpaProperties, hibernateProperties);
return builder.dataSource(dataSource).persistenceUnit("second").packages("io.velog.youmakemesmile.autoconfigure.second").build();
}
private EntityManagerFactoryBuilder createEntityManagerFactoryBuilder(JpaProperties jpaProperties, HibernateProperties hibernateProperties) {
JpaVendorAdapter jpaVendorAdapter = createJpaVendorAdapter(jpaProperties);
Map<String, Object> jpaProperties1 = hibernateProperties.determineHibernateProperties(jpaProperties.getProperties(), new HibernateSettings().ddlAuto(hibernateProperties::getDdlAuto).hibernatePropertiesCustomizers(new ArrayList<>()));
return new EntityManagerFactoryBuilder(jpaVendorAdapter, jpaProperties1, null);
}
private JpaVendorAdapter createJpaVendorAdapter(JpaProperties properties) {
// ... map JPA properties as needed
HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
adapter.setShowSql(properties.isShowSql());
if (properties.getDatabase() != null) {
adapter.setDatabase(properties.getDatabase());
}
if (properties.getDatabasePlatform() != null) {
adapter.setDatabasePlatform(properties.getDatabasePlatform());
}
adapter.setGenerateDdl(properties.isGenerateDdl());
return adapter;
}
@Bean
public JpaTransactionManager secondTransactionManager(@Qualifier("secondEntityManagerFactory") EntityManagerFactory secondEntityManagerFactory) {
return new JpaTransactionManager(secondEntityManagerFactory);
}
}
io.velog.youmakemesmile.autoconfigure.primary.PrimaryAutoConfiguration
io.velog.youmakemesmile.autoconfigure.second.SecondAutoConfiguration
위와 같이 @AutoConfiguration
클래스와 org.springframework.boot.autoconfigure.AutoConfiguration.imports
파일에 해당 클래스 패키지 경로를 명시하면 된다.
이후 Spring Boot
프로젝트에서 해당 AutoConfigure
프로젝트를 의존성으로 추가하게 되면 정상적으로 JPA
설정이 적용되는 것을 확인 할 수 있다.
spring:
application:
name: sample
datasource1:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test
username: root
password: root
hikari:
maximum-pool-size: 5
jpa:
hibernate:
ddl-auto: create-drop
naming:
# implicit-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
show-sql: true
datasource2:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/board
username: root
password: root
hikari:
maximum-pool-size: 3
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
logging:
level:
com:
zaxxer:
hikari: trace
org.hibernate: trace
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
...
implementation 'io.velog.youmakemesmile:autoconfigure:0.0.1-SNAPSHOT'
}
2025-01-04T18:13:10.700+09:00 DEBUG 56538 --- [sample] [ main] o.hibernate.jpa.internal.util.LogHelper : PersistenceUnitInfo [
name: default
persistence provider classname: null
classloader: jdk.internal.loader.ClassLoaders$AppClassLoader@4e0e2f2a
excludeUnlistedClasses: true
JTA datasource: null
Non JTA datasource: HikariDataSource (null)
Transaction type: RESOURCE_LOCAL
PU root URL: file:/Users/chkim/.m2/repository/io/velog/youmakemesmile/autoconfigure/0.0.1-SNAPSHOT/autoconfigure-0.0.1-SNAPSHOT.jar
Shared Cache Mode: UNSPECIFIED
Validation Mode: AUTO
Jar files URLs []
Managed classes names [
io.velog.youmakemesmile.autoconfigure.primary.PrimaryEntity]
Mapping files names []
Properties []
2025-01-04T18:13:12.076+09:00 DEBUG 56538 --- [sample] [ main] o.hibernate.jpa.internal.util.LogHelper : PersistenceUnitInfo [
name: second
persistence provider classname: null
classloader: jdk.internal.loader.ClassLoaders$AppClassLoader@4e0e2f2a
excludeUnlistedClasses: true
JTA datasource: null
Non JTA datasource: HikariDataSource (null)
Transaction type: RESOURCE_LOCAL
PU root URL: file:/Users/chkim/.m2/repository/io/velog/youmakemesmile/autoconfigure/0.0.1-SNAPSHOT/autoconfigure-0.0.1-SNAPSHOT.jar
Shared Cache Mode: UNSPECIFIED
Validation Mode: AUTO
Jar files URLs []
Managed classes names [
io.velog.youmakemesmile.autoconfigure.second.SecondEntity]
Mapping files names []
Properties []
위와 같이 설정한 두개의 JPA PersistenceUnit
이 정상적으로 적용되어 기동된 것을 확인 할 수 있다.
적용해본 예제는 간단한 예제이며 @Import
, @Conditional
, @Autoconfigure(after=, before=)
등을 활용하여 다양한 방법으로 상황에 맞는 Bean
등록이 가능하다.