0. 들어가기 전에
LazyConnectionDataSourceProxy
을 MySQL Replication 할 때 사용했는데, 어떤 원리로 동작하는지 알아보기 위해 글을 작성했습니다.
1. LazyConnectionDataSourceProxy란?
LazyConnectionDataSourceProxy는 JDBC 커낵션이 실제 필요한 시점에 커넥션을 가져오도록하는 프록시 객체이다.
예시 코드로 살펴보자.
@Service
@Transactional
public class LazyService {
private final DataSource dataSource;
public void lazy() {
HikariPoolMXBean hikariPool;
if (dataSource instanceof LazyConnectionDataSourceProxy) {
hikariPool = ((HikariDataSource) ((LazyConnectionDataSourceProxy) dataSource).getTargetDataSource()).getHikariPoolMXBean();
} else {
hikariPool = ((HikariDataSource) dataSource).getHikariPoolMXBean();
}
log.info("TotalConnection = {}", hikariPool.getTotalConnections());
log.info("ActiveConnection = {}", hikariPool.getActiveConnections());
log.info("IdleConnection = {}", hikariPool.getIdleConnections());
}
@Component
public class LazyProxy {
@Bean
public DataSource lazyDataSource(final DataSourceProperties dataSourceProperties) {
final HikariConfig hikariConfig = new HikariConfig();
hikariConfig.setJdbcUrl(dataSourceProperties.getUrl());
hikariConfig.setDriverClassName(dataSourceProperties.getDriverClassName());
hikariConfig.setUsername(dataSourceProperties.getUsername());
hikariConfig.setPassword(dataSourceProperties.getPassword());
return new LazyConnectionDataSourceProxy(new HikariDataSource(hikariConfig));
}
서비스 코드는 실제 DB에 쿼리를 날리지 않고, Connection의 수의 변화를 확인하는 코드이다. 여기서 중요한 것은 실제로 DB와 통신하는 부분이 없다는 점이다.
LazyProxy
는 LazyConnectionDataSourceProxy
를 빈으로 등록하는 코드이다.LazyProxy
를 통해 Proxy를 사용했을 때와 사용하지 않았을 때 어떻게 Connection이 변경되는지 확인해보자.
1.1 동작
1.1.1 LazyConnectionDataSourceProxy
를 사용하지 않았을 때
DB에 쿼리를 날리지 않음에도 Connection을 하나 사용하는 걸 볼 수 있다.
1.1.2 LazyConnectionDataSourceProxy
를 사용했을 때
DB에 쿼리를 날리지 않기 때문에 Connection이 필요한 시점까지 Connection을 가져오지 않는 모습을 볼 수 있다.
1.1.3 @Transactional
이 없을 때
만약 @Transactional
이 없으면 어떻게 될까? 잠깐 고민해보자.
정답은 모두 커넥션을 가져오지 않는다.
이다.
왜냐하면 Transaction이 시작해야 Connection을 가져오기 때문이다. 너무 당연한 말이다. 그렇다면 Transaction이 시작하고 어느 시점에서 Connection을 가져오는걸까?
2. Connection을 가져오는 타이밍
Transaction은 대략적으로 위와 같이 동작한다. 비즈니스 로직을 다루는 순서는 생략했다.
트랜잭션이 적용되어 있는 객체의 CGLIB 프록시가 호출이 되면 트랜잭션이 시작된다. 그 후 트랜잭션의 commit이나 rollback등을 관리하는 TransactionManager
인터페이스를 가져오고 DataSource를 통해 Connection을 생성한 뒤에 Transaction Synchronization Manager
를 통해 트랜잭션을 유지할 수 있게 같은 커넥션을 사용하도록 보장한다.
이런 방식으로 동작하기 때문에 @Transactional
메서드가 없으면 Connection을 가지고 오지 않는다.
만약 LazyConnectionDataSourceProxy
를 사용한다면 DB 접근하기 전 단계(쿼리를 날리기 직전)에 Connection을 가지고 오게 된다.
cf) @Transactional(readonly=true)
여도 con.setReadOnly(true)
를 설정하는 트랜잭션 동작이기 때문에 Transaction으로 동작한다.
3. Replication에서 LazyConnectionDataSourceProxy의 역할
기본 동작을 알아봤으니, DB Replication에서의 LazyConnectionDataSourceProxy의 역할을 생각해보자.
이전 글에서
public class RoutingDataSource extends AbstractRoutingDataSource {
public static RoutingDataSource createDefaultSetting(final Map<Object, Object> dataSources) {
final RoutingDataSource routingDataSource = new RoutingDataSource();
routingDataSource.setDefaultTargetDataSource(dataSources.get(DataSourceType.SOURCE));
routingDataSource.setTargetDataSources(dataSources);
return routingDataSource;
}
@Override
protected Object determineCurrentLookupKey() {
final boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if (readOnly) {
return DataSourceType.REPLICA;
}
return DataSourceType.SOURCE;
}
}
RoutingDataSource
의 determineCurrentLookupKey()
메서드를 사용해서 DataSource를 찾는다고 했다. 그렇다면 LazyConnectionDataSourceProxy
를 설정안하고 RoutingDataSource
만 사용해서 찾을 수 있을까? 아니다.
final boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
이 메서드를 보면 TransactionSynchronizationManager
를 통해서 readonly 여부를 확인하고 있다. 위에서 살펴봤듯 기본적으로 Connection을 TransactionManager
에서 TransactionSynchronizationManager
으로 넘어갈 때 가져온다. 그렇기에 LazyConnectionDataSourceProxy
를 사용하지 않고서는 readonly 여부에 따라 다른 DataSource를 선택할 수 없게 된다.
4. 결론
결론적으로 LazyConnectionDataSourceProxy
를 통해 Connection을 가져오는 동작을 쿼리가 필요한 시점으로 미루기 때문에 TransactionSynchronizationManager
를 통해 readonly 여부를 파악해 알맞은 DataSource로 라우팅을 해줄 수 있다.
'FRAMEWORK > [SPRING]' 카테고리의 다른 글
[Spring] Spring Rest docs 적용해보기 (1) | 2023.05.21 |
---|---|
[Spring] HandlerMethodArgumentResolver 동작 원리 (0) | 2023.05.07 |
[Spring] DispatcherServlet이란? (0) | 2023.04.27 |
[Spring] Spring에서 Bean은 어떤 자료구조로 관리될까요? (0) | 2023.04.22 |
[Spring] Spring에서 DI하는 3가지 방법 (1) | 2023.04.21 |