Server/Spring

Spring - 다중 데이터 소스 환경에서 spring.sql.init이 동작하지 않는 이슈 해결

JaeHoney 2022. 10. 22. 14:13
반응형

회사에서 작업하는 프로젝트가 테스트를 할 때 docker-compose에 MySQL을 띄워서 로컬에서만 테스트하고 있었다.

 

해당 부분은 자동화할 필요가 있다고 느껴서 테스트 코드에 H2 및 테스트 자동화를 적용하기로 했다.

 

그런데 hibernate의 ddl-auto 옵션을 이용한 스키마 정의는 사용할 수 없었다.

-> 엔터티의 Annotation을 아래와 같이 샤딩 모듈에 맞게 Custom하게 사용해서 DDL이 올바르지 않게 생성되지 않기 때문

@Table(schema = "mail_##part_no##")

그래서 테스트를 진행할 때 DDL 스크립트를 실행하도록 처리해야 했다.

구현

src/test/resources/application.yml에서 아래의 환경 설정을 적용했다.

spring:
  jpa:
    hibernate:
      ddl-auto: none
  sql:
    init:
      schema-locations: classpath:sql/mail-schema.sql
      mode: always

해당 설정은 Hibernate에서 제공하는 DDL 자동 생성 기능을 사용하지 않고 직접 작성한 sqll이 실행되도록 한다.

 

그런데 해당 설정을 적용하고 전체 테스트는 성공하는데 일부 테스트가 깨지는 현상이 발생한다.

  • 전체 Test: 성공
  • Repository class Test: 성공
    • Repository method Test: 성공
  • Controller class Test: 실패
    • Controller method Test: 실패
  • 나머지 Test: 성공

즉, 전체 테스트에서의 Controller Test는 통과하지만, Controller Test만 따로 실행하면 테스트가 깨졌다.

 

원인을 파악하고자 에러 메시지를 봤다.

메시지를 보니까 mail-schema.sql이 실행되지 않고 있던 것이었다.

 

이후 디버깅을 통해 Controller Test만 실행할 때는 해당 sql을 실행해주지 않음을 알 수 있었다.

(Repository Test만 실행할 때는 sql이 정상적으로 실행되었다.)

 

BaseController에 아래의 애노테이션을 붙여주면 정상적으로 동작은 하는 것을 확인할 수 있었다.

@Sql(
      scripts = { "classpath:sql/mail-schema.sql" }, 
      config = @SqlConfig(transactionManager = "MailDBTransactionManager")
)
public class BaseControllerTest {
    // ..
}

하지만 정확한 원인을 찾아서 확실하게 해결하고 싶었다.

  • yml을 통해서 간단하게 풀 수 있을 지 모르는데 불필요한 코드를 중복할 수도 있다.
  • 코드를 관리하기 어려워진다.
    • 어떤 테스트에서 해당 애노테이션을 사용하지..?! 에이! 그냥 붙이자 -> init이 두 번 실행 될 수도 있음

시도 1

처음에는 아래의 프로퍼티를 설정하지 않아서 발생한 이슈일 수 있다고 생각했다.

  • defer-datasource-initialization: true

하지만 해당 프로퍼티는 hibernate의 초기화를 통해 스키마가 생성된 이후 시점에 DML로 데이터를 채우기 위한 옵션으로

 

해당 이슈와 전혀 관계가 없었다.

시도 2

SqlInitializationAutoConfiguration가 Controller Test에서는 불러오지 않은 것이 아닐까? 라고 생각했지만,

 

해당 설정은 Controller Test와 Repository Test 시 모두 불러오고 있었다.

 

 

그래서 추가로 의심한 것이 SqlInitializationAutoConfiguration 설정이 로드 되기 전에 테스트가 실행되어 테스트가 깨졌을 수도 있을 것 같았다.

 

하지만 디버깅을 해보니까 Controller Test에서는 내부 순서와 관계없이 작성한 Sql을 실행하지 않고 있음을 알게 되었다.

 

(Test 메서드 내용을 모두 주석 처리해도 Sql이 실행되지 않고 있었음)

시도 3 (해결)

그래서 고민을 하다가.. 생각해보니! 해당 sql이 어떤 DB 서버에 날리는 sql인지 어떻게 알고 실행해주지..? 라는 의문이 들었다!

 

그래서 spring.sql.init 프로퍼티는 SpringBoot가 yml을 통해 DataSourceAutoConfiguration으로 데이터 소스를 새로 생성할 때만 적용해주는 걸까..? 라고 의심을 해봤다.

 

하지만 그렇다면 왜 전체 테스트, Repository 테스트에서는 sql.ini을 성공했는 지를 설명할 수 없었다.

 

결국 그것도 정확한 답이 아니었다..!

 

그래서 디버깅을 더 자세히 하기로 했다.

그래서 Schema, Data를 초기화하는 sql을 실행하는 곳DataSourceScriptDatabaseInitializer.class임을 알았다.

해당 runScripts가 실행되는 시점은 해당 클래스가 상속하는 추상클래스인 AbstractScriptDatabaseInitializer를 보면 알 수 있었다.

 

해당 클래스는 afterPropertiesSet를 재정의함으로써 BeanFactory에 의해 모든 property가 설정되고 나면 applySchemaScript()를 실행하고 있었다.

AbstractScriptDatabaseInitializer의 applySchemaScript() 메서드는 applyScripts() 메서드 호출을 통해 runScripts() 메서드를 실행한다.

해당 클래스를 빈으로 등록한 클래스는 DataSourceInitializationConfiguration.class 이다.!

 

지금 까지 모은 정보를 조합하면 DataSourceInitializationConfiguration 설정이 빈으로 등록이 되었다면 DataSourceScriptDatabaseInitializer에 의해 spring.sql.init를 통한 sql 스크립트가 실행되었어야 했다.

DataSourceInitializationConfiguration.class는 아래와 같이 SqlInitializationAutoConfiguration에서 Import하고 있었다.

@DataJpaTest@ImportAutoConfiguration 애노테이션을 통해 SqlInitializationAutoConfiguration설정을 불러온다.

 

(SpringBoot 실행 시에도 기본적으로 AutoConfiguration을 불러온다.)

 

그런데! 위에서 봤던 DataSourceScriptDatabaseIntializer(init.sql을 실행하는 클래스)를 빈으로 등록하는 DataSourceInitializationConfiguration.class 를 자세히 보면!

@ConditionalOnSingleCandidate(DataSource.class)를 볼 수 있었다!!

  • 해당 애노테이션은 대상으로 지정한 Bean이 하나만 존재할 때 설정을 사용한다는 것을 의미한다!

 

현재 소스 기준 Controller Test는 통합 테스트로 되어 있어서 모든 DataSource를 빈으로 등록한다!

 

즉, ControllerTest만 실행할 때는 DataSource가 2개 이상이라서 sql.init을 하지 않았던 것이었다!!

 

(전체 테스트를 할 때는 RepositoryTest에서 DataSource를 1개만 등록하므로 sql.init을 실행했기 때문에 성공했었다.)

 

결론

@ComponentScan(basePackages = "com.hiworks.office.maildb")
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public class MailRepositoryTest {
    // ...
}

RepositoryTest에서는 위와 같이 ComponantScan으로 DataSource를 1개만 빈으로 등록했기 때문에 DataSource 빈이 하나만 있는 상태라서 DataSourceInitializationConfiguration이 동작할 수 있었다.

@SpringBootTest
@Transactional
@AutoConfigureMockMvc
@AutoConfigureRestDocs
public class BaseControllerTest {
    // ...
}

반면, ControllerTest(통합 테스트)에서는 @SpirngBootTest로 MailDB의 DataSource 뿐만 아니라 다른 DataSource 빈까지 등록해서 DataSourceInitializationConfiguration 설정을 등록 하지 못해서 스크립트 실행이 되지 않았던 것이었다!!

 

그래서 각 DataSource 설정 내에서 아래의 코드를 추가함으로써 해결할 수 있었다.

@Bean
@Profile("test")
public DataSourceInitializer dataSourceInitializer(@Qualifier("mailDataSource") final DataSource
mailDataSource) {
    ResourceDatabasePopulator resourceDatabasePopulator = new ResourceDatabasePopulator();
    ClassPathResource schema = new ClassPathResource("sql/maildb/schema.sql");
    ClassPathResource data = new ClassPathResource("sql/maildb/data.sql");
    if(schema.exists()) {
        resourceDatabasePopulator.addScripts(schema);
    }
        if(data.exists()) {
        resourceDatabasePopulator.addScript(data);
    }
    DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
    dataSourceInitializer.setDataSource(subDbDataSource);
    dataSourceInitializer.setDatabasePopulator(resourceDatabasePopulator);
    return dataSourceInitializer;
}

해당 코드는 DataSourceInitializer를 빈을 등록해서 해당 DataSource가 생성될 때 특정 경로에 sql이 있다면 해당 sql로 초기화하도록 한다.

 

.

반응형