From 22b758672f24db02fb84895b0fcd4b64f05ae828 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9D=B8=EC=B2=A0?= Date: Sat, 13 Jun 2026 14:32:57 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[6=EC=9E=A5]=20=ED=82=A4-=EA=B0=92=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=86=8C=20=EC=84=A4=EA=B3=84=20=EC=9E=90?= =?UTF-8?q?=EB=A3=8C=20=EB=B0=8F=20=EC=A0=95=EC=9D=B8=EC=B2=A0=20=ED=8F=B4?= =?UTF-8?q?=EB=8D=94=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 키-값 저장소 설계 1차 정리 추가 - 회고 문서 추가 - 스터디 토론 질문 (Q1~Q5 상세화) 추가 - 정인철 개인 폴더 생성 --- ...\354\204\244\352\263\204_1\354\260\250.md" | 265 ++++++++++++++++++ ...5\354\206\214_\355\232\214\352\263\240.md" | 24 ++ ...0\353\241\240_\354\247\210\353\254\270.md" | 208 ++++++++++++++ .../.gitkeep" | 0 4 files changed, 497 insertions(+) create mode 100644 "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" create mode 100644 "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" create mode 100644 "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" create mode 100644 "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/.gitkeep" diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" new file mode 100644 index 0000000..db64f8c --- /dev/null +++ "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" @@ -0,0 +1,265 @@ +# 6장. 키-값 저장소 설계 — 1차 설계 + +## 1. 요구사항 정리 + +키-값 저장소를 설계한다. 기본 연산은 두 가지다. + +- `put(key, value)` — 키-값 쌍을 저장 +- `get(key)` — 키에 해당하는 값을 조회 + +### 제약 조건 + +| 항목 | 조건 | +|------|------| +| 키-값 쌍 크기 | 10KB 이하 | +| 데이터 규모 | 대량 저장 가능해야 함 | +| 가용성 | 일부 장애에도 빠르게 응답 | +| 확장성 | 트래픽에 따라 서버 자동 증설/삭제 | +| 일관성 | 수준 조정 가능 | +| 응답 지연 | 짧아야 함 | + +### 요구사항 해석 + +"데이터 일관성 수준은 조정이 가능해야 한다"는 조건이 핵심이라고 판단했다. 모든 상황에서 강한 일관성을 요구하는 게 아니라, 용도에 따라 일관성 ↔ 가용성 사이에서 선택할 수 있어야 한다는 의미로 이해했다. + +또한 "트래픽에 따라 자동 증설/삭제"라는 조건은 단일 서버로는 불가능하고, 분산 환경이 전제되어야 한다고 판단했다. + +--- + +## 2. 핵심 API / 기능 흐름 + +### 기본 API + +``` +PUT /store/{key} + Body: { "value": "..." } + → 키-값 쌍 저장. 이미 존재하면 덮어쓰기. + +GET /store/{key} + → 키에 해당하는 값 반환. 없으면 404. + +DELETE /store/{key} + → 키-값 쌍 삭제. +``` + +### 쓰기 흐름 + +``` +Client → 요청 수신 노드(코디네이터) + → 해시 함수로 담당 노드 결정 + → 담당 노드 + N개 복제 노드에 쓰기 + → W개 이상 성공 응답 받으면 클라이언트에 성공 반환 +``` + +### 읽기 흐름 + +``` +Client → 요청 수신 노드(코디네이터) + → 해시 함수로 담당 노드 결정 + → R개 노드에서 읽기 + → 가장 최신 버전의 데이터를 클라이언트에 반환 +``` + +--- + +## 3. 데이터 저장 구조 + +### 단일 노드 내부 구조 + +``` +[쓰기 요청] + → Write-Ahead Log (WAL) 기록 — 장애 복구용 + → 메모리 테이블 (MemTable)에 저장 — 빠른 읽기/쓰기 + → MemTable이 임계치 도달 → 디스크(SSTable)로 flush + +[읽기 요청] + → MemTable 확인 → 있으면 바로 반환 + → 없으면 → SSTable에서 조회 (Bloom Filter로 빠르게 탐색) +``` + +이 구조를 선택한 이유: 모든 데이터를 메모리에 두면 빠르지만 용량 한계가 있고, 디스크만 쓰면 느리다. MemTable + SSTable 조합이면 자주 쓰이는 데이터는 메모리에서 빠르게 읽고, 오래된 데이터는 디스크에서 읽되 Bloom Filter로 불필요한 디스크 접근을 줄일 수 있다. + +### 데이터 파티셔닝 — 안정 해시 (Consistent Hashing) + +``` +해시 링: + + key1 → 노드A + key2 → 노드B + key3 → 노드C + +노드 추가 시: 해시 링에 새 노드 삽입 → 인접 키만 재배치 +노드 삭제 시: 삭제된 노드의 키가 다음 노드로 이동 +``` + +안정 해시를 선택한 이유: 일반 해시(key % N)는 서버 수가 바뀌면 거의 모든 키가 재배치된다. 안정 해시는 노드 추가/삭제 시 평균 K/N개의 키만 이동하면 되므로 확장성 요구사항에 적합하다. + +가상 노드(Virtual Node)를 활용해서 데이터 쏠림도 방지한다. 물리 노드 1대당 가상 노드 여러 개를 해시 링에 배치하면 데이터가 고르게 분산된다. + +### 데이터 복제 (Replication) + +``` +복제 전략: + 각 키를 해시 링에서 시계방향으로 N개 노드에 복제 + + 예: N=3이면 + key1 → 노드A(원본), 노드B(복제1), 노드C(복제2) + +주의: + 가상 노드 사용 시 같은 물리 서버에 중복 저장되지 않도록 체크 필요 + 서로 다른 데이터센터에 분산 배치 → 자연재해/정전 대비 +``` + +--- + +## 4. 전체 아키텍처 + +``` +┌─────────┐ +│ Client │ +└────┬────┘ + │ + ▼ +┌──────────────┐ +│ Coordinator │ ← 아무 노드나 코디네이터 역할 가능 (리더 없음) +│ (요청 라우팅) │ +└──────┬───────┘ + │ 안정 해시로 담당 노드 결정 + ▼ +┌──────────────────────────────────────┐ +│ 해시 링 (Hash Ring) │ +│ │ +│ 노드A ──── 노드B ──── 노드C │ +│ │ │ │ │ +│ MemTable MemTable MemTable │ +│ SSTable SSTable SSTable │ +│ WAL WAL WAL │ +│ │ +│ 각 노드는 담당 키 + 복제 키를 보유 │ +└──────────────────────────────────────┘ +``` + +### 설계 핵심 결정 + +**리더 없는 구조(Leaderless)를 선택한 이유:** + +리더-팔로워 구조는 리더가 단일 장애 지점(SPOF)이 된다. 요구사항에 "높은 가용성"이 있으므로, 모든 노드가 읽기/쓰기를 처리할 수 있는 리더 없는 구조가 적합하다고 판단했다. + +**코디네이터 역할:** + +클라이언트 요청을 받은 노드가 코디네이터가 된다. 해시 함수로 담당 노드를 찾고, 복제 노드에도 요청을 전달한 뒤, 정족수(W/R) 응답을 모아서 클라이언트에 반환한다. + +--- + +## 5. 일관성 제어 — 정족수 (Quorum) + +요구사항에 "일관성 수준 조정 가능"이 있으므로, N/W/R 값으로 조절하는 방식을 설계했다. + +``` +N = 복제본 수 (예: 3) +W = 쓰기 성공 응답 필요 수 +R = 읽기 성공 응답 필요 수 +``` + +| 설정 | 특성 | 용도 | +|------|------|------| +| W=1, R=1 | 빠르지만 일관성 약함 | 로그, 조회수 | +| W=N, R=1 | 읽기 빠름, 쓰기 느림 | 읽기 집중 서비스 | +| W=1, R=N | 쓰기 빠름, 읽기 느림 | 쓰기 집중 서비스 | +| W+R > N | 강한 일관성 보장 | 금융, 결제 | + +**W+R > N이면 강한 일관성인 이유:** 읽기와 쓰기가 반드시 최소 1개 이상의 공통 노드를 거치게 되므로, 최신 데이터를 반드시 읽을 수 있다. + +--- + +## 6. 병목 / 장애 가능성 + +### 장애 감지 + +단순히 한 노드가 "저 노드 죽었어요"라고 해서 바로 장애 처리하면 안 된다. 네트워크 일시 장애일 수 있기 때문. + +**가십 프로토콜(Gossip Protocol) 채택:** + +``` +각 노드가 주기적으로 박동(heartbeat)을 교환 +→ 일정 시간 동안 박동이 안 오면 장애로 판정 +→ 여러 노드가 동의해야 최종 장애 확정 +``` + +이 방식을 선택한 이유: 모든 노드 간 직접 통신(멀티캐스팅)은 노드 수가 많아지면 O(N²)으로 비효율적이다. 가십은 무작위 노드끼리만 교환하므로 확장성이 좋다. + +### 일시적 장애 처리 — Hinted Handoff + +``` +노드C 장애 발생 +→ 노드C 담당 요청을 노드D가 임시로 처리 +→ 노드D에 "이건 노드C 꺼야"라는 힌트를 남겨둠 +→ 노드C 복구되면 노드D → 노드C로 데이터 인계 +``` + +### 영구적 장애 처리 — 머클 트리 (Merkle Tree) + +``` +각 노드가 자기 데이터의 해시 트리를 관리 +→ 두 노드의 머클 트리 루트 해시를 비교 +→ 다르면 하위 노드를 재귀적으로 비교 +→ 불일치하는 데이터만 동기화 + +장점: 전체 데이터를 비교하지 않고 차이점만 빠르게 찾음 +``` + +### 데이터 충돌 — 벡터 시계 (Vector Clock) + +``` +리더 없는 구조에서 동시 쓰기 시 충돌 가능: + +서버1: put(key1, "A") → 노드A에 기록 +서버2: put(key1, "B") → 노드B에 기록 (동시에) + +벡터 시계로 해결: + D([S1, v1]) vs D([S2, v1]) + → 서로 선후관계가 없음 → 충돌 감지 + → 클라이언트에게 두 버전을 보내서 해소 요청 +``` + +--- + +## 7. 고민한 트레이드오프 + +### 일관성 vs 가용성 (CAP) + +분산 시스템에서 네트워크 파티션은 피할 수 없으므로 P는 필수. 결국 C(일관성)와 A(가용성) 중 선택해야 한다. + +| 선택 | 장점 | 단점 | 적합한 서비스 | +|------|------|------|-------------| +| CP (일관성 우선) | 모든 노드에서 같은 데이터 보장 | 장애 시 쓰기 중단 가능 | 은행, 결제 | +| AP (가용성 우선) | 장애에도 항상 응답 | 일시적으로 옛날 데이터 반환 가능 | SNS, 캐시 | + +이 설계에서는 **AP를 기본으로 하되, 정족수(W/R) 조절로 일관성 수준을 높일 수 있는 구조**를 택했다. 요구사항에 "일관성 수준 조정 가능"이 있었기 때문. + +### 메모리 vs 디스크 + +전부 메모리에 두면 빠르지만 비용이 높고 용량 한계가 있다. 전부 디스크에 두면 느리다. MemTable(메모리) + SSTable(디스크)의 LSM Tree 구조가 쓰기 성능과 저장 용량의 균형점이라고 판단했다. + +### 강한 일관성 vs 최종 일관성 + +강한 일관성은 모든 복제본에 쓰기가 반영될 때까지 읽기/쓰기를 차단해야 한다. 고가용성 요구사항과 충돌. 최종 일관성(Eventual Consistency)을 기본으로 채택하고, 벡터 시계로 충돌을 감지/해소하는 방향으로 설계했다. + +### 벡터 시계의 한계 + +[서버:버전] 순서쌍이 계속 늘어나서 메타데이터가 커질 수 있다. 임계치를 두고 오래된 순서쌍을 제거하는 방식으로 대응하지만, 이 경우 버전 간 선후관계 판단 정확도가 떨어질 수 있다. + +--- + +## 8. 내가 이 구조를 선택한 이유 — 요약 + +| 결정 | 선택 | 이유 | +|------|------|------| +| 아키텍처 | 리더 없는 분산 구조 | 높은 가용성, SPOF 제거 | +| 파티셔닝 | 안정 해시 + 가상 노드 | 노드 추가/삭제 시 최소 재배치 | +| 복제 | N개 노드 복제 | 가용성 + 내구성 확보 | +| 일관성 제어 | 정족수 (N/W/R) | 요구사항의 "일관성 수준 조정" 대응 | +| 일관성 모델 | 최종 일관성 + 벡터 시계 | 가용성 우선, 충돌은 감지 후 해소 | +| 장애 감지 | 가십 프로토콜 | 확장성 좋은 분산 장애 감지 | +| 장애 처리 | Hinted Handoff + 머클 트리 | 일시적/영구적 장애 각각 대응 | +| 저장 구조 | WAL + MemTable + SSTable | 쓰기 성능과 저장 용량의 균형 | diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" new file mode 100644 index 0000000..8460fd1 --- /dev/null +++ "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" @@ -0,0 +1,24 @@ +# 6장. 키-값 저장소 설계 — 회고 + +## 어려웠던 것 + +- 처음엔 그냥 "HashMap 크게 만들면 되는 거 아닌가?" 생각했는데 분산 환경 고려하니까 생각보다 복잡했음 +- CAP 이론을 이론으로는 알았는데, 실제로 W/R/N 숫자로 일관성 수준을 조절한다는 게 처음엔 잘 와닿지 않았음 +- 벡터 시계가 특히 어려웠음 — 충돌 감지까지는 이해했는데 "그래서 클라이언트가 어떻게 해소해?" 부분은 아직도 명확하지 않음 + +## 새로 알게 된 것 + +- 안정 해시를 실제 DB(Cassandra, DynamoDB 등)가 쓴다는 걸 알게 되니까 갑자기 현실감이 생겼음 +- Hinted Handoff + Gossip Protocol 조합이 "아 이게 그냥 이론이 아니라 실제 Redis Cluster나 Cassandra에 들어가 있는 거구나" 싶었음 +- LSM Tree(MemTable + SSTable) 구조가 왜 쓰기에 유리한지 처음 이해했음 + +## 아직 불명확한 것 + +- 벡터 시계로 충돌 감지 후 클라이언트가 해소하는 구체적인 UX/로직 +- 가상 노드 개수를 실제로 어떻게 결정하는지 (몇 개가 적당한지 기준이 없음) +- 머클 트리 동기화 주기를 얼마나 자주 돌려야 하는지 + +## 느낀 점 + +- 평소에 Redis 쓸 때 그냥 캐시 도구로만 봤는데 이제 내부가 조금 보이는 것 같음 +- 트레이드오프를 명시적으로 고민하는 연습이 됐음 — 설계에 "정답"이 없고 요구사항마다 다르다는 걸 체감했음 diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" new file mode 100644 index 0000000..ca47bb8 --- /dev/null +++ "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" @@ -0,0 +1,208 @@ +# 6장. 키-값 저장소 설계 — 스터디 토론 질문 + +## Q1. 키-값 저장소는 왜 탄생했을까? + +> 관계형 DB(MySQL, PostgreSQL 등)가 이미 있는데, 왜 키-값 저장소가 따로 만들어졌을까요? +> 어떤 요구사항이나 한계가 있었길래 새로운 저장소가 필요했을까요? + +### 배경 + +2000년대 초 인터넷 서비스가 폭발적으로 성장하면서 RDB가 감당하기 어려운 상황들이 생겼다. + +- **Amazon**: 수억 명의 사용자 장바구니, 상품 정보를 RDB 하나로 감당하기 어려워짐 +- **Google**: 웹 전체를 크롤링한 데이터를 저장해야 하는데, 테이블 구조로 맞추기 어려움 +- **Facebook**: 친구 관계, 피드, 좋아요 등 비정형 데이터가 폭발적으로 증가 + +이런 배경에서 Amazon은 2007년 **Dynamo 논문**을, Google은 2006년 **Bigtable 논문**을 발표했고, 이게 현대 키-값 저장소의 시초가 됐다. + +### RDB의 한계 — 어디서 막혔을까? + +| 한계 | 설명 | +|------|------| +| 수평 확장 어려움 | RDB는 기본적으로 단일 서버 가정. 서버를 늘리려면 샤딩이 필요한데 매우 복잡함 | +| 스키마 경직성 | 컬럼 추가/변경 시 마이그레이션 비용이 크고, 비정형 데이터에 취약 | +| JOIN 비용 | 데이터가 수억 건이 되면 JOIN이 병목 | +| 가용성 | 리더 장애 시 서비스 중단 위험 | + +### 토론 + +- "RDB로 해결이 안 됐던 게 뭔지" 각자 하나씩 얘기해보기 +- 반대로 키-값 저장소가 RDB보다 못한 점은 뭐가 있을까? +- 지금 내가 다루는 서비스에서 RDB 대신 키-값 저장소를 쓰면 좋을 게 있을까? + +--- + +## Q2. 노드 장애를 어떻게 감지할까? + +> 설계에서는 가십 프로토콜을 썼는데, 만약 가십 없이 "한 노드가 죽으면 즉시 다른 노드가 알 수 있게" 하려면 어떻게 해야 할까요? +> 그 방식의 문제점은 뭘까요? + +### 배경 + +분산 시스템에서 "노드가 죽었다"는 사실을 알리는 것은 생각보다 어렵다. 단순히 ping을 쏴보면 되지 않냐고 생각할 수 있지만, 수십~수백 개의 노드가 서로를 감시하는 구조에서는 **어떻게 알릴 것인가** 자체가 설계 문제가 된다. + +### 가십 없이 장애를 감지하는 방법들 + +| 방식 | 설명 | 문제점 | +|------|------|--------| +| **중앙 모니터 노드** | 마스터 노드 하나가 모든 노드에 주기적으로 heartbeat 전송 | 마스터 노드 자체가 SPOF(단일 장애점)가 됨 | +| **All-to-All heartbeat** | 모든 노드가 다른 모든 노드에게 heartbeat 전송 | 노드 N개일 때 메시지가 N²로 폭발적으로 증가 | +| **링 구조** | 각 노드가 옆 노드만 감시 | 연쇄 장애 시 감지 지연, 링 분단 문제 | + +### 가십 프로토콜이 선택된 이유 + +가십(Gossip) 프로토콜은 각 노드가 **랜덤하게 선택한 소수의 이웃**에게만 자신의 상태를 전파한다. + +``` +노드 A → 랜덤하게 B, D에게 heartbeat 목록 전달 +노드 B → 랜덤하게 A, C에게 전달 +... 이렇게 소문처럼 퍼짐 +``` + +- 메시지 수가 O(log N) 수준으로 유지됨 +- 마스터 노드 없이도 전체 상태 동기화 가능 +- 네트워크 단절이 있어도 나중에 복구 시 수렴 + +### 토론 + +- All-to-All heartbeat로 충분히 되지 않을까? 노드가 몇 개쯤 되면 문제가 생길까? +- 가십 프로토콜에서 "죽었다고 판단하는 기준"은 어떻게 잡을까? (타임아웃? 몇 번 연속 실패?) +- 실제로 쓰는 서비스에서 노드 장애를 감지하는 방식이 있다면 어떤 방식인지 얘기해보기 + +--- + +## Q3. Redis는 리더가 있을까, 없을까? + +> 이번 설계에서는 리더 없는(Leaderless) 구조를 선택했는데, +> 실제로 Redis Cluster는 어떤 구조일까요? (찾아보거나 아는 대로) +> 리더가 있으면 뭐가 편하고, 없으면 뭐가 편할까요? + +### 배경 + +분산 키-값 저장소는 크게 두 가지 복제 구조로 나뉜다. + +- **리더 기반(Leader-based)**: 쓰기는 리더에게만, 리더가 팔로워에게 복제 +- **리더 없는(Leaderless)**: 아무 노드에나 쓰기 가능, 쿼럼으로 일관성 보장 + +이번 장의 설계는 Leaderless 방식이었지만, 실제 Redis Cluster는 다른 선택을 했다. + +### Redis Cluster의 실제 구조 + +Redis Cluster는 **리더 기반**이다. 다만 단순한 단일 리더가 아니라, 슬롯(slot) 단위로 리더가 분산되어 있다. + +``` +슬롯 0~5000 → Primary A (+ Replica A1, A2) +슬롯 5001~10000 → Primary B (+ Replica B1, B2) +슬롯 10001~16383 → Primary C (+ Replica C1, C2) +``` + +- 클라이언트는 키의 해시값으로 어느 Primary에 쓸지 계산 +- Primary가 죽으면 해당 Replica 중 하나가 Primary로 승격 + +### 리더 유무에 따른 트레이드오프 + +| | 리더 기반 | 리더 없는 | +|--|---------|---------| +| **일관성** | 강함 (쓰기 순서 보장) | 약함 (충돌 가능) | +| **쓰기 가용성** | 리더 장애 시 일시 중단 | 어느 노드로든 계속 가능 | +| **구현 복잡도** | 상대적으로 단순 | 충돌 해소 로직 필요 | +| **지연 시간** | 리더 경유로 살짝 느릴 수 있음 | 가까운 노드로 바로 쓰기 가능 | +| **대표 시스템** | Redis Cluster, Kafka | Cassandra, DynamoDB | + +### 토론 + +- Redis가 Leaderless 대신 리더 기반을 선택한 이유가 뭘까? +- 내가 만드는 서비스에서 "리더 노드가 죽는 순간"에 어떤 일이 일어나는지 생각해보기 +- Cassandra는 Leaderless인데, Redis와 어떤 용도에서 선택이 갈릴까? + +--- + +## Q4. 내가 설계자라면 N, W, R을 얼마로 잡을까? + +> N=3 기준으로 아래 중 본인이라면 어떤 걸 고를지, 이유도 함께: +> +> | | W | R | +> |---|---|---| +> | A | 1 | 1 | +> | B | 2 | 2 | +> | C | 3 | 1 | +> | D | 1 | 3 | +> +> 서비스 종류에 따라 달라진다면 어떤 서비스에 어떤 조합이 맞을지도 얘기해보기 + +### 배경 + +쿼럼(Quorum) 조건이란, **W + R > N** 을 만족하면 읽기와 쓰기가 적어도 한 노드에서 겹친다는 보장이다. 이 조건을 만족해야 "가장 최신 데이터를 반드시 읽을 수 있다"고 할 수 있다. + +``` +N = 복제 노드 수 +W = 쓰기 성공으로 인정할 최소 노드 수 +R = 읽기 성공으로 인정할 최소 노드 수 + +W + R > N → 강한 일관성 보장 +W + R ≤ N → 최종 일관성 (빠르지만 오래된 데이터 읽힐 수 있음) +``` + +### 각 조합 분석 (N=3 기준) + +| 조합 | W | R | W+R > N? | 특징 | 적합한 서비스 | +|------|---|---|----------|------|------------| +| **A** | 1 | 1 | 1+1=2, **아님** | 가장 빠름, 일관성 포기 | 실시간 로그 수집, 방문자 카운터 | +| **B** | 2 | 2 | 2+2=4, **만족** | 균형 잡힌 선택 | 일반적인 API 서버, 사용자 프로필 | +| **C** | 3 | 1 | 3+1=4, **만족** | 쓰기에 엄격, 읽기는 빠름 | 금융 거래, 재고 차감 | +| **D** | 1 | 3 | 1+3=4, **만족** | 쓰기는 빠름, 읽기에 엄격 | 읽기 중심 + 정확성 필요한 서비스 | + +### 현실에서의 힌트 + +- **DynamoDB**: 기본값 W=1, R=1 (빠른 응답 우선, 강한 일관성은 옵션) +- **Cassandra**: 기본 QUORUM = N/2+1, 즉 N=3이면 W=2, R=2 + +### 토론 + +- 내가 만드는 서비스에서 "오래된 데이터를 읽어도 괜찮은 경우"가 있을까? 어떤 경우? +- W=3 (모든 노드에 써야 성공)으로 잡으면 어떤 문제가 생길까? +- 쓰기가 많은 서비스와 읽기가 많은 서비스에서 선택이 어떻게 달라질까? + +--- + +## Q5. 벡터 시계로 충돌이 났을 때 클라이언트가 해소한다는 게 실제로 어떤 모습일까? + +> "두 버전을 클라이언트에 보내서 해소 요청"이라고 했는데, +> 실제 앱이라면 사용자에게 어떻게 보여줄까요? +> (힌트: 구글 Docs 동시 편집, 카카오톡 메시지 순서 등 생각해보기) + +### 배경 + +벡터 시계는 "어떤 버전이 먼저인가"를 판단하기 위한 논리적 시계다. 두 버전을 비교했을 때 어느 쪽도 다른 쪽보다 앞서지 않으면 **충돌(conflict)** 이 발생한다. + +``` +버전 A: [서버1=2, 서버2=1] → 서버1에서 2번, 서버2에서 1번 변경 +버전 B: [서버1=1, 서버2=2] → 서버1에서 1번, 서버2에서 2번 변경 + +A와 B 중 어느 쪽이 최신? → 판단 불가 → 충돌! +``` + +이때 서버는 두 버전을 모두 클라이언트에 넘기고 "알아서 합쳐라"고 한다. + +### 실제 서비스에서 충돌 해소 방식 + +| 서비스 | 충돌 상황 | 해소 방법 | +|--------|---------|---------| +| **Amazon 장바구니** | 두 기기에서 동시에 상품 추가/삭제 | 두 장바구니를 **합집합**으로 병합 (삭제가 무시될 수 있음) | +| **Google Docs** | 두 사람이 같은 문단 동시 편집 | OT(Operational Transform) 알고리즘으로 자동 병합 | +| **Notion / 협업 툴** | 오프라인 후 동기화 시 충돌 | "충돌 발생" 표시 후 사용자에게 선택 요청 | +| **Git** | 같은 파일 같은 줄 동시 수정 | `<<<<<<` 마커로 표시, 사용자가 직접 해소 | +| **카카오톡 메시지 순서** | 서버 시간 오차로 순서 뒤바뀜 | 서버 수신 시간 기준으로 강제 정렬 (충돌 포기) | + +### 충돌 해소 전략의 종류 + +- **Last Write Wins (LWW)**: 타임스탬프가 나중인 것 채택 → 단순하지만 데이터 유실 가능 +- **자동 병합**: 충돌 없이 합칠 수 있으면 자동으로 (Google Docs 방식) +- **사용자에게 위임**: 두 버전 모두 보여주고 선택하게 함 (Git, Notion 방식) +- **도메인 규칙 적용**: 장바구니 합집합처럼, 비즈니스 로직으로 결정 + +### 토론 + +- Amazon이 장바구니 충돌을 "합집합"으로 해소한 게 맞는 선택일까? 삭제한 상품이 다시 살아나면? +- 내가 만드는 서비스에서 충돌이 날 수 있는 데이터가 있다면? 그건 어떻게 해소할까? +- "자동으로 합치는 것"과 "사용자에게 선택하게 하는 것" 중 언제 어떤 걸 쓰는 게 맞을까? diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/.gitkeep" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/.gitkeep" new file mode 100644 index 0000000..e69de29 From 9e848f2b36561ff5e14b7abf9fca13037d918a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=9D=B8=EC=B2=A0?= Date: Sun, 14 Jun 2026 21:39:27 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[6=EC=9E=A5]=20=ED=82=A4-=EA=B0=92=20?= =?UTF-8?q?=EC=A0=80=EC=9E=A5=EC=86=8C=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20?= =?UTF-8?q?=EC=A0=95=EC=9D=B8=EC=B2=A0=20=ED=8F=B4=EB=8D=94=EB=A1=9C=20?= =?UTF-8?q?=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../\354\240\225\354\235\270\354\262\240/.gitkeep" | 0 ...236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" | 0 ...4\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" | 0 ...\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" | 0 4 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/.gitkeep" rename "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" => "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" (100%) rename "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" => "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" (100%) rename "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" => "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" (100%) diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/.gitkeep" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/.gitkeep" deleted file mode 100644 index e69de29..0000000 diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" similarity index 100% rename from "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" rename to "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/01_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\354\204\244\352\263\204_1\354\260\250.md" diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" similarity index 100% rename from "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" rename to "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/02_\355\202\244\352\260\222_\354\240\200\354\236\245\354\206\214_\355\232\214\352\263\240.md" diff --git "a/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" "b/06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" similarity index 100% rename from "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md" rename to "06-\355\202\244\352\260\222-\354\240\200\354\236\245\354\206\214/\354\240\225\354\235\270\354\262\240/03_\354\212\244\355\204\260\353\224\224_\355\206\240\353\241\240_\354\247\210\353\254\270.md"