안녕하세요! 오늘은 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
📋 목차
🎯 프로젝트 소개
대용량 트래픽을 처리하는 서비스에서는 읽기와 쓰기를 분리하여 데이터베이스 부하를 분산시키는 것이 중요합니다. 이번 프로젝트에서는 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라는 커스텀 클래스를 만들어서 트랜잭션 컨텍스트를 기반으로 데이터베이스를 선택하도록 구현했어요.
테스트 시나리오
실제로 다음과 같은 단계별 테스트를 진행했습니다:
- 컨테이너 상태 확인 🔍
- Master와 Slave 컨테이너가 정상 실행되는지 체크
- 복제 상태 검증 🔄
- Slave_IO_Running과 Slave_SQL_Running이 모두 Yes인지 확인
- 실제 복제가 잘 되고 있는지 검증
- 쓰기 트랜잭션 테스트 ✏️
- Master DB로 정상 라우팅되는지 확인
- 사용자 생성 및 ID 발급 검증
- // 예시 코드 (실제 구현은 환경변수로 처리) userService.createUser(userData);
- 읽기 트랜잭션 테스트 👁️
- Slave DB로 정상 라우팅되는지 확인
- Master에서 생성한 데이터가 Slave에 복제되었는지 검증
- // 예시 코드 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 속성에 따른 라우팅이 정확하게 작동
- 트랜잭션 전파 시 예상대로 동작
🔍 주요 발견사항
- 트랜잭션 전파의 중요성: 외부 트랜잭션이 있으면 내부 메소드의 readOnly 설정이 무시됨
- 데이터 일관성: Master에서 쓴 데이터가 Slave로 실시간 복제됨
- 성능 최적화: 읽기 작업을 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)
'💻 백엔드' 카테고리의 다른 글
| JDK 21 : 가상쓰레드(Virtual Thread) Deep dive 해보자! - 2편 (0) | 2025.12.16 |
|---|---|
| JDK 21 : 가상쓰레드(Virtual Thread) Deep dive 해보자! - 1편 (1) | 2025.12.15 |
| Python FastAPI에서 로거 설정하기 🚀 (1) | 2025.08.28 |
| 🔍 Spring Filter, Interceptor, AOP - 언제 뭘 써야 할까? (1) | 2025.06.14 |
| 🐬 MySQL Replication 구축하기: Docker로 간단하게 시작하는 데이터베이스 복제 (0) | 2025.06.12 |