이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- Read-after-write와 monotonic read는 각각 어떤 사용자 경험을 보호하는가?
- replica read, cache, read model을 쓰면서 방금 쓴 값을 보여주려면 무엇이 필요한가?
- version token, primary routing, lag wait 중 어떤 전략을 선택해야 하는가?
개요
Read-after-write는 사용자가 방금 쓴 값을 다음 읽기에서 볼 수 있게 하는 보장이다.
Monotonic read는 사용자가 한 번 본 값보다 더 오래된 값을 나중에 보지 않게 하는 보장이다.
둘은 비슷해 보이지만 다르다.
주문 직후 상세 화면에 주문이 보여야 하는 것은 read-after-write다.
주문 상태가 배송 준비로 보였다가 다시 결제 완료로 내려가면 monotonic read가 깨진 것이다.
Replica, cache, read model을 쓰면 이 두 경험이 쉽게 깨진다.
이 문서가 바꾸는 설계 축
| 축 | 바뀌는 내용 |
|---|---|
| 성능 | primary routing이나 lag wait는 latency와 primary 부하를 늘린다. |
| 정합성 | 사용자가 쓴 값과 이미 본 version을 기준으로 읽기 경로를 보정한다. |
| 장애 격리 | replica lag가 커져도 핵심 화면만 primary로 우회할 수 있다. |
| 운영 복잡도 | version token, session hint, lag metric, fallback 정책을 관리한다. |
두 보장을 구분한다
| 보장 | 예시 | 깨졌을 때 |
|---|---|---|
| Read-after-write | 프로필 수정 직후 내 프로필에서 새 이름이 보인다. | 사용자는 저장 실패로 오해한다. |
| Monotonic read | 주문 상태가 배송중으로 보인 뒤 다시 준비중으로 보이지 않는다. | 사용자는 시스템을 믿지 못한다. |
| Consistent prefix | 채팅 메시지 순서가 앞뒤로 뒤집히지 않는다. | 대화 흐름이 깨진다. |
| Causal read | 댓글 작성 후 알림/답글이 원인보다 먼저 보이지 않는다. | 인과 관계가 이상해진다. |
| 실무에서는 모든 읽기에 linearizable read를 요구하기보다, 필요한 화면에 필요한 보장을 붙인다. |
대표 시나리오
사용자가 배송지를 수정한 뒤 주문 상세와 주문 목록을 본다.
| 화면 | 기대 | 허용 전략 |
|---|---|---|
| 배송지 수정 완료 응답 | 새 배송지 version 반환 | write result |
| 주문 상세 | 방금 쓴 배송지가 즉시 보여야 함 | primary read 또는 version wait |
| 주문 목록 | 몇 초 지연 가능 | replica read 가능 |
| 관리자 통계 | 수 분 지연 가능 | projection read |
| 이때 주문 상세까지 replica에서 읽으면 사용자가 이전 배송지를 볼 수 있다. | ||
| 목록과 통계는 지연을 허용할 수 있지만 상세는 read-after-write가 필요하다. |
Version token 전략
쓰기 응답에 version을 반환하고, 다음 읽기에서 최소 version을 요구한다.
PUT /shipping-address
200 OK
{
"addressId": "addr-10",
"version": 42
}다음 읽기:
GET /orders/order-1
X-Min-Version: address:42서버는 replica가 version 42 이상이면 replica를 읽고, 아니면 primary를 읽거나 잠깐 기다린다. 이 방식은 모든 요청을 primary로 보내지 않으면서 read-after-write를 보정한다.
Primary routing 전략
가장 단순한 방법은 쓰기 직후 일정 시간 primary를 읽는 것이다.
public OrderView getOrder(String orderId, ReadHint hint) {
if (hint.afterWrite()) {
return orderReader.readFromPrimary(orderId);
}
return orderReader.readFromReplica(orderId);
}이 방식은 구현이 쉽다.
하지만 쓰기 직후 사용자가 여러 화면을 빠르게 이동하면 primary 부하가 늘어난다.
afterWrite hint를 몇 초 유지할지, 어떤 API에만 적용할지 정해야 한다.
Replica lag wait 전략
Replica lag가 짧고 측정 가능하면 일정 시간 기다릴 수 있다.
required_version = 42
replica_visible_version = 41
max_wait = 200ms200ms 안에 replica가 version 42에 도달하면 replica를 읽는다.
도달하지 못하면 primary read나 PROCESSING 응답으로 전환한다.
이 방식은 latency budget이 있어야 한다.
무한 대기는 availability를 해친다.
Monotonic read 전략
Monotonic read는 사용자가 본 최대 version을 기억해야 한다.
GET /orders/order-1
X-Seen-Version: order:57서버는 최소 57 이상을 보여줘야 한다.
더 낮은 version밖에 없으면 primary로 가거나, 잠시 기다리거나, refreshing 상태를 준다.
사용자별 seen version을 client token, session, local storage 중 어디에 둘지도 결정해야 한다.
보안 민감 데이터라면 client가 임의로 조작해도 안전한 방식으로 검증해야 한다.
Cache와 read model
Cache는 read-after-write를 쉽게 깨뜨린다. 사용자가 프로필을 바꿨는데 cache TTL이 5분이면 이전 이름이 계속 보일 수 있다. 해결 방식은 여러 가지다.
| 방식 | 장점 | 비용 |
|---|---|---|
| write-through invalidation | 즉시 반영이 쉽다 | invalidation 실패 처리 필요 |
| user-specific bypass | 방금 쓴 사용자만 primary/cache bypass | 요청 경로가 복잡해진다 |
| short TTL | 단순하다 | miss 증가와 DB 부하 |
| response overlay | client가 방금 쓴 값을 임시 표시 | server truth와 차이 관리 |
| read model도 마찬가지다. | ||
| projection lag를 숨길지, 화면에 processing을 보여줄지 결정해야 한다. |
API 응답 계약
보장을 못 지킬 때도 계약이 필요하다.
200 OK
{
"orderId": "order-1",
"status": "PAYMENT_COMPLETED",
"freshness": "STALE",
"visibleVersion": 56,
"expectedVersion": 57
}또는 사용자가 기다려야 한다면:
202 Accepted
{
"status": "PROCESSING",
"retryAfterMillis": 500
}무조건 오래된 값을 200으로 주면 사용자는 저장 실패로 오해한다.
개인 프로젝트 기준
- 쓰기 직후 바로 보여야 하는 화면을 API별로 표시한다.
- replica나 cache를 쓴다면 read-after-write 예외 경로를 하나 둔다.
- version 컬럼이나 updatedAt으로 최소 version 기준을 만들 수 있는지 확인한다.
- 목록과 상세의 일관성 요구를 다르게 적는다.
- stale 응답을 숨기지 말고 간단한 상태 필드로 드러낸다.
기업 운영 기준
- read-after-write miss rate와 replica lag를 함께 본다.
- primary routing 비율이 늘면 primary capacity와 비용을 재검토한다.
- version token, session hint, client overlay 정책을 API 문서에 둔다.
- projection lag가 사용자 SLO를 넘으면 alert를 발생시킨다.
- support/CS가 stale과 processing의 의미를 이해하도록 문구를 통일한다.
위험 신호
- 모든 조회를 replica로 보내면서 주문 직후 상세 화면을 검증하지 않는다.
- cache TTL만 줄이고 invalidation 실패나 사용자별 bypass를 설계하지 않는다.
- 사용자가 본 상태가 뒤로 가도 괜찮다고 가정한다.
- version 없이 timestamp만으로 순서를 추정한다.
- stale 상태를 숨기고 저장 실패처럼 보이게 만든다.
- primary 우회 비율을 측정하지 않아 비용 증가를 모른다.
확인 질문
확인 질문
- 확인 질문: Read-after-write와 monotonic read의 차이는 무엇인가?
- 답변: read-after-write는 방금 쓴 값이 보여야 하는 보장이고, monotonic read는 이미 본 값보다 오래된 값을 나중에 보지 않게 하는 보장이다.
- 확인 질문: replica read를 쓰면서 read-after-write를 지키는 방법은 무엇인가?
- 답변: 일정 시간 primary routing, version token 기반 lag wait, 사용자별 cache bypass 같은 보정 경로가 필요하다.
- 확인 질문: stale 응답을 숨기면 왜 위험한가?
- 답변: 사용자가 저장 실패나 상태 후퇴로 오해해 재시도, 중복 요청, 문의가 늘어날 수 있기 때문이다.