[토비-스프링부트-이해와원리]

Spring JDBC 자동 구성 개발

지금까지 우리에게 익숙한 SpringBoot의 자동 설정 기능을 이용하여 JDBC 자동 설정을 개발해 보자.

MyAutoConfigurationDataSourceConfig라는 구성 정보 클래스를 만들고 모든 것을 넣기만 하면 됩니다.

해당 설정 정보는 org.springframework.jdbc.core.JdbcOperations 인터페이스가 존재하는 경우에만 설정 정보로 등록된다.

이 인터페이스는 Spring의 JDBC 모듈이 JdbcTemplate의 인터페이스로 추가된 경우에만 존재합니다.

먼저 DataSource가 필요합니다. DataSource는 DB 연결을 담당하는 인터페이스입니다.

먼저 SimpleDriverDataSource 구현으로 간단하게 유지하겠습니다.

그리고 다양한 환경변수를 읽기 위한 DataSourceProperties가 필요하다.

클래스에 @MyConfigurationProperties를 첨부하고 SimpleDriverDataSource가 bean으로 등록된 경우에만 환경 변수를 읽습니다.

그런 다음 HikariDataSource가 있으면 HikariDataSource를 사용하고 없으면 SimpleDriverDataSource를 사용하도록 코드를 변경합니다.

그리고 JdbcTemplate과 JdbcTransactionManager를 통해 트랜잭션을 관리해 보자. 그리고 @ConditionalOnSingleCandidate를 통해 하나의 DataSource bean만 존재하는 경우 두 bean이 DataSource를 주입하도록 합니다.

간단한 인메모리 DB H2 for DB를 사용해보자.

그림을 보면 이렇습니다.


DataSource 자동 구성 클래스

먼저 다음 종속성을 추가해 보겠습니다.

implementation("org.springframework:spring-jdbc")

그런 다음 아래에서 JdbcOperations 인터페이스를 찾으십시오.


그런 다음 DataSourceConfig 를 만듭니다.

먼저 @MyAutoConfiguration, @ConditionalMyOnClass 주석을 첨부하여 MyAutoConfiguration.imports 존재하는 빈을 선택함을 알려드립니다.

사실 @Configuration을 추가해도 상관없지만 관례를 따르자.

그리고 dataSource 빈을 생성합니다.

@MyAutoConfiguration
@ConditionalMyOnClass("org.springframework.jdbc.core.JdbcOperations")
class DataSourceConfig {
    @Bean
    fun dataSource(): DataSource {
        val dataSource = SimpleDriverDataSource()
        return dataSource
    }
}

MyAutoConfiguration.imports 파일에 빈을 등록합니다.


dataSource를 그대로 반환하는 대신 이제 환경 변수를 읽고 URL, 비밀번호 등을 설정한 다음 반환해야 합니다.

따라서 MyDataSourceProperties라는 빈 클래스를 만들고 다음과 같이 작성해 보겠습니다.

@MyAutoConfiguration
@ConditionalMyOnClass("org.springframework.jdbc.core.JdbcOperations")
class DataSourceConfig {
    @Bean
    fun dataSource(properties : MyDataSourceProperties): DataSource {
        val dataSource = SimpleDriverDataSource()
        return dataSource
    }
}

그런 다음 MyDataSourceProperties가 빈이 아니므로 자동으로 연결할 수 없다는 컴파일 오류가 발생합니다.

그래서 아래와 같이 @MyConfigurationProperties 주석을 붙이고 필요한 정보를 데이터 클래스에 주입하여 생성합니다.

@MyConfigurationProperties(prefix = "data")
data class MyDataSourceProperties(
    val driverClassName: String? = null,
    val url: String? = null,
    val username: String? = null,
    val password: String? = null,
)

그리고 DataSourceConfig 등록 시 MyDataSourceProperties만 bean으로 등록되도록 다음을 수행한다.

@MyAutoConfiguration
@ConditionalMyOnClass("org.springframework.jdbc.core.JdbcOperations")
@EnableMyConfigurationProperties(MyDataSourceProperties::class)
class DataSourceConfig {
    @Bean
    fun dataSource(properties: MyDataSourceProperties): DataSource {
        val dataSource = SimpleDriverDataSource()
        return dataSource
    }
}

그리고 아래에 h2 데이터베이스 엔진 종속성을 추가합니다.

implementation("com.h2database:h2:2.1.214")

그리고 아래와 같이 application.properties를 변경합니다.

server.contextPath=/app
server.port=8080
data.driver-class-name=org.h2.Driver
data.url=jdbc:h2:mem:
data.username=sa
data.password=

그리고 앱을 실행해 봅시다. 잘 작동한다.

그럼에도 불구하고 보기만 해도 왠지 모르게 뭉클해진다. 테스트 코드를 만들어 봅시다.

먼저 테스트에 대한 참고 사항을 살펴보겠습니다.

@ExtendWith(SpringExtension::class)

Junit5가 SpringExtension 확장 기능을 사용할 것이라는 선언입니다. 이를 통해 테스트 코드에서 @Autowired 또는 @MockBean과 같은 주석을 사용할 수 있습니다.

@ContextConfiguration(classes = (TobyspringApplication::class))

그리고 위의 주석은 클래스에 의해 등록된 모든 빈이 등록된다는 것을 의미합니다.

@TestPropertySource("classpath:/application.properties")

마지막으로 Annotation은 Spring Boot의 기능인 application.properties를 읽고 삽입하는 역할을 합니다.

결과는 다음 코드입니다.

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = (TobyspringApplication::class))
@TestPropertySource("classpath:/application.properties")
class DataSourceTest {
    @Autowired
    lateinit var dataSource: DataSource

    @Test
    fun connect() {
        val connection = dataSource.connection
        connection.close()
    }
}

결과는 성공적입니다.


이번에는 Hikari 데이터 소스를 만들어 보겠습니다. 먼저 다음 주석을 추가합니다.

implementation("hikari-cp:hikari-cp:3.0.1")

그리고 다음과 같이 DataSourceConfig에 hikariDataSource Bean을 등록합니다.

@Bean
fun hikariDataSource(properties: MyDataSourceProperties): DataSource {
    val dataSource = HikariDataSource()
    dataSource.jdbcUrl = properties.url
    dataSource.username = properties.username
    dataSource.password = properties.password
    dataSource.driverClassName = properties.driverClassName
    return dataSource
}

그러나 물론 이렇게 하고 응용 프로그램을 실행하면 겹치는 빈으로 인해 오류가 발생합니다.

따라서 아래와 같이 Hikari 라이브러리가 등록되어 있는 경우에만 Hikari dataSource를 등록하고 그렇지 않으면 simpleDataSource를 사용한다.

@MyAutoConfiguration
@ConditionalMyOnClass("org.springframework.jdbc.core.JdbcOperations")
@EnableMyConfigurationProperties(MyDataSourceProperties::class)
class DataSourceConfig {
    @Bean
    @ConditionalMyOnClass("com.zaxxer.hikari.HikariDataSource")
    fun hikariDataSource(properties: MyDataSourceProperties): DataSource {
        val dataSource = HikariDataSource()
        dataSource.jdbcUrl = properties.url
        dataSource.username = properties.username
        dataSource.password = properties.password
        dataSource.driverClassName = properties.driverClassName
        return dataSource
    }
    @Bean
    @ConditionalOnMissingBean
    fun dataSource(properties: MyDataSourceProperties): DataSource {
        val dataSource = SimpleDriverDataSource()
        dataSource.url = properties.url
        dataSource.username = properties.username
        dataSource.password = properties.password
        dataSource.driver = Class.forName(properties.driverClassName).getConstructor().newInstance() as Driver?
        return dataSource
    }
}

테스트 코드를 다시 실행하고 검증하면 Hikari를 검증할 수 있습니다.


이번에는 JdbcTemplate과 PlatformTransactionManager를 만들어 봅시다.

먼저 다음 두 개의 빈을 DataSourceConfig에 추가합니다.

jdbcTemplate은 db에 요청을 보내는 데 필요한 상용구를 작성하지 않고도 쿼리를 실행할 수 있는 템플릿입니다.

PlatformJdbcTransactionManager는 트랜잭션 제어에 도움이 되는 추상 인터페이스입니다.

평소에 bean을 직접 사용하지는 않지만 @Transactional 애노테이션을 붙이면 해당 bean이 AOP를 통해 접근하여 transaction을 도와준다.

@Bean
@ConditionalOnSingleCandidate(DataSource::class)
@ConditionalOnMissingBean
fun jdbcTemplate(dataSource: DataSource): JdbcTemplate {
    return JdbcTemplate(dataSource)
}

@Bean
@ConditionalOnMissingBean
fun jdbcTransactionManager(dataSource: DataSource): PlatformTransactionManager {
    return JdbcTransactionManager(dataSource)
}

그리고 DataSourceConfig에 다음 주석을 추가합니다.

Activate를 추가하면 보통 조건에 따라 무언가를 불러오는 기능이 있습니다.

이 어노테이션은 빈 포스트 프로세서를 조건에 따라 구성 정보로 등록합니다.

@EnableTransactionManagement

그런 다음 빈 포스트 프로세서는 모든 @Transactional 주석이 달린 빈을 반복하고 AOP에서 PlatformTransactionManager를 사용하여 트랜잭션 프록시 개체를 만듭니다.

JdbcTemplate에 대한 테스트 코드를 생성하면 다음과 같습니다.

@ExtendWith(SpringExtension::class)
@ContextConfiguration(classes = (TobyspringApplication::class))
@TestPropertySource("classpath:/application.properties")
@Transactional
class JdbcTemplateTest {
    @Autowired
    lateinit var jdbcTemplate: JdbcTemplate

    @BeforeEach
    fun init() {
        jdbcTemplate.execute("create table if not exists hello(name varchar(50) primary key, count int)")
    }

    @Test
    fun insertAndQuery() {
        jdbcTemplate.update("insert into hello values(?, ?)", "Toby",3)
        jdbcTemplate.update("insert into hello values(?, ?)", "Spring",1)

        val count = jdbcTemplate.queryForObject("select count(*) from hello", Int::class.java)
        assertThat(count).isEqualTo(2)
    }
}

테스트를 실행해보면 스프링 컨텍스트를 등록하면서 Hikarina 환경변수 주입이 잘 되는 것을 확인할 수 있다.