Read/Write DB Connection 획득 타이밍 문제

띠용·2025년 11월 15일

우테코 7기 BE

목록 보기
10/14
  • 복제 지연 문제를 학습하기 위해
  • 읽기/쓰기 데이터 소스를 Master/Slave DB로 분리하는 과정에서
  • 모든 조회가 Master로 접근하는 문제가 발생했다.

[문제]

MySQL Master/Slave 구조로 읽기/쓰기 분리를 구현했다.

  • 쓰기: @Transactional → Master
  • 읽기: @Transactional(readOnly = true) → Slave

를 기대하면서 AbstractRoutingDataSource로 라우팅을 했고, 라우터는 대략 이렇게 되어 있었다.

protected Object determineCurrentLookupKey() {

if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {

return DataSourceType.READER; // Slave

}

return DataSourceType.WRITER;     // Master

}

서비스는 다음처럼 구성했다.

@Transactional
public void create(Coupon coupon) { ... }

@Transactional(readOnly = true)
public Coupon getCoupon(Long id) { ... }

복제 지연 미션을 위해 Slave에 replication delay 1초도 걸어두고, create → 바로 getCoupon 호출해서 “복제 지연”을 체감하려고 했는데,

실패해야 할 테스트가 자꾸 잘 통과한다.

[행동]

먼저 라우터와 서비스 내부에 로그를 추가해서 상태를 확인했다.

  • 라우터
    System.out.println("DETERMINE");
    
    if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
    
    System.out.println("READ ONLY");
    
    return DataSourceType.READER;
    
    }
  • 서비스
    @Transactional(readOnly = true)
    
    public Coupon getCoupon(Long id) {
    
    System.out.println("getCoupon readOnly = " +
    
    TransactionSynchronizationManager.isCurrentTransactionReadOnly());
    
    ...
    
    }

실행 결과는 이랬다.

  • getCoupon 안에서 출력한 readOnly 값은 true
  • 그런데 라우터에서는 READ ONLY 로그가 전혀 찍히지 않음

정리하면, “트랜잭션은 분명 readOnly=true인데, 라우팅할 때는 false로 보인다”는 이상한 상황이었다.

원인을 추적하다가 LMS에서 힌트를 얻었다.

  • AbstractRoutingDataSource“커넥션을 가져오는 시점”determineCurrentLookupKey()를 호출한다.
  • 스프링 트랜잭션의 readOnly 설정은, 커넥션을 얻은 뒤에 커넥션/세션에 적용될 수 있다.
  • 즉, 라우팅은 트랜잭션 readOnly가 fully 반영되기 전에 실행될 수 있다.

이 말은 곧, “커넥션을 늦게 가져오게 만들어야, 라우팅 시점에 readOnly 값을 볼 수 있다”는 뜻이라서, Spring에서 제공하는 LazyConnectionDataSourceProxy를 쓰기로 했다.

기존에는 routingDataSource를 그대로 @Primary dataSource로 노출하고 있었는데, 이를 수정했다.

  • 변경 전 dataSource = routingDataSource
  • 변경 후 dataSource = new LazyConnectionDataSourceProxy(routingDataSource)

즉, 구조를 JPA → RoutingDataSource → Writer/Reader 에서 JPA → LazyConnectionDataSourceProxy → RoutingDataSource → Writer/Reader로 바꿨다.

이렇게 하면 트랜잭션이 먼저 시작되고(readOnly 플래그 포함),

실제 커넥션이 필요한 시점에야 LazyConnectionDataSourceProxyRoutingDataSource에 커넥션을 요청하게 되어, 그때는 이미 readOnly 상태가 제대로 잡혀 있다.

[결과]

LazyConnectionDataSourceProxy로 감싸서 dataSource를 변경하자, 바로 증상이 사라졌다.

  • 라우터 로그에서 쓰기 메서드 호출 시: determine, readOnly=false → WRITER 읽기 메서드 호출 시: determine, readOnly=true → READ ONLY → READER 가 정상적으로 찍히기 시작했다.
  • Master에는 insert/update 위주로, Slave에는 select 위주로 쿼리가 분리되는 것도 확인했다.
  • replication delay를 준 Slave에서 복제 지연 테스트를 하면, 이제는 “바로 읽으면 아직 안 보이는” 상황도 실제로 관찰할 수 있게 됐다.

[요약]

  • AbstractRoutingDataSource 단독 사용 시, 커넥션 획득 시점과 트랜잭션 readOnly 적용 시점이 어긋나서, 라우팅 시점에는 readOnly 정보를 제대로 볼 수 없다

이를 해결하기 위해

  • RoutingDataSource 앞에 LazyConnectionDataSourceProxy를 한 번 감싸서
  • “트랜잭션 설정 → 커넥션 획득 → 라우팅” 순서가 맞도록 만듬

Read/Write 분기하려면 AbstractRoutingDataSource만 쓰지 말고 LazyConnectionDataSourceProxy까지 같이 써야 한다

0개의 댓글