Programing/Framework

[Spring] 중첩된 @Transactional의 readOnly 동작 확인

나모찾기 2024. 10. 26. 23:36

배경

이번 주 진행했던 작업 중에 @Transactional(readOnly = true)가 붙은 리포지토리 메서드에서 데이터를 읽지 못하는 현상이 있어서 디버깅을 했다.

가설

가장 먼저 떠오른 원인은 Writer 인스턴스와 Reader 인스턴스간의 복제 지연(Replication Rag)이었다.

 

이전 회사에서 복제 지연으로 조회 데이터가 없었던 경험이 있었기 때문이다.
당시 주문 도메인을 담당하고 있었다. 주문이 만들어지고 나서 메시지 큐로 이벤트를 보내는 역할이었다. 마침 큐를 소비(consume)하는 클라이언트가 API를 통해 주문 데이터를 조회를 하고 있었다. 문제는 API가 데이터 응답을 할 때 사용하는 DB는 Reader 인스턴스에서 읽도록 구현이 되어 있었다. 당시에는 이런 상황을 위해 Writer 인스턴스에서 읽을 수 있게 API를 제공하는 방법으로 대응을 했다.

 

가설 검증 준비

개발 환경의 Aurora Cluster에는 비용 절감을 이유로 운영과 달리 Writer 인스턴스만 있는 구조였다.
따라서 @Transactional(readOnly = true)를 통해 읽기 인스턴스 지정이 있어도 Writer 인스턴스로 연결이 이루어지고 있었다.
따라서 정확한 재현을 위해 Reader 인스턴스를 임시적으로 추가했다.

현재 트랜잭션의 Reader / Writer 인스턴스 확인 방법

현재 연결된 인스턴스가 어떤 것인지 확인하는 방법은 여러 가지가 있겠지만 아래 쿼리로 확인을 했다.

select @@innodb_read_only;

해당 프로젝트에서 Spring Data JPA를 사용하고 있어서 JdbcTemplate을 이용해서 쿼리를 수행하도록 해서 확인을 했다.

JdbcTemplate 은 주입을 받고 queryForObject 를 수행하면 0 또는 1을 반환한다. 0이면 Writer 인스턴스이고 1이면 Reader 인스턴스이다.

jdbcTemplate.queryForObject("select @@innodb_read_only;", Int::class.java)

 

가설 확인 결과

디버그를 걸어서 확인을 해보니 문제가 되었던 호출의 경우 애플리케이션 서비스 Layer에서 트랜잭션이 걸려있지 않았다.

따라서 문제가 되던 읽기 부분만 Reader Instance로 연결이 되는 것을 확인했다.

 

추가적으로 궁금했던 부분에 대한 검증을 진행했다.

선언적 트랜잭션의 경우 내부에 중첩적으로 사용할 수 있는데 밖에서는 readOnly이 false이고 중첩되는 다른 부분에 true로 걸려있는 경우에 어떻게 동작하는지였다.

예)

@Service
class ApplicationService(
  private val domainService: DomainService,
) {
  @Transactional(readOnly=false) // default가 false라서 readOnly 속성은 생략이 가능
  fun somethingUseCase() {
    // .. create operation
    domainService.somethingRead(id = ...)
  }
}

@Component
class DomainService(
  private val domainRepository: DomainRepository,
) {
 @Transactional(readOnly=true)  // propagation 속성의 기본값은 Propagation.REQUIRED
 fun somethingRead(id: Long): DomainDto {
   // .. read from reader instance hopefully
   // return value...
 }
}

 

위와 같은 경우에 domainService.somethingRead는 Reader 인스턴스에서 읽기를 희망했지만

호출하는 상위 트랜잭션이 Writer 인스턴스를 희망했기에 Reader 인스턴스가 아닌 Writer 인스턴스의 연결을 사용하는 것으로 확인했다.

propagation의 기본 값이 Propagation.REQUIRED 이므로 기존에 트랜잭션이 있는 경우에는 새로 트랜잭션을 시작하지 않기 때문이라 생각할 수 있다.

Support a current transaction; create a new one if none exists.
Analogous to the EJB transaction attribute of the same name.