SpringBoot Master/Slave DB 세팅 2

2024. 3. 3. 16:03·Spring

지난번 포스팅에서는 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 을 호출하며 마무리 한다.

 

 

 

반응형
'Spring' 카테고리의 다른 글
  • SpringBoot Master/Slave DB 세팅
  • OAS 사용해 API 문서 작성하기
  • Resilience4jFeign Java Record 문제
  • Tomcat 실행시 스프링 내부 동작과정
Jadie Blog
Jadie Blog
  • Jadie Blog
    Jadie
    Jadie Blog
  • 전체
    오늘
    어제
    • 분류 전체보기 (44)
      • OOP (7)
      • DDD (1)
      • JAVA (8)
      • Spring (12)
      • Kafka (1)
      • TDD,Test (4)
      • Basic (1)
      • ETC (1)
      • MySQL (0)
      • Javascript (0)
      • Spark (3)
      • Infra (2)
      • Algorithm (0)
      • Network (1)
      • Jobs (0)
      • 일상 (0)
  • 블로그 메뉴

    • 홈
    • 태그
    • 미디어로그
    • 위치로그
    • 방명록
  • 링크

    • 휴튼
  • 공지사항

  • 인기 글

  • 태그

    OAuth2 #Spring
    Spring
    메시징시스템
    localdatetime
    JPQL
    Transactional Outbox
    우아한스터디
    entitymanager
    HTTP #HTTPS
    Spring #ApplicationContext
    JAVA #IO
    java
    routingdatasource
    MASTER
    테스트
    캡슐화
    객체지향
    Kafka
    jpa
    slave
    API문서
    객체지향사실과오해
    추상클래스 #인터페이스
    의존역전원칙
    Resilience4jFeign
    OOP
    Test
    글또
    MSA
    springboot
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.1
Jadie Blog
SpringBoot Master/Slave DB 세팅 2
상단으로

티스토리툴바