Primary/Replica DB를 분리하는 이유, Replication Lag 문제, 그리고 해결 전략을 정리합니다.

왜 Primary/Replica를 분리하는가

일반적인 웹 애플리케이션은 읽기 트래픽이 쓰기 트래픽보다 훨씬 많습니다. 하나의 DB가 모든 트래픽을 처리하면 부하가 집중되고, 읽기 쿼리가 쓰기 락에 영향을 받을 수 있습니다.

[분리 전]
App → 단일 DB (읽기 + 쓰기 모두 처리)

[분리 후]
     ┌─→ Primary DB (쓰기)
App ─┤
     └─→ Replica DB (읽기)

Primary/Replica 분리의 목적은 크게 세 가지입니다.

목적 설명
부하 분산 읽기 트래픽을 Replica로 보내 Primary의 부하를 줄인다
읽기 성능 향상 Replica를 여러 대 두어 읽기 처리량을 수평 확장한다
가용성 Primary 장애 시 Replica를 승격하여 새로운 Primary로 전환(failover)할 수 있다

Replication Lag

Primary/Replica 분리에는 근본적인 문제가 따릅니다. Primary에 기록된 데이터가 Replica에서 조회 가능해지기까지 지연이 발생하며, 이를 Replication Lag이라 합니다.

Primary/Replica 분리는 읽기 성능을 확장하는 구조이지만, 읽기 일관성(read-after-write consistency)은 보장하지 않습니다. 따라서 쓰기 직후 읽기에서는 Replication Lag으로 인한 문제가 발생할 수 있습니다.

오동작 사례

주문 생성 후 바로 주문을 조회하는 코드입니다.

@Transactional
public void createOrder(OrderRequest request) {
  orderRepository.save(new Order(request));  // Primary에 INSERT
}

@Transactional(readOnly = true)
public Order getOrder(Long orderId) {
  return orderRepository.findById(orderId)  // Replica에서 SELECT
      .orElseThrow(() -> new OrderNotFoundException(orderId));
}
// Controller
orderService.createOrder(request);            // Primary에 저장
Order order = orderService.getOrder(orderId); // Replica에서 조회 → 아직 없음!

Primary에 INSERT한 직후 Replica에서 SELECT하면, Replica에 데이터가 아직 복제되지 않아 조회 결과가 없습니다. 사용자는 주문을 생성했는데 “주문을 찾을 수 없습니다"라는 응답을 받게 됩니다.

이 문제는 쓰기 직후 읽기가 발생하는 모든 곳에서 나타날 수 있습니다. 회원가입 후 로그인, 게시글 작성 후 목록 조회, 설정 변경 후 변경된 값 확인 등이 대표적입니다.

Replication Lag이 발생하는 이유

MySQL은 binlog를 Replica로 전송하여 재실행하는 방식입니다.

[MySQL Replication]
Primary binlog → Replica IO thread → relay log → Replica SQL thread → apply (replay)
→ 네트워크 전송 + relay log 기록 + SQL 재실행 비용 = 수백ms~수초 lag (부하에 따라 더 증가 가능)

Aurora는 공유 스토리지 아키텍처를 사용하여 Replication 구조 자체가 다릅니다.

[Aurora Replication]
Primary → 공유 스토리지 (redo log 기록) ← Replica (읽기)
→ 100ms 미만 (공식 문서 기준: "usually much less than 100 milliseconds")

Aurora의 Primary와 Replica는 같은 스토리지 볼륨을 바라봅니다. Primary가 데이터를 commit하면 스토리지에 기록되고 Replica는 같은 스토리지를 읽습니다. 다만 Primary에서 commit된 변경 사항이 Replica에서 조회 가능해지기까지는 짧은 지연이 발생할 수 있습니다. 이는 commit 정보가 Replica로 전파되는 과정과 Replica의 버퍼 캐시 무효화 과정 때문입니다. 따라서 Aurora에서도 짧은 Replication Lag이 존재합니다.

Aurora는 Primary와 Replica가 같은 스토리지를 공유하기 때문에 전통적인 MySQL Replication보다 lag이 크게 줄어들지만, commit 정보 전파와 캐시 무효화 과정 때문에 완전히 0ms가 되지는 않습니다.

Aurora의 Replication Lag은 일반적으로 수십 ms 수준이며, 보통 100ms 미만입니다. 인스턴스 사양과 부하에 따라 달라지며, 실제 운영 환경에서 CloudWatch AuroraReplicaLag 지표를 확인한 결과 20ms 이하로 관측되었습니다. 기존 MySQL 대비 크게 개선되었지만 0ms는 아니므로, 쓰기 직후 읽기 패턴에서는 여전히 문제가 될 수 있습니다.


해결 전략

Replication Lag에 대한 완벽한 해결책은 없습니다. 상황에 따라 적절한 전략을 선택해야 합니다.

쓰기 직후 읽기는 Primary에서 조회

가장 단순하고 확실한 방법입니다. 쓰기 직후 조회가 필요한 경우에만 Primary를 사용합니다. 쓰기 트랜잭션 안에서 결과를 반환하거나, 해당 조회만 readOnly를 빼서 Primary로 보내는 방법이 있습니다. 다만 이 패턴을 남용하면 읽기 부하가 Primary에 집중되어 분리의 의미가 퇴색되므로, lag이 문제가 되는 특정 흐름에서만 제한적으로 사용해야 합니다.

지연 읽기 (Delay)

조회 시점을 의도적으로 늦추는 방법입니다. 클라이언트 측에서 폴링 간격을 두거나, 서버에서 짧은 대기 후 조회합니다. 실시간성이 중요하지 않은 경우(이메일 발송 확인, 비동기 작업 결과 등)에 적합합니다.

세션 기반 읽기 일관성

사용자가 데이터를 쓴 뒤, 해당 사용자의 읽기 요청만 일정 시간 동안 Primary로 보내는 방법입니다. 구현이 복잡하고 사용자별 상태를 관리해야 하는 부담이 있습니다.

정리

방법 복잡도 적합한 경우
쓰기 직후 Primary 조회 낮음 쓰기 후 즉시 결과가 필요한 API
지연 읽기 낮음 실시간성이 덜 중요한 경우
세션 기반 읽기 일관성 높음 대규모 서비스, 정교한 제어 필요

대부분의 경우 “쓰기 직후 Primary 조회"로 충분합니다. 모든 읽기를 Primary로 보내는 것이 아니라, lag이 문제가 되는 특정 흐름에서만 Primary를 사용하는 것이 핵심입니다.

참고