Spring Boot 아직도 모르고 그냥 사용한다고?!? - AutoConfiguration

YouMakeMeSmile·2025년 1월 4일
0
post-thumbnail

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가 생성된다. 위의 내용을 이해하자면 다음과 같다.

  1. @AutoConfiguration(before = SqlInitializationAutoConfiguration.class) : 해당 AutoConfiguration를 수행하기 이전에 SqlInitializationAutoConfiguration를 수행한다.
  2. @ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class }) : DataSource.class, EmbeddedDatabaseType.class 두 클래스가 모두 classpath에 존재해야 한다.
  3. @ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory") : io.r2dbc.spi.ConnectionFactory 타입의 Bean이 없어야 한다.

  1. @Conditional(PooledDataSourceCondition.class) : PooledDataSourceCondition의 조건을 만족해야 한다.
  2. @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;
        }
    }
    ...
  1. @ConditionalOnClass({HikariDataSource.class}) : HikariDataSource.class이 classpath에 존재해야 한다.
  2. @ConditionalOnMissingBean({DataSource.class}) : DataSource 타입의 Bean이 없어야 한다.
  3. @ConditionalOnProperty( name = {"spring.datasource.type"}, havingValue = "com.zaxxer.hikari.HikariDataSource", matchIfMissing = true ) : spring.datasource.type 값이 com.zaxxer.hikari.HikariDataSource으로 일치하거나 값이 설정이 안되어야한다.

이렇게 HikariDataSourceBean으로 등록되는 것이다!!!


그렇다면 이제는 우리가 만든 설정들에 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 등록이 가능하다.

profile
어느새 7년차 중니어 백엔드 개발자 입니다.

0개의 댓글

관련 채용 정보