test code를 작성와중에 service를 호출해야하는 경우가 있었다. service 에는 jpa를 사용해 save() 하는 함수를 호출하는 로직이 있었는데 이 경우 까지 테스트를 진행하고 싶었다. 테스트 코드를 작성하고 crud 를 진행하기 위해서 코드를 실행했는데 아래와 같은 오류상황이 발생했다. test code를 작성하는게 처음이여서 에러도 생소한 에러였다.
java.lang.IllegalStateException: Failed to load ApplicationContext for
[WebMergedContextConfiguration@1c25deb0 testClass = com.user.server.user.service.UserServiceTest, locations =
[], classes = [com.user.server.UserApplication], contextInitializerClasses = [], activeProfiles = ["test"],
propertySourceLocations = [], propertySourceProperties =
["org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true"], contextCustomizers =
[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@47e2e487,
org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCust
omizer@26e356f0, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0,
org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@783a467b,
org.springframework.boot.test.web.reactive.server.WebTestClientContextCustomizer@3527942a,
org.springframework.boot.test.autoconfigure.actuate.observability.ObservabilityContextCustomizerFactory$Disab
leObservabilityContextCustomizer@9da1,
org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0,
org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@6340e5f0
, org.springframework.boot.test.context.SpringBootTestAnnotation@51e2b707], resourceBasePath =
"src/main/webapp", contextLoader = org.springframework.boot.test.context.SpringBootContextLoader, parent =
null]
첫번째 에러 구문을 살펴보면 코드가 실행중에 ApplicationContext가 정상적으로 실행이 안된다고 설명이 적혀있다.
런타임 에러인데 런타임 에러가 발생한 이유가 다양하기 때문에 다음 에러구문을 봐야지 어느정도 파악할 수 있을것 같았다.
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name
'entityManagerFactory' defined in class path resource
[com/user/server/config/datasource/DataSourceConfig.class]: [PersistenceUnit: default] Unable to build
Hibernate SessionFactory; nested exception is java.lang.IllegalArgumentException: dataSource or
dataSourceClassName or jdbcUrl is required.
여기서 대략 어떤 에러가 발생한건지 유추할 수 있었다. DataSourceConfig 에서 DataSource가 올바르게 주입되지 않아서 jdbc를 읽을 수 없다는 에러였다. 기존에 MySQL 을 DB로 사용을 하다가 TestCode를 진행할때는 H2 를 사용해서 테스트 코드를 진행하려고 하기 때문에 H2에 대한 정보를 properties에 작성을 했었다.
Caused by: jakarta.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate
SessionFactory; nested exception is java.lang.IllegalArgumentException: dataSource or dataSourceClassName or
jdbcUrl is required.
Caused by: java.lang.IllegalArgumentException: dataSource or dataSourceClassName or jdbcUrl is required.
at com.zaxxer.hikari.HikariConfig.validate(HikariConfig.java:1026)
at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:109)
at org.hibernate.engine.jdbc.connections.internal.DatasourceConnectionProviderImpl.getConnection(DatasourceConnectionProviderImpl.java:122)
at org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator$ConnectionProviderJdbcConnectionAccess.obtainConnection(JdbcEnvironmentInitiator.java:284)
at org.hibernate.resource.transaction.backend.jdbc.internal.DdlTransactionIsolatorNonJtaImpl.getIsolatedConnection(DdlTransactionIsolatorNonJtaImpl.java:41)
at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.jdbcStatement(GenerationTargetToDatabase.java:77)
at org.hibernate.tool.schema.internal.exec.GenerationTargetToDatabase.accept(GenerationTargetToDatabase.java:53)
at org.hibernate.tool.schema.internal.SchemaDropperImpl.applySqlString(SchemaDropperImpl.java:419)
at org.hibernate.tool.schema.internal.SchemaDropperImpl.applySqlStrings(SchemaDropperImpl.java:403)
at org.hibernate.tool.schema.internal.SchemaDropperImpl.dropFromMetadata(SchemaDropperImpl.java:272)
at org.hibernate.tool.schema.internal.SchemaDropperImpl.performDrop(SchemaDropperImpl.java:178)
at org.hibernate.tool.schema.internal.SchemaDropperImpl.doDrop(SchemaDropperImpl.java:149)
at org.hibernate.tool.schema.internal.SchemaDropperImpl.doDrop(SchemaDropperImpl.java:117)
at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.performDatabaseAction(SchemaManagementToolCoordinator.java:242)
at org.hibernate.tool.schema.spi.SchemaManagementToolCoordinator.lambda$process$5(SchemaManagementToolCoordinator.java:143)
여기까지가 에러 로그다.
[com.user.server.user.service.UserServiceTest] [INFO] The following 1 profile is active: "test"
[org.hibernate.dialect.H2Dialect] [WARN] HHH000393: The 0.0.0 version of H2 implements temporary table creation such that it commits current transaction; multi-table, bulk hql/jpaql will not work properly
[SQL dialect] [INFO] HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
위에 로그와 같이 test 프로퍼티와 H2가 실행이 되었지만 Bean으로 h2가 정상적으로 등록되지 못했다고 볼수있으 DataSourceConfig에서 MySQL에 대한 정보만 받도록 하였고 H2에 대한 어떠한 처리도 하지 않았기에 위의 에러로그처럼 발생한것 같았다.
@Configuration
@EnableJpaRepositories(
basePackages = "com.user.server.**.repository",
entityManagerFactoryRef = "entityManagerFactory",
transactionManagerRef = "transactionManager"
)
public class DataSourceConfig {
@Bean(name = "MysqlDataSource")
@ConfigurationProperties(prefix = "spring.datasource.mysql")
@Profile("!test")
public DataSource mysqlDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "H2DataSource")
@ConfigurationProperties(prefix = "spring.datasource.h2")
@Profile("test")
public DataSource h2DataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "entityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("MysqlDataSource") @Autowired(required = false) DataSource mysqlDataSource,
@Qualifier("H2DataSource") @Autowired(required = false) DataSource h2DataSource,
Environment env) {
LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean();
String activeProfile = env.getActiveProfiles().length > 0 ? env.getActiveProfiles()[0] : "default";
DataSource selectedDataSource = activeProfile.equals("test") ? h2DataSource : mysqlDataSource;
if (selectedDataSource == null) {
throw new IllegalStateException("DataSource가 설정되지 않았습니다. (환경: " + activeProfile + ")");
}
em.setDataSource(selectedDataSource);
em.setPackagesToScan("com.user.server.**.entity");
em.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
Map<String, Object> properties = new HashMap<>();
properties.put("hibernate.hbm2ddl.auto", env.getProperty("spring.jpa.hibernate.ddl-auto"));
properties.put("hibernate.dialect", env.getProperty("spring.jpa.properties.hibernate.dialect"));
properties.put("hibernate.show_sql", env.getProperty("spring.jpa.show-sql"));
properties.put("hibernate.format_sql", env.getProperty("spring.jpa.properties.hibernate.format_sql"));
em.setJpaPropertyMap(properties);
return em;
}
@Bean(name = "transactionManager")
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
return new JpaTransactionManager(entityManagerFactory);
}
}
위의 코드 처럼 properties가 test가 아닐 경우에는 mysql 을 작동하도록 하고 test일 경우에는 h2를 작동하도록 datasourceConfig를 수정했다.
코드를 수정하고 테스트 코드를 돌려봤는데 똑같은 에러가 발생해서 멘붕이 왔었다..
분명 profiles는 test
를 가리키고 있고 datasource.properties도 test를 사용하고 있는데 왜 자꾸 mysql 을 사용하는걸까...?
자료들을 찾아보니
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY)
어노테이션을 사용하면 MySQL 사용하는걸 H2로 덮어버릴 수 있다고 한다. 하지만 이건 내가 properties에 설정한 h2 데이터베이스가 아니라 spring boot가 내장형 h2로 새로만든 db로 덮어써서 사용하게 된다.
그렇기 때문에 지금 일시적인 상황 해결이 아니라 계속 h2를 사용해서 테스트 코드를 유용하게 사용하기 위해서는 properties에 있는 h2 db를 사용하게 하고 싶었다.
그래서 이것 저것 자료들을 찾아보다 jdbc-url 에 대한 내용이 생각났다. jdbc url 을 찾는건데 jdbc url 에 대한 문제는 없는 것일까?
찾아보니 HikariCP에서 기존에는 driverClassName
이 있으면 스프링이 url 을 jdbcUrl로 매핑해주었었다고 한다. 하지만 버전에 상관 없이 dirverClassName
을 직접 쓰고 싶다면 jdbc-url을 직접 지정하라고 한다.
그 글을 읽고 spring.datasource.h2.url
에서 spring.datasource.h2.jdb-url
로 변경을 해주니 정확하게 test properties에 있는 h2를 읽게 되었다.