💻 백엔드

@Transactional(readOnly = true) 트랜잭션 전파 테스트 🚀

twoweekhee 2025. 9. 21. 21:59

안녕하세요! 오늘은 Spring Boot 환경에서 Master-Slave 데이터베이스 구조를 구축하고, 트랜잭션의 Read-Only 속성에 따라 자동으로 데이터베이스를 분기하는 시스템을 만들어본 경험을 공유하려고 합니다. 📊

 

아래 깃헙 계정에서 테스트 가능합니다.

https://github.com/twoweekhee/transaction-test

 

GitHub - twoweekhee/transaction-test: transaction propagation test

transaction propagation test . Contribute to twoweekhee/transaction-test development by creating an account on GitHub.

github.com

📋 목차

  1. 프로젝트 소개
  2. 기술 스택
  3. 개발 환경 구성
  4. Master-Slave 라우팅 구현
  5. 트랜잭션 전파 테스트
  6. 실제 테스트 결과
  7. 마무리

🎯 프로젝트 소개

대용량 트래픽을 처리하는 서비스에서는 읽기와 쓰기를 분리하여 데이터베이스 부하를 분산시키는 것이 중요합니다. 이번 프로젝트에서는 Spring Boot를 사용해서 다음과 같은 기능을 구현해보았어요:

  • 쓰기 작업: Master 데이터베이스로 자동 라우팅 ✍️
  • 읽기 작업: Slave 데이터베이스로 자동 라우팅 📖
  • 트랜잭션 전파: 복합 트랜잭션에서의 동작 검증 🔄

이를 통해 데이터베이스 성능을 최적화하고 시스템의 확장성을 높일 수 있습니다!

🛠 기술 스택

프로젝트에서 사용한 기술들은 다음과 같습니다:

  • Language: Java 21 ☕
  • Framework: Spring Boot 3.x, Spring Data JPA 🌱
  • Database: MySQL 8.0 🗄️
  • Testing: JUnit 5, Testcontainers 🧪
  • Tools: Docker, Lombok 🐳

특히 Testcontainers를 사용해서 실제 MySQL 환경을 컨테이너로 띄워서 테스트할 수 있었는데, 이게 정말 편리했어요!

🚀 개발 환경 구성

Docker 환경 설정

가장 먼저 테스트 환경을 구성해야 했습니다. Testcontainers를 활용하면 별도의 MySQL 설치 없이도 테스트가 가능해요:

# 테스트 실행 명령어
./gradlew test

Docker Desktop만 설치되어 있으면 Testcontainers가 자동으로 Master-Slave MySQL 환경을 구성해줍니다. 정말 간단하죠? 😄

라우팅 로직 구현

트랜잭션의 readOnly 속성을 감지해서 데이터베이스를 분기하는 로직을 구현했습니다:

  • @Transactional(readOnly = false) 또는 @Transactional: Master DB 🏢
  • @Transactional(readOnly = true): Slave DB 🏠

📊 Master-Slave 라우팅 구현

라우팅 데이터소스 구성

TestReplicationRoutingDataSource라는 커스텀 클래스를 만들어서 트랜잭션 컨텍스트를 기반으로 데이터베이스를 선택하도록 구현했어요.

테스트 시나리오

실제로 다음과 같은 단계별 테스트를 진행했습니다:

  1. 컨테이너 상태 확인 🔍
    • Master와 Slave 컨테이너가 정상 실행되는지 체크
  2. 복제 상태 검증 🔄
    • Slave_IO_Running과 Slave_SQL_Running이 모두 Yes인지 확인
    • 실제 복제가 잘 되고 있는지 검증
  3. 쓰기 트랜잭션 테스트 ✏️
    • Master DB로 정상 라우팅되는지 확인
    • 사용자 생성 및 ID 발급 검증
  4. // 예시 코드 (실제 구현은 환경변수로 처리) userService.createUser(userData);
  5. 읽기 트랜잭션 테스트 👁️
    • Slave DB로 정상 라우팅되는지 확인
    • Master에서 생성한 데이터가 Slave에 복제되었는지 검증
  6. // 예시 코드 userService.findAllUsersReadOnly();

🔄 트랜잭션 전파 테스트

가장 흥미로운 부분이었던 트랜잭션 전파 테스트입니다! 하나의 서비스 메소드에서 읽기와 쓰기가 함께 호출될 때 어떻게 동작하는지 테스트해봤어요.

케이스 1: 트랜잭션 없이 호출 🆓

// 외부 트랜잭션이 없는 상태
public void testReplicaToMain() {
    findAllUsersReadOnly(); // Slave DB
    createUser();          // Master DB
}

결과: 각각 독립적인 트랜잭션으로 실행되어 정상 동작! ✅

케이스 2: 외부 트랜잭션 내에서 호출 📦

@Transactional // readOnly = false (기본값)
public void testReplicaToMainWithTransaction() {
    findAllUsersReadOnly(); // Master DB (외부 트랜잭션 따름)
    createUser();          // Master DB
}

결과: 모든 작업이 Master DB에서 처리됨! 🎯

이게 핵심인데요, 내부 메소드가 readOnly=true로 설정되어 있어도 외부 트랜잭션의 속성을 따라가더라고요!

케이스 3: REQUIRES_NEW 전파 속성 🆕

@Transactional
public void testReplicaToMainWithNew() {
    findAllUsersReadOnly();        // Master DB (외부 트랜잭션)
    createUserNew();              // Master DB (새로운 트랜잭션)
}

결과: 새로운 트랜잭션이 생성되지만 여전히 Master DB에서 처리! 🔄

📈 실제 테스트 결과

모든 테스트를 실행한 결과, 다음과 같은 인사이트를 얻을 수 있었습니다:

✅ 성공적인 결과들

  • Master-Slave 복제가 안정적으로 동작
  • readOnly 속성에 따른 라우팅이 정확하게 작동
  • 트랜잭션 전파 시 예상대로 동작

🔍 주요 발견사항

  1. 트랜잭션 전파의 중요성: 외부 트랜잭션이 있으면 내부 메소드의 readOnly 설정이 무시됨
  2. 데이터 일관성: Master에서 쓴 데이터가 Slave로 실시간 복제됨
  3. 성능 최적화: 읽기 작업을 Slave로 분산하여 Master 부하 감소

🎉 마무리

이번 프로젝트를 통해 Master-Slave 환경에서의 트랜잭션 라우팅을 직접 구현하고 테스트해볼 수 있었습니다. 특히 트랜잭션 전파 속성이 라우팅에 미치는 영향을 실제로 확인할 수 있어서 매우 유익했어요! 🌟

핵심 포인트 정리

  • 읽기 작업을 Slave로 분산하여 성능 향상 📈
  • 쓰기 작업은 Master에서 처리하여 데이터 정합성 보장 🔒
  • 트랜잭션 전파 속성을 고려한 설계 필요 ⚠️

이러한 패턴을 실제 운영 환경에 적용하면 대용량 트래픽 처리 시 큰 도움이 될 것 같습니다!


Tags: #Spring Boot #Master Slave #Database #Transaction #JPA #MySQL #Performance #Testcontainers #Java #읽기분산 #데이터베이스최적화 #트랜잭션전파 #@Transactional(readOnly = true) #@Transactional

@Transactional(readOnly = true)