Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
265 changes: 265 additions & 0 deletions 06-키값-저장소/정인철/01_키값_저장소_설계_1차.md
Original file line number Diff line number Diff line change
@@ -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 | 쓰기 성능과 저장 용량의 균형 |
24 changes: 24 additions & 0 deletions 06-키값-저장소/정인철/02_키값_저장소_회고.md
Original file line number Diff line number Diff line change
@@ -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 쓸 때 그냥 캐시 도구로만 봤는데 이제 내부가 조금 보이는 것 같음
- 트레이드오프를 명시적으로 고민하는 연습이 됐음 — 설계에 "정답"이 없고 요구사항마다 다르다는 걸 체감했음
Loading