Master DB에 INSERT한 회원 데이터가 Slave에선 조회되는데 Master의 다른 세션에선 안 보이던 간헐적 이슈를, MVCC 스냅샷과 autocommit·open-in-view·@Transactional의 상호작용까지 파고들어 해결한 글이다.
핵심 포인트- Aurora MySQL(REPEATABLE READ)에서 autocommit=false인데 COMMIT/ROLLBACK/BEGIN이 안 일어나면 InnoDB의 Consistent Nonlocking Read가 최초 시점 스냅샷을 유지해 이후 커밋된 데이터를 못 본다.
- 사용자 정의 쿼리 메서드(findByMemberNo 등)엔 @Transactional이 안 붙어 COMMIT이 없고, open-in-view=true라 Connection이 API 끝까지 유지되며, 하위 readOnly 트랜잭션의 COMMIT 탓에 ROLLBACK도 안 돼 스냅샷이 갱신되지 않았다.
- Slave 세션은 MariaDB Connector/J aurora 모드의 readOnly 라우팅으로 매 요청 COMMIT돼 스냅샷이 갱신되니 조회되고, Master 세션만 옛 스냅샷을 물고 있어 안 보이는 현상이 생겼다.
- 해결은 outerMethod에 @Transactional(readOnly=true)를 붙여 메서드 종료 시 COMMIT으로 스냅샷을 갱신(전체 쿼리를 Slave에서 실행)하는 것이다.
상세 정리- 증상: 간헐적으로 회원 데이터가 조회되지 않다가 재요청하면 조회됐다. 격리수준 REPEATABLE READ, HikariCP auto-commit=false, open-in-view=true 설정이었다.
- Replica Lag 배제: 조회 커넥션을 디버깅하니 Slave가 아니라 Master에서 실행 중이라 복제 지연이 원인이 아니었다.
- 진짜 원인: autocommit=false에선 종료 시점에 수동 COMMIT이 필요한데 이게 없으면 스냅샷이 갱신되지 않는다. Consistent Nonlocking Read는 트랜잭션 첫 읽기 시점 스냅샷을 유지하고, COMMIT/ROLLBACK/BEGIN이 스냅샷을 새로 뜬다.
- 시나리오: (A세션) 기존 회원 조회로 스냅샷#1 생성 → (B세션) 신규 가입·조회 성공 → (A세션) 스냅샷#1엔 신규 데이터가 없어 조회 실패 → ROLLBACK 후 다음 요청에서 스냅샷이 갱신돼 조회 성공.
- 해결책 비교: (1) READ COMMITTED로 낮추면 Phantom Read 사이드이펙트, (2) LOCK IN SHARE MODE 잠금 읽기는 락 경합·데드락 위험이 있어 둘 다 배제했다.
- 채택: @Transactional(readOnly=true)를 추가해 메서드 종료 시 COMMIT으로 스냅샷을 갱신했다.
- 함정1: Spring Data JPA 기본 메서드(findById 등)엔 @Transactional이 있지만, 인터페이스에 직접 정의한 쿼리 메서드는 없어 COMMIT이 일어나지 않는다.
- 함정2: HikariCP는 Connection close 시 isCommitStateDirty이고 autocommit=false면 ROLLBACK을 실행한다. open-in-view=true면 Connection이 API 끝까지 유지되는데, 하위 readOnly 트랜잭션이 이미 COMMIT을 해버려 close 시 ROLLBACK이 안 나 스냅샷이 갱신되지 않았다.
- 라우팅: aurora 모드에서 @Transactional(readOnly=true)는 Slave로, 그 외/무설정은 Master로 간다(단 Connector/J 3.0.3+는 aurora 모드 미지원).
- 부수 성과: autocommit=false로 트랜잭션 전후 set autocommit 0/1 쿼리를 없애 API 응답시간을 1.5ms 줄여 약 40% 개선했다.
왜 읽나JPA/HikariCP/Aurora 조합에서 MVCC 스냅샷·autocommit·open-in-view가 얽혀 "있던 데이터가 안 보이는" 현상을 겪는 백엔드·DB 개발자에게 원인과 트랜잭션 경계 설계를 알려주는 심층 사례.