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

  • Read Write 분리는 어떤 읽기 병목을 쓰기 경로에서 떼어내려는 선택인가?
  • 인덱스, 캐시, read replica, materialized view 중 어디까지가 충분한가?
  • 읽기 부하 분리가 성능, 정합성, 장애 격리, 운영 복잡도를 어떻게 바꾸는가?

목적을 좁혀서 본다

Read Write 분리는 “조회가 많으니 DB를 하나 더 둔다”가 아니다.

쓰기 경로가 지켜야 하는 불변식과 읽기 경로가 요구하는 조회 형태가 서로 충돌할 때, 읽기 부하를 쓰기 경로에서 떼어내는 선택이다.

예시는 주문 생성 API관리자 매출 대시보드가 같은 주문 DB를 사용하는 상황이다.

주문 생성은 재고 차감, 결제 금액, 주문 상태 전이의 정합성을 지켜야 한다.

대시보드는 일별 매출, 취소율, 배송 상태, 고객 등급을 빠르게 보여줘야 한다.

대시보드의 큰 조인과 집계가 주문 생성 트랜잭션과 같은 buffer, I/O, connection pool을 쓰면 쓰기 경로가 같이 느려진다.

Read Write 분리의 1차 목적은 조회를 빠르게 하는 것이 아니라 쓰기 경로를 보호하는 것이다.

이 선택이 바꾸는 설계 축

변화
성능무거운 조회를 별도 경로로 보내 write DB의 p95와 pool 대기를 낮춘다.
정합성읽기 결과가 쓰기 직후 최신이 아닐 수 있다.
장애 격리대시보드 조회 장애가 주문 생성 장애로 번지는 것을 줄인다.
운영 복잡도replica lag, cache invalidation, view refresh, fallback 정책을 운영한다.

성능 개선만 보고 분리하면 이후 장애가 “빠른데 틀린 조회”로 바뀐다.

따라서 어떤 읽기가 stale을 허용하는지 먼저 정해야 한다.

먼저 확인할 증거

분리 전에 읽기가 실제로 쓰기를 방해하는지 확인한다.

다음 신호가 반복될 때 분리를 검토한다.

신호확인할 지표
관리자 조회 시간대에 주문 생성이 느려진다write API p95, DB CPU/I/O, pool pending
조회 쿼리가 큰 정렬과 집계를 수행한다slow query, temp file, sort memory
read QPS와 write TPS의 peak가 다르다endpoint별 QPS, 시간대별 traffic
조회 장애가 쓰기 API 장애로 번진다connection pool 점유, lock wait
원본 스키마와 화면 응답 형태가 계속 어긋난다join 수, DTO 조립 비용

단일 SQL이 느린 문제라면 인덱스와 pagination으로 먼저 해결한다.

구조 분리는 관측된 압박을 떼어낼 때만 비용을 정당화한다.

기준 시나리오

주문 생성 = 500 TPS
주문 상세 조회 = 1,500 QPS
관리자 대시보드 = 3,000 QPS
대시보드 허용 지연 = 30초
주문 생성 목표 p95 = 150ms
대시보드 목표 p95 = 400ms

이 상황에서 모든 조회를 primary DB로 보내면 대시보드 새로고침이 주문 생성 latency를 흔들 수 있다.

반대로 모든 조회를 replica로 보내면 주문 직후 사용자가 자기 주문을 못 보는 문제가 생긴다.

따라서 읽기 경로를 API별로 나눈다.

  • 주문 생성 직후 내 주문 상세: primary 또는 write result 기반
  • 공개 주문 통계: read replica 가능
  • 관리자 대시보드: replica, materialized view, cache 검토
  • 실시간 재고 확인: primary 또는 재고 owner API

Read Write 분리는 DB 라우팅 규칙과 사용자 정합성 계약을 함께 정하는 작업이다.

선택지를 단계적으로 올린다

선택지먼저 얻는 것새 비용멈출 조건
인덱스와 pagination특정 조회 개선write 비용 증가몇 개 쿼리만 문제
Cache반복 조회 흡수TTL, invalidationstale 허용 가능
Read replicaprimary 읽기 부하 감소replica lag같은 스키마로 조회 가능
Materialized view집계 조회 단순화refresh 비용DB 내부 집계로 충분
조회 전용 table화면 형태 최적화동기화와 검증원본 스키마와 화면이 크게 다름

이 표의 마지막이 항상 CQRS는 아니다.

많은 서비스는 read replica나 materialized view에서 멈추는 것이 더 낫다.

도메인 command 모델까지 나누는 결정은 다음 문서의 CQRS 적용 기준에서 따로 판단한다.

Read replica 사용 기준

Read replica는 같은 모델을 복제해 읽는 선택이다.

따라서 원본 테이블 형태로도 조회가 가능하고, 약간의 lag를 허용할 수 있을 때 적합하다.

사용자가 방금 쓴 데이터, 결제 상태, 권한 판단, 중복 처리 확인은 replica보다 primary가 적합하다.

@Transactional(readOnly = true)
@UseReplica
public DashboardSummary getDashboard(LocalDate date) {
    return dashboardQueryRepository.summary(date);
}
 
@Transactional(readOnly = true)
public OrderDetail getMyRecentOrder(Long memberId, Long orderId) {
    return orderRepository.findOwnerView(memberId, orderId)
        .orElseThrow();
}

readOnly=true라고 모두 replica로 보내면 안 된다.

최신성 요구사항을 기준으로 라우팅해야 한다.

Cache와 view의 경계

캐시는 같은 조회 결과가 반복될 때 좋다.

하지만 결제 금액, 재고 차감, 권한 확인처럼 최신성이 중요한 값은 캐시가 주 경로가 되면 위험하다.

Materialized view는 큰 집계를 읽기 쉬운 형태로 만들지만 refresh 시점과 잠금을 고려해야 한다.

dashboard_view_refresh_interval = 30s
max_stale_allowed = 60s
fallback = last_successful_snapshot

view가 오래되었을 때 마지막 안전 시점을 보여줄 수 있으면 운영자가 상황을 설명할 수 있다.

이 기준이 없으면 사용자는 빠른 숫자를 보지만 그 숫자를 믿어도 되는지 알 수 없다.

API 계약

읽기 분리를 했다면 응답에 기준 시각을 드러낸다.

{
  "salesAmount": 18200000,
  "orderCount": 9421,
  "asOf": "2026-07-01T10:14:35+09:00",
  "freshness": "STALE_BUT_AVAILABLE"
}

쓰기 API는 read model 반영을 암묵적으로 약속하지 않는다.

{
  "orderId": "ord-1001",
  "status": "ACCEPTED",
  "visibleInDashboard": "EVENTUAL"
}

이 계약이 있어야 주문 성공과 대시보드 반영 지연을 분리해서 설명할 수 있다.

운영 지표

Read Write 분리 후에는 두 경로를 따로 본다.

  • write API p95와 DB pool pending
  • read API p95와 error rate
  • replica lag 또는 view refresh lag
  • stale response ratio
  • primary로 우회한 read 비율
  • dashboard fallback 사용 횟수

읽기 성능이 좋아졌는데 primary 우회가 계속 늘면 분리 기준이 잘못된 것이다.

반대로 stale response가 많아지면 성능 개선이 사용자 신뢰 비용으로 바뀐 것이다.

위험 신호

  • 느린 SQL 하나를 고치지 않고 바로 구조를 분리한다.
  • 모든 readOnly transaction을 replica로 보낸다.
  • 주문, 결제, 권한 판단을 replica에서 읽는다.
  • stale 허용 시간을 API별로 정하지 않았다.
  • replica lag나 view refresh lag를 보지 않는다.
  • read 장애가 write 장애로 번지지 않는지 검증하지 않았다.

확인 질문

확인 질문

  • 확인 질문: Read Write 분리를 검토하기 전에 가장 먼저 확인할 것은 무엇인가?
    • 답변: 읽기 쿼리가 실제로 쓰기 DB의 latency, I/O, connection pool을 압박하는지 관측해야 한다.
  • 확인 질문: read replica만으로 충분한 경우는 언제인가?
    • 답변: 같은 스키마로 조회할 수 있고, 해당 API가 replica lag를 허용할 수 있을 때다.
  • 확인 질문: 이 선택이 운영 복잡도를 늘리는 이유는 무엇인가?
    • 답변: replica lag, stale 응답, cache invalidation, view refresh, primary 우회 정책을 새로 운영해야 하기 때문이다.

참고 문서