훈훈훈
Spring boot :: Multiple DataSource 환경에서 @DataJpaTest 이슈 정리 및 스프링 코드 분석 본문
Spring boot :: Multiple DataSource 환경에서 @DataJpaTest 이슈 정리 및 스프링 코드 분석
훈훈훈 2021. 10. 11. 01:52Introduction
이번에는 Multiple DataSource 환경에서 JPA 로 개발 된 Repository 테스트를 할 때 사용하는 @DataJpaTest 로 테스트를 작성할 때 발생하는 문제, 그리고 어떻게 해결해야하는지에 대하여 정리해보려고 한다.
참고로 MyBatis 를 사용한다면, mybatis-spring-boot-starter-test 에 있는 @MyBatisTest 를 사용하면 된다.
@DataJpaTest 는 JPA, Entity Manager 를 @MyBatisTest 는 MyBatis 를 AutoConfiguration 을 하는 점 이외에는 동일하다.
예제 코드는 GitHub 에서 확인할 수 있다.
Problem
결론부터 말하자면 여러 DataSource 를 구성할 때 프로퍼티를 spring.datasource.url 이 아닌 spring.datasource.master.hikari 와 같이 커스텀하게 작성해서 발생하는 이슈이다.
아래는 일반적으로 단일 DataSource 를 사용할 때 작성하는 프로퍼티이다.
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/multiple-datesource?serverTimezone=UTC
username: root
password:
Spring boot 는 DataSource 를 AutoConfiguration 을 할 때, 기본적으로 spring.datasource.url 를 가져오지만, 여러 Datasource 를 구성하는 경우 Hikari CP DataSource 를 생성하기 위해서는 커스텀하게 작성한다.
이제 왜 이런 이슈가 발생하는지 예제 코드를 보면서 살펴보자.
아래는 예제로 작성한 DataSource 설정 클래스이다.
@Configuration
public class DataSourceConfig {
@Primary
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix="spring.datasource.master.hikari")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix="spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
}
Master / Slave 두 종류의 DataSource 로 구성하였고, 프로퍼티는 spring.datasource.master(slave).hikari 하위의 값들을 가져오도록 설정하였다.
그리고 아래는 Master / Slave datasource 를 구성하기 위한 프로퍼티 파일이다.
spring:
datasource:
master:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/multiple-datesource?serverTimezone=UTC
read-only: false
username: root
password:
slave:
hikari:
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://127.0.0.1:3306/multiple-datesource?serverTimezone=UTC
read-only: true
username: root
password:
이제 테스트 코드를 살펴보자.
아래 코드는 @DataJpaTest 를 사용하여 간단한 테스트 코드를 만들어 보았다.
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
public class JpaTest {
@Autowired
UserRepository userRepository;
@Test
void test() {
User user = userRepository.findUserByName("name").orElse(null);
System.out.println(user);
}
}
만약 위 코드가 Multiple DataSource 로 구성하지 않았다면 정상적으로 테스트가 종료가 되겠지만, 현재 구조로 테스트를 실행한다면 아래와 같은 오류 메시지를 볼 수 있다.
Failed to configure a DataSource: 'url' attribute is not specified and no embedded datasource could be configured. Reason: Failed to determine a suitable driver class
위에서 언급했듯이, 프로퍼티 파일에서 spring.datasource.url 을 가져오지 못하여 어플리케이션 실행을 하지 못한다고 알려주고 있다.
이제 스프링 코드를 보면서 왜 이런 문제가 발생하는지 살펴보자.
테스트할 때 사용했던 @DataJpaTest 어노테이션을 살펴보자
@DataJpaTest 는 기본적으로 in-memory DB 를 사용한다.
MySQL, PostgreSQL 같은 DB 를 사용하려면 @AutoConfigureTestDatabase 사용하라고 주석에 명시되어 있다.
테스트 코드를 작성할 때, embedded database 인 H2 를 사용한다면 어플리케이션만 띄우면 되기 때문에 편리하겠지만, 데이터베이스에 따라서 SQL 이 조금씩 다르기 때문에 실제 라이브에 배포 된 어플리케이션의 테스트 정확도를 보장하지는 못한다.
두 가지 방법 중 어떤 방법이 더 좋다는 접근 보다는 어떤 점을 더 중요하게 생각하는지에 따라서 embedded 로 사용할지 혹은 실제 database 를 사용할지 결정하는 것이 좋다고 생각한다.
이 글에서는 @AutoConfigureTestDatabas 설정을 통해 MySQL 을 사용할 것이다.
이제 해당 어노테이션을 살펴보자.
@AutoConfigureTestDatabas 살펴보면, Enum 값인 Replace 가 있는 것을 볼 수 있다.
디폴트로 ANY 적용되고 AUTO_CONFIGURED, NONE 두 가지 타입이 더 있는 것을 볼 수 있다.
주석만으로는 각각 타입에 따라 어떤 동작을 하는지 파악하기 어려워서 실제로 저 값을 사용하는 코드를 살펴보자.
아래 TestDatabaseAutoConfiguration 코드를 살펴보면 Replace 가 AUTO_CONFIGURED 또는 ANY 일 경우 embedded DB 를 생성하고 NONE 일 경우 아무런 행위 없이 지나가고 DataSourceAutoConfiguration 이 실행되는 것을 볼 수 있다.
위 과정까지가 테스트 어노테이션을 사용할 때 추가로 동작하는 부분이다.
이제 스프링이 DataSource Bean 을 등록할 때 사용하는 DataSourceAutoConfiguration 을 통해 DataSource 를 Bean 으로 등록하는 과정을 보자.
시작하기 전에 짚고 넘어가야 될 부분은 Spring boot 2.0 부터는 Tomcat Connection Pool 이 아닌 Hikari Connection Pool 을 사용한다. 따라서 DataSourceAutoConfiguration 을 통해 Hikari CP 가 등록되는 과정을 살펴보자.
아래는 DataSourceConfiguration 클래스에 있는 내부 클래스 Hikari 코드이다.
코드를 살펴보면 DataSourceProperties 를 넘겨 받아 DataSource 를 생성하는 것을 볼 수 있다.
이 부분이 Multiple DataSource 를 사용할 때 발생하는 문제의 시작점이라고 볼 수 있다.
그 이유는 파라미터로 받는 DataSourceProperties 를 보면 알 수 있는데, 코드를 살펴보면 spring.datasource 하위에 있는 값들을 가져오는 것을 볼 수 있다.
즉, 위의 예시처럼 spring.datasource.master.hikari 이런식으로 설정 값들을 작성을 하면 DataSourceProperties 는 값을 읽어올 방법이 없는 것이다.
아래는 DataSourceProperties 코드이다.
아래는 DataSourceConfiguration 클래스 내에 있는 Hikari 내부 클래스에서 createDataSource( ) 메서드를 호출할 때 실질적으로 호출되는 메서드이다.
해당 코드는 DataSourceBuilder 를 사용해서 프로퍼티와 타입을 가지고 DataSource 를 생성하는 것을 볼 수 있다.
문제의 원인은 위에서 발견했지만, DataSource 가 어떻게 생성이 되는지 궁금하여 DataSourceBuilder 코드를 좀 더 살펴 보았다.
먼저, DataSourceBuilder 는 스프링 부트 버전에 따라 코드가 변경되었다.
현업에서 사용했던 2.3.10 버전 그리고 위에서 계속 살펴보았던 예제 코드는 스프링 2.5.5 버전이다.
서로 살펴보면서 코드를 보던 중 상당 부분이 바뀌어서 두 버전 다 살펴보려고 한다.
먼저 2.3.10 버전의 DataSourceBuilder 보면 상당히 심플한 구조를 보이고 있다.
build( ) 메서드를 살펴보면, DataSource Type 을 가져오고 나서 bind( ) 메서드를 호출하여 url 과 jdbc-url 을 서로 매핑하는 것을 볼 수 있다.
그에 비하여 2.5.5 버전은 조금 복잡한 로직으로 되어 있다.
build( ) 메서드에서 첫번째 라인 DataSourceProperties.forType( ) 메서드의 호출을 따라가다 보면 아래와 같이 bind 역할을 하는 HikariDataSourceProperties 를 볼 수 있다.
아래는 HikariDataSourceProperties 내부 클래스 코드이다.
코드를 보면 DataSourceProperty 에 있는 값, 그리고 HikariDataSource 의 get(set) 메서드를 파라미터로 넘겨줘서 add( ) 하고 있는 것을 볼 수 있다.
호출되는 add( ) 는 마찬가지로 DataSourceBuilder 에 있는 MappedDataSourceProperties 내부 클래스에 있는 메서드이다. Map 객체에 데이터를 넣어주는 것을 볼 수 있다.
스프링 코드를 따라가 보았을 때, 저 Map 객체에서 위에서 저장 헀던, HikariDataSource 의 get(set) 메서드를 호출하면서 값들을 매핑하는 것을 볼 수 있다.
스프링 코드는 여기까지 살펴보고 정리하자면, @DataJpaTest 를 사용할 때 Spring boot 에서 기본적으로 지원하는 자동 설정으로 실행이 되기 때문에 오류가 발생한다. 이 문제를 해결하기 위해 @ImportAutoConfiguration 를 사용하여 등록한 Datasource 설정 클래스를 Import 시켜줘야 한다.
Code Example
아래 코드는 위에 예제에서 작성했던 Multiple DataSource 설정 클래스이다.
@Configuration
public class DataSourceConfig {
@Primary
@Bean(name = "masterDataSource")
@ConfigurationProperties(prefix="spring.datasource.master.hikari")
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(name = "slaveDataSource")
@ConfigurationProperties(prefix="spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
}
위 설정 클래스를 아래 테스트 코드르 작성할 때, @ImportAutoConfiguration(DataSourceConfig.class) 라인을 추가해주면 정상적으로 실행이 되는 것을 볼 수 있다.
@AutoConfigureTestDatabase(replace = Replace.NONE)
@DataJpaTest
@ImportAutoConfiguration(DataSourceConfig.class)
public class JpaTest {
@Autowired
UserRepository userRepository;
@Test
void test() {
User user = userRepository.findUserByName("name").orElse(null);
System.out.println(user);
}
}
'Spring Framework > 개념' 카테고리의 다른 글
Spring Cloud :: Spring cloud sleuth 정리 (0) | 2021.11.13 |
---|---|
Spring boot :: Caffeine cache 정리 (2) | 2021.10.31 |
Spring boot :: Mockito 로 WebClient 테스트 하기 (1) | 2021.09.26 |
Spring boot :: JPA, Mybatis Transaction Manager 정리 (1) | 2021.09.06 |
Spring boot :: Datasource Replication 구현 (1) | 2021.09.05 |