이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.

  • Replica lag는 왜 발생하고 사용자에게 어떤 버그로 보이는가?
  • read replica를 쓰면 어떤 읽기를 primary로 보내야 하는가?
  • 백엔드에서 read-after-write 일관성을 어떻게 설계해야 하는가?

개요

Read replica는 primary DB의 변경을 복제해 읽기 부하를 분산하는 구조다. 읽기 트래픽이 많은 서비스에서 유용하지만, 복제가 지연되면 최신 데이터를 바로 보장하지 못한다.

Replica lag는 백엔드 API에서 “저장했는데 안 보여요”라는 버그로 나타난다. 이것은 DB 장애가 아니라 설계상 가능한 일일 수 있다.

원리

일반 흐름은 다음과 같다.

Client
  POST /posts -> primary write 성공
  GET /posts/{id} -> replica read
    replica가 아직 변경을 적용하지 못함
      404 또는 이전 상태 반환

Primary는 쓰기를 완료했지만 replica가 아직 따라오지 못하면 읽기 결과가 과거일 수 있다.

따라서 read/write split은 성능 최적화가 아니라 일관성 정책이다. 어떤 읽기가 stale read를 허용하는지, 어떤 읽기는 반드시 primary로 가야 하는지 API별로 정해야 한다.

Replica Lag 원인

Replica lag는 다음 상황에서 커질 수 있다.

  • primary 쓰기량 급증
  • replica 스펙 부족
  • 긴 쿼리가 replica 리소스를 점유
  • 대량 migration 또는 backfill
  • 네트워크 지연
  • 인덱스 생성, vacuum, checkpoint 같은 운영 작업

Replica는 읽기 전용이라 안전해 보이지만, 무거운 분석 쿼리를 replica에서 돌리면 복제 적용이 늦어질 수 있다.

백엔드에서 나타나는 증상

  • 회원가입 직후 내 프로필이 404다.
  • 주문 직후 주문 상세가 이전 상태다.
  • 결제 성공 후 목록에는 아직 미결제로 보인다.
  • 관리자 페이지와 사용자 페이지의 상태가 다르다.
  • 테스트에서는 통과하는데 운영에서 간헐적으로 실패한다.

이런 문제는 코드 버그일 수도 있지만 read replica 라우팅 문제일 수도 있다.

설계 기준

다음 읽기는 primary로 보내는 것을 검토한다.

  • 사용자가 방금 쓴 데이터를 즉시 확인하는 읽기
  • 결제, 주문, 권한, 잔액처럼 최신성이 중요한 읽기
  • 상태 전이를 결정하기 직전의 읽기
  • 중복 처리와 멱등성 판단에 필요한 읽기

다음 읽기는 replica에 더 적합할 수 있다.

  • 공개 게시글 목록
  • 오래된 통계
  • 검색 결과
  • 약간의 지연이 허용되는 대시보드
  • 읽기 전용 대량 조회

코드 예시

읽기 라우팅을 추상화할 수 있다.

@Transactional(readOnly = true)
@UseReplica
public List<PostSummary> listPublicPosts(PostSearchCondition condition) {
    return postRepository.findPublicPosts(condition);
}
 
@Transactional(readOnly = true)
public OrderDetail getMyRecentOrder(Long orderId, Long memberId) {
    return orderRepository.findByIdAndMemberId(orderId, memberId)
        .orElseThrow();
}

여기서 @UseReplica는 지연 허용 읽기에만 붙이는 식이다. 실제 구현은 AbstractRoutingDataSource 같은 방식으로 할 수 있지만, 핵심은 최신성 요구사항을 코드에 드러내는 것이다.

write 직후 일정 시간 동안 primary로 고정하는 전략도 있다.

public void markJustWritten(Long memberId) {
    consistencyContext.forcePrimaryFor(memberId, Duration.ofSeconds(5));
}

이 방식은 완벽한 보장은 아니지만 “방금 쓴 내 데이터”를 바로 보는 UX 문제를 줄일 수 있다. 더 강한 보장이 필요하면 primary read, transaction 내 read, version 기반 확인, 이벤트 처리 완료 확인 같은 방법을 검토한다.

실전 팁

  • write 직후 일정 시간 동안 같은 사용자의 읽기를 primary로 보내는 전략을 검토한다.
  • replica lag 지표를 API 오류와 함께 본다.
  • 관리자 대량 쿼리가 replica lag를 만들 수 있음을 이해한다.
  • read replica 장애 시 fallback 정책을 정한다.
  • replica에서 읽어도 되는 API 목록을 명시적으로 관리한다.
  • write 직후 사용자 단위 primary stickiness를 둘지 검토한다.
  • replica lag가 임계치를 넘으면 replica 라우팅을 중단하거나 지연 허용 API만 남기는 정책을 둔다.
  • readOnly라고 모두 replica가 아니라 최신성 요구사항을 기준으로 라우팅한다.

위험 신호!

  • 모든 readOnly transaction을 replica로 보낸다.
  • 주문, 결제, 권한 검사를 replica에서 읽는다.
  • replica lag 지표 없이 read/write split을 도입한다.
  • 대량 분석 쿼리를 운영 replica에서 마음대로 실행한다.
  • 저장 직후 조회 실패를 프론트 문제로만 본다.
  • replica lag 임계치와 fallback 정책 없이 read/write split을 켠다.
  • 권한, 잔액, 결제 상태처럼 최신성이 필요한 검사를 replica에서 수행한다.

확인 질문

  • Replica lag가 사용자에게 어떻게 보일 수 있는가?
    • 저장이나 상태 변경이 성공했는데 직후 조회에서 데이터가 없거나 이전 상태로 보일 수 있다.
  • 어떤 읽기는 primary로 보내야 하는가?
    • 방금 쓴 데이터 확인, 결제/주문/권한/잔액처럼 최신성이 중요한 읽기, 상태 전이 판단 읽기는 primary가 적합하다.
  • read replica를 도입할 때 필요한 관측 지표는 무엇인가?
    • replica lag, replica CPU/I/O, 쿼리 지연, 복제 오류, primary와 replica 트래픽 분포다.
  • read/write split에서 가장 먼저 정해야 할 정책은 무엇인가?
    • 어떤 API가 stale read를 허용하고 어떤 API가 반드시 primary 최신 읽기를 해야 하는지 정해야 한다.

참고 문서