본문 바로가기
공부/프로그래밍

[JPA] DataSource 사용자설정 및 @DataJpaTest 테스트

by demonic_ 2019. 8. 4.
반응형

여기서 진행하는 내용의 DBMS는 mysql 8.x 를 이용했고 로컬에 설치했다.
그래서 url 부분에 serverTimezone=UTC 가 파라미터로 추가되었다.(5.x 이하버전은 안해되 됨)

spring.datasource 를 사용하지 않고 커스텀하게 설정할 것이며 여기서는 alert.datasource 로 생성한다.


우선 application.properties 를 다음과 같이 등록한다

spring.jpa.generate-ddl=true
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.database=mysql
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# 테스트서버
alert.datasource.driverClassName=com.mysql.cj.jdbc.Driver
alert.datasource.jdbcUrl=jdbc:mysql://localhost:3306/alert?serverTimezone=UTC
alert.datasource.username=alert
alert.datasource.password=test1234

 

Alert Datasource 를 등록할 설정클래스를 생성한다
@EnableJpaRepositories 안에는 레파지토리 경로를,
LocalContainerEntityManagerFactoryBean 메서드 내 package 는 Entity 경로를 적어준다.

@Configuration
@PropertySource(value = "classpath:application.properties")
@EnableJpaRepositories(
        basePackages = "kr.jpamulti.jpa.alert.repository",
        entityManagerFactoryRef = "alertEntityManagerFactory",
        transactionManagerRef = "alertTransactionManager"
)
public class AlertDataSourceConfig {

    @Bean
    @Primary
    @ConfigurationProperties(prefix = "alert.datasource")
    public DataSource alertDataSource(){
        DataSource build = DataSourceBuilder
                .create()
                .build();
        return build;
    }

    @Bean
    @Primary
    public LocalContainerEntityManagerFactoryBean alertEntityManagerFactory(EntityManagerFactoryBuilder builder, @Qualifier("alertDataSource") DataSource dataSource) {
        return builder.dataSource(dataSource)
                .packages("kr.jpamulti.jpa.alert.entity")
                .build();
    }

    @Bean
    @Primary
    public PlatformTransactionManager alertTransactionManager(@Qualifier("alertEntityManagerFactory") EntityManagerFactory entityManagerFactory){
        return new JpaTransactionManager(entityManagerFactory);
    }
}

 

application.properties 를 보면 url 대신 jdbcUrl을 사용한다.
이름을 변경한 이유는 DataSourceBuilder 를 이용해 생성할 때(아래 AlertDataSourceConfig 클래스 참조), jdbcUrl 를 사용하기 때문이다. 

만약 url로 등록하면 다음의 에러를 발생한다

Caused by: javax.persistence.PersistenceException: [PersistenceUnit: default] Unable to build Hibernate SessionFactory; nested exception is java.lang.IllegalArgumentException: jdbcUrl is required with driverClassName.

JPA는 HikariCP(DB Connection Pool)를 이용하는데 생성하기 전에 HikariConfig 에서 필요한 값이 있는지 체크한다.

public class HikariConfig implements HikariConfigMXBean
{
...
   @SuppressWarnings("StatementWithEmptyBody")
   public void validate()
   {
      if (poolName == null) {
         poolName = generatePoolName();
      }
      else if (isRegisterMbeans && poolName.contains(":")) {
         throw new IllegalArgumentException("poolName cannot contain ':' when used with JMX");
      }

      // treat empty property as null
      //noinspection NonAtomicOperationOnVolatileField
      catalog = getNullIfEmpty(catalog);
      connectionInitSql = getNullIfEmpty(connectionInitSql);
      connectionTestQuery = getNullIfEmpty(connectionTestQuery);
      transactionIsolationName = getNullIfEmpty(transactionIsolationName);
      dataSourceClassName = getNullIfEmpty(dataSourceClassName);
      dataSourceJndiName = getNullIfEmpty(dataSourceJndiName);
      driverClassName = getNullIfEmpty(driverClassName);
      jdbcUrl = getNullIfEmpty(jdbcUrl);

      // Check Data Source Options
      if (dataSource != null) {
         if (dataSourceClassName != null) {
            LOGGER.warn("{} - using dataSource and ignoring dataSourceClassName.", poolName);
         }
      }
      else if (dataSourceClassName != null) {
         if (driverClassName != null) {
            LOGGER.error("{} - cannot use driverClassName and dataSourceClassName together.", poolName);
            // NOTE: This exception text is referenced by a Spring Boot FailureAnalyzer, it should not be
            // changed without first notifying the Spring Boot developers.
            throw new IllegalStateException("cannot use driverClassName and dataSourceClassName together.");
         }
         else if (jdbcUrl != null) {
            LOGGER.warn("{} - using dataSourceClassName and ignoring jdbcUrl.", poolName);
         }
      }
      else if (jdbcUrl != null || dataSourceJndiName != null) {
         // ok
      }
      else if (driverClassName != null) {
         jdbcUrl is required with driverClassName.", poolName);
         throw new IllegalArgumentException("jdbcUrl is required with driverClassName.");
      }
      else {
         LOGGER.error("{} - dataSource or dataSourceClassName or jdbcUrl is required.", poolName);
         throw new IllegalArgumentException("dataSource or dataSourceClassName or jdbcUrl is required.");
      }

      validateNumerics();

      if (LOGGER.isDebugEnabled() || unitTest) {
         logConfiguration();
      }
   }
...   
}

 

변수명을 보면 url 대신 jdbcUrl 을 이용한다. 그래서 jdbcUrl 을 쓰게되면 'else if (jdbcUrl != null || dataSourceJndiName != null) {' 부분에서 OK가 떨어져 다음으로 넘어가지만 url을 쓸 경우 'else if (driverClassName != null) {' 이 부분에서 걸리면서 jdbcUrl is required with driverClassName. 문구를 찍는 것이다. 그래서 jdbcUrl 로 입력해야 한다.

그리고 spring.datasource 를 쓸 경우 url 그대로 쓴다.

살펴볼 것은 둘다 org.springframework.boot.jdbc.DataSourceBuilder 를 쓴다는 점인데, 한쪽은 url을, 한쪽은 jdbcUrl을 쓴다는 점이다.

spring.datasource 를 이용해 생성할 경우 DataSourceConfiguration 클래스에 createDataSource를 이용해 빈을 생성하는데 이때 DataSourceProperties.initializeDataSourceBuilder 를 이용한다. 코드를 살펴보면 spring.datasource 나 alert.datasource 나 둘다 DataSourceBuilder를 이용하지만 spring.datasource 의 경우 서버접속정보를 직접 등록한다.(만약 위의 코드에서 alert.datasource를 직접등록한다면 property의 키값을 임의로 줘도 상관없다는 의미)

 

(gradle: org.springframework.boot:spring-boot-autoconfigure)
DataSourceConfiguration.class

abstract class DataSourceConfiguration {
...
	@SuppressWarnings("unchecked")
	protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
		return (T) properties.initializeDataSourceBuilder().type(type).build();
	}
...
}

 

(gradle: org.springframework.boot:spring-boot 참조)
org.springframework.boot.jdbc.DataSourceProperties 파일

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
...
	/**
	 * Initialize a {@link DataSourceBuilder} with the state of this instance.
	 * @return a {@link DataSourceBuilder} initialized with the customizations defined on
	 * this instance
	 */
	public DataSourceBuilder<?> initializeDataSourceBuilder() {
		return DataSourceBuilder.create(getClassLoader()).type(getType()).driverClassName(determineDriverClassName())
				.url(determineUrl()).username(determineUsername()).password(determinePassword());
	}
...
}

 

따라가다 보면 등록된 정보중 몇몇개는 다른 이름으로 치환된다.
url => jdbc-url
username => user

public final class DataSourceBuilder<T extends DataSource> {
...
	@SuppressWarnings("unchecked")
	public T build() {
		Class<? extends DataSource> type = getType();
		DataSource result = BeanUtils.instantiateClass(type);
		maybeGetDriverClassName();
		bind(result);
		return (T) result;
	}
...
	private void bind(DataSource result) {
		ConfigurationPropertySource source = new MapConfigurationPropertySource(this.properties);
		ConfigurationPropertyNameAliases aliases = new ConfigurationPropertyNameAliases();
		aliases.addAliases("url", "jdbc-url");
		aliases.addAliases("username", "user");
		Binder binder = new Binder(source.withAliases(aliases));
		binder.bind(ConfigurationPropertyName.EMPTY, Bindable.ofInstance(result));
	}
...
	public DataSourceBuilder<T> url(String url) {
		this.properties.put("url", url);
		return this;
	}
...
}

 

그래서 최종적으로 생성된 dataSource 를 보면 url은 없고 jdbcUrl만 있다.

dataSource = {HikariDataSource@3769} "HikariDataSource (null)"
	isShutdown = {AtomicBoolean@3808} "false"
	fastPathPool = null
	pool = null
	catalog = null
	connectionTimeout = 30000
	validationTimeout = 5000
	idleTimeout = 600000
	leakDetectionThreshold = 0
	maxLifetime = 1800000
	maxPoolSize = -1
	minIdle = -1
	username = "alert"
	password = "test1234"
	initializationFailTimeout = 1
	connectionInitSql = null
	connectionTestQuery = null
	dataSourceClassName = null
	dataSourceJndiName = null
	driverClassName = "com.mysql.cj.jdbc.Driver"
	jdbcUrl = "jdbc:mysql://localhost:3306/alert?serverTimezone=UTC"
	poolName = null
	schema = null
	transactionIsolationName = null
	isAutoCommit = true
	isReadOnly = false
	isIsolateInternalQueries = false
	isRegisterMbeans = false
	isAllowPoolSuspension = false
	dataSource = null
	dataSourceProperties = {Properties@3813}  size = 0
	threadFactory = null
	scheduledExecutor = null
	metricsTrackerFactory = null
	metricRegistry = null
	healthCheckRegistry = null
	healthCheckProperties = {Properties@3814}  size = 0
	sealed = false

 

Alert 엔티티를 생성한다

package kr.jpamulti.jpa.alert.entity;

import javax.persistence.Entity;
import javax.persistence.Id

@Entity
public class Alert {

    @Id
    private Long id;

    private String content;


    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

 

레파지토리를 생성한다

package kr.jpamulti.jpa.alert.repository;

import kr.jpamulti.jpa.alert.entity.Alert;
import org.springframework.data.jpa.repository.JpaRepository;

public interface AlertRepository extends JpaRepository<Alert, Long> {
}

 

 

# 테스트 작성

테스트 클래스를 생성한다

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class AlertRepositoryTest {

    @Autowired
    private AlertRepository alertRepository;

    @Test
    public void crud(){

        Alert alert = new Alert();
        alert.setId(1l);
        alert.setContent("테스트입니다");

        alertRepository.save(alert);

        Long count = alertRepository.count();

        System.out.println("========================== 카운트: " + count);

        assertThat(count).isEqualTo(1);
    }
}

 

에러가 난다. 문구를 살펴보니 기본 dataSource가 없어 에러가 난다.
기본 dataSource 를 생성하지 못했기 때문.

Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaConfiguration': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource [org/springframework/boot/autoconfigure/jdbc/DataSourceConfiguration$Hikari.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [com.zaxxer.hikari.HikariDataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Failed to determine a suitable driver class

 

해결하기 위해선 2가지 선택이 있다.
1) 테스트에 @DataJpaTest 대신 @SpringBootTest를 추가
2) 관련 빈을 사용하는 Configuration 을 Import로 등록

 

여기서는 후자를 사용해서 Import를 이용했다.

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(AlertDataSourceConfig.class)
public class AlertRepositoryTest {
	...

 

로그 확인

Hibernate: 
    select
        alert0_.id as id1_0_0_,
        alert0_.content as content2_0_0_ 
    from
        Alert alert0_ 
    where
        alert0_.id=?
2019-08-03 20:49:17.128  INFO 32220 --- [           main] o.h.h.i.QueryTranslatorFactoryInitiator  : HHH000397: Using ASTQueryTranslatorFactory
Hibernate: 
    insert 
    into
        Alert
        (content, id) 
    values
        (?, ?)
Hibernate: 
    select
        count(*) as col_0_0_ 
    from
        Alert alert0_
========================== 카운트: 1

 

데이터베이스에서 결과 확인

mysql> show tables;
+-----------------+
| Tables_in_alert |
+-----------------+
| Alert           |
+-----------------+
1 row in set (0.00 sec)

mysql> select * from Alert;
Empty set (0.00 sec)

 

테스트이기 때문에 롤백되어 데이터가 없다.

끝.

 

 

 

참고 github

https://github.com/lemontia/jpa-datasource-custom

 

lemontia/jpa-datasource-custom

Contribute to lemontia/jpa-datasource-custom development by creating an account on GitHub.

github.com

 

반응형

댓글