서론
데이터베이스를 이용한다면 대부분 쓰기보다 읽기 의 행위가 더 많습니다.
DB의 부하를 줄이기 위해 다음과 같이 Master - Slave 구조를 많이 사용하는데요.
이러한 구조를 가지고 있을 때 Transection의 속성이 readOnly = true
인 경우
Slave 데이터베이스에 Select query 가 발생하게 해야합니다.
따라서 이번 본문에서는
@Transactional(readOnly = true)
인 경우는 Slave DB 접근@Transactional(readOnly = false)
인 경우에는 Master DB 접근
위의 조건을 만족하기 위한 방법을 Spring-boot 기준으로 소개하고자 합니다.
사전 환경설정
실습 환경은 다른 것은 필요 없고 저는 MySQL 데이터베이스를 이용하였고
Master - Slave 구조가 준비되었다고 가정합니다.
저는 Docker를 이용하였습니다.
- Master DB : 3306 PORT
- Slave DB - 3307 PORT
저와 같은 환경을 설정하고 싶으신 분 Github를 참고해주세요.
본문
application.yml 설정
먼저 DB에 접근하기 위해 application.yml
에 접근 정보를 정의해보겠습니다.
spring:
datasource:
master:
hikari:
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3306/examreplication
slave:
hikari:
username: root
password: password
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://localhost:3307/examreplication
현재 Master, Slave DB를 2 개를 사용해야 하니 Datasource를 직접 생성해야 합니다.
따라서 해당 접근 정보들을 이용하여 DataSource
을 만들어야 하니 정확히 입력해주세요!
DataSource Bean 등록하기
일단 readOnly
속성 별로 분기를 하기 전에 앞서 설정한 정보로 DataSource를 Bean으로 등록하는 과정입니다.
@Configuration
public class DataSourceConfiguration {
public static final String MASTER_DATASOURCE = "masterDataSource";
public static final String SLAVE_DATASOURCE = "slaveDataSource";
@Bean(MASTER_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.master.hikari") // (1)
public DataSource masterDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
@Bean(SLAVE_DATASOURCE)
@ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
public DataSource slaveDataSource() {
return DataSourceBuilder.create()
.type(HikariDataSource.class)
.build();
}
}
1. spring.datasource.master.hikari
에 해당하는 property를 DataSource를 생성하는데 이용.
여기서 1번 과정이 잘 이해가 안 되신다면 해당 자료를 참고해주세요.
AbstractRoutingDataSource 구현
AbstractRoutingDataSource.class
는 조회 key 기반으로 등록된 Datasource 중 하나를 호출을 하게 해 줍니다.
말이 거창하긴 한데 소스코드를 보면 매우 간단합니다.
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() { // (1)
return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master"; //(2)
}
}
-
determineCurrentLookupKey()
메서드는 현재 조회 키를 반환받기 위해 구현해야 하는 추상 메서드입니다. - 따라서 저희는 readOnly 속성을 구별하여 key를 반환하게 합니다.
AbstractRoutingDataSource Bean 등록하기
@Bean
@Primary
@DependsOn({MASTER_DATASOURCE, SLAVE_DATASOURCE})
public DataSource routingDataSource(
@Qualifier(MASTER_DATASOURCE) DataSource masterDataSource,
@Qualifier(SLAVE_DATASOURCE) DataSource slaveDataSource) {
RoutingDataSource routingDataSource = new RoutingDataSource();
Map<Object, Object> datasourceMap = new HashMap<>() {
{
put("master", masterDataSource);
put("slave", slaveDataSource);
}
};
routingDataSource.setTargetDataSources(datasourceMap);
routingDataSource.setDefaultTargetDataSource(masterDataSource);
return routingDataSource;
}
routingDataSource Bean으로 생성하여 @Primary를 선언하여 DataSource 타입에 바인딩되는 객체로 정의합니다.
꼭! 궁금한 부분까지 읽어주세요 ! 아직 설정해야하는 부분이있습니다.
결과 확인
@Service
@RequiredArgsConstructor
@Transactional
public class BoardService {
private final BoardRepository boardRepository;
private final DataSource dataSource;
@Transactional(readOnly = true)
public List<Board> getBoardList(){
return boardRepository.findAll();
}
public List<Board> updateTitle() {
List<Board> boards = boardRepository.findAll();
for (Board board : boards) {
board.setTitle("newTitle");
}
return getBoardList();
}
}
이러한 서비스 코드가 존재할 때
getBoardList()
는 readOnly = ture
임으로 -> jdbc:mysql://localhost:3307 으로
updateTitle()
는 readOnly =false
임으로 -> jdbc:mysql://localhost:3306 으로
-> 여기서 중요한 점은 method의 시작 트랜잭션이 읽기 전용이 아님으로 내부에서 getBoardList()
을 호출하였을 때 Slave DB에 쿼리가 발생하는 것이 아닌 Master DB로 query 가 발생합니다.
궁금한 점🧐??!!
해당 주제를 공부하면서 많은 예제와 블로그를 찾아본 결과
@Bean
@DependsOn("routingDataSource")
public LazyConnectionDataSourceProxy dataSource(DataSource routingDataSource){
return new LazyConnectionDataSourceProxy(routingDataSource);
}
해당 예제처럼 AbstractRoutingDataSource를
LazyConnectionDataSourceProxy로 한번 감싸는 예제들이 많았다.
이렇게 감싸는 이유로는 다음과 같이 설명하고 있다.(link)
TransactionManager 선별 -> DataSource에서 Connection 획득 -> Transaction 동기화(Synchronization)
여기서 보면 트랜잭션 동기화를 마친 뒤에 ReplicationRoutingDataSource.java에서 커넥션을 획득해야만 이게 올바로 동작하는데 그 순서가 뒤바뀌어 있기 때문이다.
따라서 LazyConnectionDataSourceProxy 한번 감싸서 다음과 같이 작동하게한다.
TransactionManager -> LazyConnectionDataSourceProxy에서 Connection Proxy 객체 획득 -> Transaction 동기화
실제 쿼리 호출시에 ReplicationRoutingDataSource.getConnection()/determineCurrentLookupKey() 호출
잘못된 테스트 😅
그래서 정말 그런가 싶어서 LazyConnectionDataSourceProxy 감싸지 않고 AbstractRoutingDataSource 을 빈으로 등록 사용하였는데
readOnly = ture/false
속성에 따라 Master / Slave DB에 원하는 대로 잘 쿼리가 분배가 된다.
따라서 제가 이해하지 못하여 이렇게나마 소개하고자 합니다.
추 후 LazyConnectionDataSourceProxy에 대해 파악하여 본문에 추가하도록 하겠습니다.
+++ 추가
LazyConnectionDataSourceProxy 으로 한번 감싸지 않고 사용할시 앞서 설명하고 있는것처럼
AbstractRoutingDataSource 로 사용할시 모든 쿼리가 Master DB 로 가는 현상이 있었다.
따라서 Transaction 동기화 시점에 커넥션을 획득하기 위해 LazyConnectionDataSourceProxy 로 한번감싸주어야 한다.
긴 글 읽어주셔서 감사합니다.
부족한 내용이나 틀린 내용이 있다면 댓글로 남겨주세요! 🙏
참고자료
'Spring > boot' 카테고리의 다른 글
[Spring boot] 여러 필드를 검사하기위한 Custom Valid Annotation 만들기 (0) | 2021.06.25 |
---|---|
[Spring-boot] ExceptionHandler (0) | 2021.01.22 |
[Spring-boot] Thymeleaf (0) | 2021.01.21 |
[Spring-boot] 웹MVC 2부 : 정적리소스 (0) | 2021.01.19 |
[Spring-boot] 웹MVC (1부) : 소개 , ViewResolver (0) | 2021.01.19 |