MySQL Master/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());
...
}실행 결과는 이랬다.
true정리하면, “트랜잭션은 분명 readOnly=true인데, 라우팅할 때는 false로 보인다”는 이상한 상황이었다.
원인을 추적하다가 LMS에서 힌트를 얻었다.
AbstractRoutingDataSource는 “커넥션을 가져오는 시점”에 determineCurrentLookupKey()를 호출한다.이 말은 곧, “커넥션을 늦게 가져오게 만들어야, 라우팅 시점에 readOnly 값을 볼 수 있다”는 뜻이라서, Spring에서 제공하는 LazyConnectionDataSourceProxy를 쓰기로 했다.
기존에는 routingDataSource를 그대로 @Primary dataSource로 노출하고 있었는데, 이를 수정했다.
dataSource = routingDataSourcedataSource = new LazyConnectionDataSourceProxy(routingDataSource)즉, 구조를 JPA → RoutingDataSource → Writer/Reader 에서 JPA → LazyConnectionDataSourceProxy → RoutingDataSource → Writer/Reader로 바꿨다.
이렇게 하면 트랜잭션이 먼저 시작되고(readOnly 플래그 포함),
실제 커넥션이 필요한 시점에야 LazyConnectionDataSourceProxy가 RoutingDataSource에 커넥션을 요청하게 되어, 그때는 이미 readOnly 상태가 제대로 잡혀 있다.
LazyConnectionDataSourceProxy로 감싸서 dataSource를 변경하자, 바로 증상이 사라졌다.
AbstractRoutingDataSource 단독 사용 시, 커넥션 획득 시점과 트랜잭션 readOnly 적용 시점이 어긋나서, 라우팅 시점에는 readOnly 정보를 제대로 볼 수 없다이를 해결하기 위해
RoutingDataSource 앞에 LazyConnectionDataSourceProxy를 한 번 감싸서Read/Write 분기하려면 AbstractRoutingDataSource만 쓰지 말고 LazyConnectionDataSourceProxy까지 같이 써야 한다