지난번 포스팅에서는 Master/Slave DB를 사용하기 위해 어떻게 세팅을 하는지에 대해 다뤘다. 이러한 세팅을 적용하면서 궁금증이 생겼다. 세팅을 하기 위해 자료를 찾은 것과 그동안 공부했던 것들을 바탕으로 대충은 어떻게 동작하는지는 알게되었으나 더 자세히 알고 싶어졌다. 그래서 이번 글에서는 이 세팅이 어떻게 동작하는지 알아보기위해 코드를 훑어보는 시간을 가져보려고 한다.
개인 공부 용으로 남겨 놓는거라 복잡합니다. 핵심 부분은 LazyConnectionDataSourceProxy에 있으니 그 부분만 보시는 것을 추천합니다.
실행할 로직은 간단한 조회 API 입니다.
@RestController
public class AppApi {
private final AppService appService;
public StoreAppApi(appService appService) {
this.appService = appService;
}
@GetMapping("/apps")
public ApiResponse<?> getApps() {
return ApiResponse.of("00", "성공", appService.getAllApps());
}
}
@Service
public class AppService {
private final AppRepository appRepository;
public AppService(AppRepository appRepository) {
this.appRepository = appRepository;
}
@Transactional(readOnly = true)
public AppListResponse getAllApps() {
List<App> apps = appRepository.findAll();
return AppListResponse.of(apps.stream()
.map(App::getAppName)
.collect(Collectors.joining(","))) ;
}
}
public interface AppRepository extends Repository<App, Long> {
List<App> findAll();
}
이전 글에서의 세팅을 적용한 상태다.. readOnly=ture 를 적용했으니 최종적으로 lazyConnectionDataSourceProxy 를 통해 slave DB 를 사용해야 한다.
처음에 어플리케이션을 실행하면 스프링 빈을 생성한다. RoutingDataSource 를 생성하고 LazyConnectionDataSourceProxy 를 생성한다. @DependsOn 어노테이션 때문에 RoutingDataSource 를 생성하고 LazyConnectionDataSourceProxy 를 생성하는 것이다. 그다음 EntityManagerFactory 와 TransactionManager 를 생성한다. 이제 API 를 호출해보겠다.
트랜잭션 생성
컨트롤러를 통해 @Transactional 이 적용된 서비스 메서드를 호출한다. 그러면 CglibAopProxy 를 통해 트랜잭션 생성을 시작한다. TransactionInterceptor -> TransactionAspectSupport#invokeWithinTransaction 메서드를 를 호출하게 된다.
이 메서드 에서 트랜잭션의 속성을 얻고 트랜잭션 매니저를 결정한다. 트랜잭션 속성은 @Transactional 에 정의된 속성이다. 이 트랜잭션에서는 PROPAGATION_REQUIRED, ISOLATION_DEFAULT, readOnly 속성이 있다.
determineTransactionManager 에서는 기존의 트랜잭션 매니저를 얻거나 캐시에 있는 트랜잭션 매니저를 얻어온다. 하지만 기존에 사용하는 트랜잭션이 없기 때문에 beanFactory 를 통해 만들어진 트랜잭션 매니저 빈을 가져온다.
이제 TransactionalAspectSupport 로 돌아오면 트랜잭션을 만드는 로직을 볼 수 있다.createTransactionIfNecessary 메서드로 들어가면 트랜잭션 매니저를 통해 트랜잭션을 얻는 로직이 있다. tm.getTransaction 을 호출하면 PlatformTransactionManager 구현체인 AbstractPlatformTransactionManager#getTransaction 을 호출하게 된다.
AbstractPlatformTransactionManager#getTransaction
여기서 doGetTransaction() 은 추상메서드이다. 이 메서드를 호출하면 트랜잭션 매니저 빈을 정의할 때 사용한 JpaTransactionManager의 메서드를 호출하게 된다.
트랜잭션 동기화 매니저를 통해 EntityManagerHolder, ConnectionHolder 를 얻는다. 이 메서드에서는 둘다 null을 얻게 된다. 첫번째 트랜잭션이기 때문에 등록된게 없어 그런것 같다. 참고로 getDataSource() 메서드를 통해 등록된 DataSource 가 있는지 확인하는데, 여기서는 LazyConnectionDataSource 가 등록되어 있다. 이제 AbstractPlatformTransactionManager 로 돌아가 다음 로직을 보자.
isExistingTransaction 은 기존에 트랜잭션이 있는지 확인한다.
내부 로직을 확인해보면 JpaTransactionManager#hasTransaction 에 entityManagerHolder 를 확인하는데 현재 사용중인 entityManageHolder 가 없기 때문에 isExistingTransaction 은 false 가 나오게 된다. 첫번째 트랜잭션 생성 로직을 수행한 이후의 트랜잭션을 마주하게 되면 true 로 나와 해당 로직을 수행하게 되는 것 같다.
그 다음에는 PROPAGATION_MANDATORY 면 예외를 일으키는 로직이 있다. PROPAGATION_MANDATORY 는 이전 트랜잭션이 존재하면 그 이전 트랜잭션을 사용하는 옵션이다. 그리고 이전 트랜잭션은 반드시 존재해야만 한다. 만약 PROPAGATION_MANDATORY 설정이고 이전 트랜잭션이 없는 첫번째 트랜잭션이면 예외가 발생한다. 그리고 이제 AbstractPlatformTransactionManager 의 startTransaction을 실행해보자.
AbstractPlatformTransactionManager#startTransaction
기존에 활성화된 트랜잭션 매니저가 없으니 newSynchronization 은 true 이다. doBegin은 추상메서드다. 이 메서드를 호출하면 JpaTransactionManager#doBegin 이 실행된다.
createEntityManagerForTransaction 메서드를 호출하면 엔티티 매니저를 만든다.
createNativeEntityManager 메서드를 호출하면 위와 같이 SessionImpl 이 생성된다. 생성된 newEm 엔티티 매니저 객체를 EntityManagerHolder 로 생성해 트랜잭션에 set 한다.
새로 생성된 엔티티 매니저를 사용해 getJpaDialect().beginTransaction 을 호출하자. Hibernate를 사용했으니 HibernateJpaDialect #beginTransaction 을 호출한다.
DataSourceUtils.prepareConnectionForTransaction 메서드에서 @Transactional 에 정의된 속성들을 이용해 커넥션에 세팅을 한다.
그리고 begin() 메서드를 호출해 코드를 추적하다 보면 AbstractLogicalConnectionImplementor#begin() 메서드를 호출해 트랜잭션의 상태를 ACTIVE 로 변경한다. 이제 JpaTransactionManager 로 돌아오면 아래와 같은 로직을 만나게 된다.
트랜잭션 리소스 등록
jpaDialect 의 JdbcConnection 을 호출하면 HibernateJpaDialect#getJdbcConnection 을 호출하게 된다. Connection을 가져와 ConnectionHolder 를 만들고 트랜잭션 객체에 ConnectionHolder 를 설정한다. TransactionSynchronizationManager 의 bindResource 메서드는 key, value 를 파라미터로 받아 리소스로 등록하는 메서드다.
위에서는 DataSource를 key 로 ConnectionHolder value 로 저장했고, 그다음에는 EntityManagerFactory 를 key, EntityManagerHolder 를 value 로 저장한다. 그리고 트랜잭션의 EntityManagerHolder 를 가져와 싱크를 true 로 설정하는 로직이 있다. 여기까지가 AbstractPlatformTransactionManager#startTransaction 메서드의 끝이다. 이후엔 TransactionAspectSupport -> TransactionInterceptor -> CglibAopProxy 로 돌아와 AOP 로 감싸고 있는 비즈니스 로직을 실행하게 된다.
AppRepository.findAll()
AppRepository는 Repository 를 구현한 인터페이스다. 모든 APP 을 조회하는 로직이고 해당 로직을 실행하면 위에서 트랜잭션을 실행할 때와 같이 CglibAopProxy -> TransactionInterceptor -> TransactionAspectSupport#invokeWithinTransaction 이 호출된다.
위에서 호출 했을 땐 transactionManagerBean 이 값이 없어서 아래의 로직을 실행했는데, 여기서는 트랜잭션이 실행된 후니까 determineQualifiedTransactionalManager() 를 호출하게 된다.
이전에 생성한 트랜잭션 매니저가 캐쉬에는 등록되지 않았으니 txManager 는 null 을 가진다. 여기서 transactionManagerCache 에 트랜잭션 매니저를 저장한다. 이후에 createTransactionIfNecessary 를 호출해 트랜잭션 매니저의 getTransaction 을 호출한다. 그러면 구현체인 AbstractPlatformTransactionManager 의 getTransaction 을 호출하게 된다. getTransaction() 에서 doGetTransaction 메서드를 호출하면 아래와 같은 코드가 있다.
트랜잭션을 생성할 땐 emHolder, conHolder 가 null 이었지만 여기서는 아니다. 위에 트랜잭션 리소스 등록을 보면 TransactionSynchronizationManager 를 통해 리소스를 등록하는 부분이 있었다. 이 로직은 그 부분에서 등록한 엔티티 매니저 홀더와 커넥션 홀더를 가져와 트랜잭션 객체에 설정해주는 로직이다.
트랜잭션을 생성할 땐 isExistionTransaction 이 false 라 이 로직을 안탔지만, 이번에는 트랜잭션이 존재하기 때문에 이 로직을 타게 됐다.
handlerExistingTransaction 에서는 트랜잭션에 설정된 속성에 따른 처리를 하고 마지막에 TransactionSynchronizationManager 의 값을 설정하고 TransactionStatus 를 반환한다. 이제 TransactionAspectSupport 의 createTransactionIfNecessary 에 있는 prepareTransactionInfo 메서드를 실행한다.
새로운 트랜잭션 객체를 만들고 TransactionStatus 를 설정해준다. 이제 트랜잭션 세팅이 끝났으니 쿼리를 실행하는 로직으로 넘어간다. QueryExecutorMethodInterceptor#invoke -> QueryExecutorResultHandler#postProcessInvocationResult -> RepositoryFactorySupport#invoke 를 호출한다. 그 이후로도 쿼리와 관련된 메서드가 호출되는 것을 확인할 수 있다. RepositoryCompository -> RepositoryMethodInvoker -> SimpleJpaRepository -> QueryTranslatorImpl -> QueryLoader -> Loader -> StatementPrepareImpl 호출이 된다. 이제야 이 훑어보기 글의 목적이 되는 부분이 나온다.
LazyConnectionDataSourceProxy
connection 을 얻어 쿼리를 실행하는 부분이다. connection 을 얻어오는 부분을 타고 들어가면 LazyConnectionDataSourceProxy 가 나온다.
DataSource 를 얻고 커넥션을 얻어 오는 부분의 로직을 보면 익숙한 메서드가 보인다. 바로 determineTargetDataSource 이다. RoutingDataSourceRouter 을 정의할 때 AbstractRoutingDataSource 를 상속받아 오버라이딩한 메서드다.
트랜잭션에 readOnly 가 있으니 slave 를 반환한다.
반환받은 slave 를 사용해 resolvedDataSources 에서 데이터 소스를 조회한다.
이 코드에서 target 은 위에서 조회한 DataSource 를 통해 가져온 Connection 이다. Connection 에 readOnly, isolation 등을 설정한다.
Connection 을 사용해 쿼리를 호출하고 CrudMethodMetadataPostProcessor#invoke 를 호출하고 TransactionSynchronizationManager#unbindResource 를 호출한다. bindResource 를 통해 리소스에 등록한 데이터를 제거하는 로직이다. appRepository#findAll 메서드 호출이 끝난 이후에 서비스로 돌아온 다음, 트랜잭션이 끝날 때 cleanupTransactionInfo, commit 을 호출하며 마무리 한다.