1. 배경
우리 서비스는 RW(Read-Write) / RO(Read-Only) DB를 분리 운영하고 있으며,
Spring의 AbstractRoutingDataSource를 상속한 Custom RoutingDataSource를 통해
트랜잭션 readOnly 여부에 따라 사용하는 DB를 분기하도록 구성되어 있다.
구조는 아래와 같다.
- AbstractRoutingDataSource 내부의 targetDataSources(Map)
- RW → Master DB
- RO → Slave DB
- determineCurrentLookupKey() 오버라이드
- TransactionSynchronizationManager.isCurrentTransactionReadOnly() 값에 따라
"RW" 또는 "RO" key 반환
- TransactionSynchronizationManager.isCurrentTransactionReadOnly() 값에 따라
즉, @Transactional(readOnly = true) 가 선언된 메서드는
Slave(RO) DB Connection 을 사용해야 한다.
2. 문제 발생
하지만 특정 메서드에서 @Transactional(readOnly = true) 를 선언했음에도
RW DB Connection을 물고 오는 증상이 발생했다.
즉, 분기 로직이 동작하지 않고 있었다.
3. 분석 과정
문제를 좁혀가기 위해 다음을 하나씩 확인했다.
✔️ 1) TransactionManager 내부의 DataSource 확인
PlatformTransactionManager 내부에는
RW / RO DataSource 모두 정상적으로 주입되어 있음을 확인했다.
✔️ 2) 실제 트랜잭션 ReadOnly 옵션 확인
메서드 내부에서 다음 코드를 실행해보면:
→ 결과는 true
즉, 트랜잭션 자체는 readOnly 로 생성되고 있었다.
✔️ 3) RoutingDataSource에서 확인
의심 지점은 여기였다.
AbstractRoutingDataSource → determineTargetDataSource() → determineCurrentLookupKey()
이 메서드 안에서 다시 아래를 찍어보았다.
→ 결과는 false
바로 여기서 RW DB를 선택해버리고 있었음.
같은 트랜잭션인데, 왜 시점에 따라 readOnly 값이 다르게 나올까?
4. 원인 파악: Connection 획득 시점이 문제였다
Spring Transaction 흐름을 따라가며 실제 코드를 분석했다.
핵심은 Connection을 언제 획득하느냐이다.
트랜잭션 시작 흐름 (중요 부분만 발췌)
즉,
❗ Spring은 트랜잭션 동기화(readOnly 등)를 준비하기 전에
이미 DataSource에서 Connection을 가져온다.
그리고 Connection을 가져오는 순간,
RoutingDataSource의 determineCurrentLookupKey() 가 호출된다.
그러니 이 시점에서는 아직:
- readOnly = true 로 설정되지 않았고
- TransactionSynchronizationManager 의 readOnly 값도 false
➡️ 그래서 RoutingDataSource 는 항상 RW DB를 선택하고 있었던 것.
5. 해결 전략
Connection을 가져오기 전에
트랜잭션의 readOnly 속성을 먼저 등록하도록 우선순위를 바꿔주면 된다.
Spring의 기본 DataSourceTransactionManager에서는 이를 조절할 수 없으므로
커스텀 TransactionManager를 만들었다.
6. 해결: Custom TransactionManager 적용
📌 RoutingDataSourceTransactionManager
즉,
- Transaction의 이름·readOnly·Isolation을
Connection 획득 전에 먼저 세팅 - 그 이후 실제 super.doBegin() 호출
→ RoutingDataSource가 determineCurrentLookupKey() 를 호출하기 전에
readOnly 상태가 완전히 반영되므로 정상적으로 RO DB를 선택한다.
7. 적용 후 결과
- @Transactional(readOnly = true) 메서드는 정상적으로 Slave(RO) DB Connection 사용
- 기존 RoutingDataSource 로직 유지
- Spring 기본 흐름을 크게 해치지 않으면서 문제 해결
8. 회고
기능적으로는 문제를 해결했지만,
트랜잭션 내부 동작 순서를 재정의한 것이므로
Framework의 기본 동작을 우회하는 접근이 맞는지는 여전히 고민이 남는다.
다만,
운영 환경에서 즉시 필요한 요구사항이었고
명확한 원인 분석을 통한 최소한의 조치였다는 점에서
의미 있는 경험이었다.
'Spring Boot' 카테고리의 다른 글
| @Transactional (0) | 2023.02.05 |
|---|---|
| Redis (0) | 2022.03.26 |
| DBCP (1) | 2022.03.26 |
| 인메모리 데이터베이스 (0) | 2022.03.05 |
| CORS (0) | 2022.02.28 |
댓글