훈훈훈

Spring boot :: Multiple DataSource 환경에서 @DataJpaTest 이슈 정리 및 스프링 코드 분석 본문

Spring Framework/개념

Spring boot :: Multiple DataSource 환경에서 @DataJpaTest 이슈 정리 및 스프링 코드 분석

훈훈훈 2021. 10. 11. 01:52

Introduction


이번에는 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);
    }
}

 

 

 

Comments