2017년 5월 21일 일요일

Spring boot datasource replication

스프링 부트에서 dataBase를 하나가 아닌, 여러 개의 갯수를 사용할 때 일정 조건에 따라 Routing을 설정할 수 있다.
이 글에서는 서비스 단에서의 transaction 처리때 해당 transaction을 readOnly로 설정하였는지 아닌지에 따라,
master, slave로 나누어서 connection을 가져오는 예제를 살펴볼 것이다.

이번 예제를 진행하며 가장 혼란스러웠던 부분은 바로 아래 관련 부분이다.

@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})

spring boot data jpa를 사용하면 기본적으로 DataSourceAutoConfiguration을 사용하기 위해,
최소 하나의 datasource가 선언되어 있어야 한다.
하지만 routingDataSource를 사용하게 되면 실질적인 datasource가 생성되기 이전에 아래와 같이
순환 참조가 계속 이루어져 진행이 되지 않는다.
때문에 위와 같이 autoConfiguration에서 해당 DataSourceAutoConfiguration 을 제외하고 진행하여야
아래와 같은 error가 발생하지 않는다.

Description:

The dependencies of some of the beans in the application context form a cycle:

   org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
┌─────┐
|  lazyConnectionDataSourceProxy defined in class path resource [org/blog/test/configuration/datasource/DataSourceConfig.class]
↑     ↓
|  routeDataSource defined in class path resource [org/blog/test/configuration/datasource/DataSourceConfig.class]
↑     ↓
|  masterDataSource defined in class path resource [org/blog/test/configuration/datasource/DataSourceConfig.class]
↑     ↓
|  dataSourceInitializer
└─────┘



가장 먼저, datasource routing을 사용하기 위해 spring boot에서 제공하는 AbstractRoutingDataSource를 사용하여,
아래와 같이 해당 transaction이 readOnly 일때는 slave, 아닌 경우에는 master 라는 값을 리턴하도록 설정한다.

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
            return DbType.SLAVE;
        }
        return DbType.MASTER;
    }
}

이후 해당 리턴 값을 통해 선언된 datasource 중에서 해당값에 해당하는 datasource를 사용하도록 아래와 같이 설정할 수 있다.
먼저, ConfigurationProperties 어노테이션을 이용하여 각각의 datasource 값을 설정하여 bean으로 선언해주도록 한다.
이후, 아래와 같이 routingDataSource 클래스를 통해, 기본 datasource와 각각의 값에 따른 datasource 리턴값을 설정해주도록 한다.

@Configuration
public class DataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "datasource.master")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties(prefix = "datasource.slave")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean
    public DataSource routeDataSource() {
        return new RoutingDataSource() {{
            setDefaultTargetDataSource(masterDataSource());
            setTargetDataSources(new HashMap() {{
                put(DbType.MASTER, masterDataSource());
                put(DbType.SLAVE, slaveDataSource());
            }});
        }};
    }

    @Bean
    @Primary
    public LazyConnectionDataSourceProxy lazyConnectionDataSourceProxy() {
        return new LazyConnectionDataSourceProxy(routeDataSource());
    }
}

하지만, 해당 routeDataSource 선언만으로 service 단의 method 별로 datasource를 라우팅할 수는 없는데,
그 이유는 spring에서는 별다른 설정을 해주지 않은 경우, transaction의 동기화 이전에 connection을 확보하고 진행하기 때문이다.
해당 이슈를 해결하기 위해 LazyConnectionDataSourceProxy로 감싸주어 datasource를 접근하게 되면,
실질적인 transaction의 첫번째 query를 생성하기 바로 전에 datasource를 가져오기 때문에 위와 같이 transaction별로 routing 설정이 가능해진다.


각각의 dataBase table에 master, slave 값을 넣어놓고 테스트하면 아래와 같이 각각 다른 DB에 접근하여 값을 가져오는 것을 확인할 수 있다.

- 수행 결과

2017-05-22 00:18:45.826  INFO 9588 --- [           main] org.blog.test.RoutingApplication         : personService.getPerson : Person(id=1, name=master)
2017-05-22 00:18:45.877  INFO 9588 --- [           main] org.blog.test.RoutingApplication         : personService.getPersonFromSlave : Person(id=1, name=slave)


전체 예제는 아래 gitlab에서 다운로드 받을 수 있다.

https://gitlab.com/shashaka/datasource-routing-project


참조 : http://egloos.zum.com/kwon37xi/v/5364167

댓글 5개 :

  1. 안녕하세요ㅎㅎ
    이렇게 하면 트랜젝션이 제대로 걸려서 롤백되던가요?

    참조하신 링크 보고 저도 해봤었는데
    저는 처음 api call을 통해 db에서 데이터를 가져온 이후
    한 40초~1분 정도가 지나면 커넥션이 끊겨서 db접근이 안되는 현상이 생기더라구요.

    답글삭제
    답글
    1. 남겨주신 답글보고 오랜만에 테스트를 해보았네요~^^
      controller 단만 추가하여 간단한 테스트만 해보아서 커넥션이 끊기는 현상은 따로 발견하지 못하였는데요.
      위 예제로 받아보시고, 안되는 현상이 있으시면 댓글로 남겨주시면 더 테스트해서 블로그에도 내용을 보충할 수 있을 것 같네요~^^

      삭제
  2. 앗 테스트까지 하시다니.. 감사합니다 ㅎㅎ
    내일한번 받아서 트랜잭션 제대로 동작하는지
    이런부분들 테스트해봐야겠어요.

    도움주셔서 감사드립니다! :)

    답글삭제
  3. 안녕하세요~
    혹시 트랜젝션 처리도 잘 되시던가요? 소스상에서는
    트랜젝션 매니저를 따로 선언하는 부분이 없어
    트랜젝션 처리가 안되는게 아닌가 싶네요.

    답글삭제
    답글
    1. 예제로 테스트를 해보니, RuntimeException 이 발생하였을 때 rollback도 잘 되는 것을 확인하였습니다.
      덧붙여서 말씀드리면, transaction의 경우, unchecked excpetion에 대해서만 기본적으로 rollback을 적용합니다. 물론, 설정을 통해 추가적으로 다른 exception에 대해서도 rollback 적용이 가능합니다.

      In its default configuration, the Spring Framework’s transaction infrastructure code only marks a transaction for rollback in the case of runtime, unchecked exceptions; that is, when the thrown exception is an instance or subclass of RuntimeException. ( Errors will also - by default - result in a rollback). Checked exceptions that are thrown from a transactional method do not result in rollback in the default configuration.

      삭제