대규모 데이터 시스템, 무엇을 포기하고 무엇을 얻는가
Lambda/Kappa 아키텍처 선택부터 시계열 DB 다운샘플링, Redis Cluster 슬롯 분산, 샤딩 전략, 글로벌 복제 일관성까지 — 데이터 시스템 설계의 근본 트레이드오프를 추적한다.
- 01 시스템 설계의 모든 결정은 하나의 질문으로 귀결된다
- 02 트래픽 진입점 설계 — DNS부터 스토리지까지
- 03 시스템 설계 면접의 공통 문법
- 04 대규모 서비스 설계의 공통 언어
- 05 대규모 데이터 시스템, 무엇을 포기하고 무엇을 얻는가
- 06 시스템은 어떻게 실패하지 않는가
- 07 아키텍처 결정은 어떻게 기억되는가
대규모 데이터 시스템을 설계할 때 “정답”이란 없다. 있는 것은 트레이드오프의 선택뿐이다. Lambda를 쓸 것인가 Kappa를 쓸 것인가, 시계열 데이터를 얼마나 오래 원본 해상도로 보존할 것인가, 샤드 키를 무엇으로 잡을 것인가, 글로벌 복제를 동기로 할 것인가 비동기로 할 것인가 — 이 모든 결정은 결국 하나의 질문으로 수렴한다. 무엇을 포기하고 무엇을 얻을 것인가?
파이프라인: 코드를 한 번 쓸 것인가, 두 번 쓸 것인가
데이터 파이프라인 아키텍처 논쟁의 핵심은 “실시간”과 “정확성” 사이의 긴장이다.
Lambda 아키텍처는 이 긴장을 병렬 레이어로 해소한다. Batch Layer는 매일 자정 전날 데이터를 완전히 재계산해 정확한 뷰를 만들고, Speed Layer는 Flink/Spark Streaming으로 최근 수분~수시간의 대략적 실시간 결과를 만든다. 사용자는 두 결과의 합집합을 본다. 문제는 같은 집계 로직을 배치 코드와 스트리밍 코드로 두 번 작성해야 한다는 것이다. 유지보수 비용이 두 배가 되고, 두 결과가 미묘하게 불일치할 위험이 항상 존재한다.
Kappa 아키텍처는 이 중복을 잘라낸다. Kafka의 보존 기간을 길게 설정하고(7~30일), 모든 처리를 단일 스트리밍 엔진으로 통일한다. 집계 로직을 수정해야 할 때는 Kafka 처음부터 다시 읽어 재처리한다. Netflix와 LinkedIn이 이 방식을 채택한 이유다. 단, Kafka 스토리지 비용이 증가하고 수백 TB 규모의 재처리는 느릴 수 있다.
Lambda는 기존 배치 인프라가 이미 존재할 때 유리하다. 신규 구축이고 스트리밍 중심이라면 Kappa가 코드 단순성과 일관성 면에서 우위다. 배치만으로 분석 지연을 허용할 수 있다면 — 그게 가장 단순한 선택이다.
시계열: 시간이 지날수록 해상도를 낮춘다
서버 1만 대에서 메트릭 100개를 10초마다 수집하면 초당 100,000 쓰기가 발생한다. 이를 1년 원본 그대로 보존하면 156TB에 달한다. 시계열 데이터 시스템의 핵심 설계 결정은 여기서 나온다 — 시간이 지날수록 해상도를 낮추는 Rollup 전략.
0 ~ 7일: 원본 (10초 해상도)
7일 ~ 1달: 1분 평균으로 롤업 → 1/6 데이터
1달 ~ 1년: 1시간 평균으로 롤업 → 1/360 데이터
이 전략만으로 156TB를 약 5TB로 줄일 수 있다(30배 절감). InfluxDB의 Continuous Query가 이 롤업을 자동화한다.
또 다른 함정은 고카디널리티다. host × service × region × environment 조합이 3,000만 시계열을 만들면 InfluxDB는 각 시계열마다 인덱스 항목을 메모리에 유지하다 OOM으로 죽는다. 선택지는 세 가지다 — 태그 카디널리티를 제한하거나, 고카디널리티에 강한 ClickHouse로 전환하거나, Prometheus + VictoriaMetrics 조합을 쓰거나.
분산 캐시: 슬롯, 잠금, 스탬피드
Redis Cluster는 16,384개 해시 슬롯으로 데이터를 분산한다. 키의 슬롯은 CRC16(key) % 16384로 결정된다. 여기서 Hash Tag가 중요해진다.
"user:{1001}:profile" → CRC16("1001") → 슬롯 X
"user:{1001}:session" → CRC16("1001") → 같은 슬롯 X
"user:{1001}:cart" → CRC16("1001") → 같은 슬롯 X
중괄호 안의 값만으로 슬롯을 결정하므로, 사용자 관련 키 전체가 같은 노드에 배치된다. Multi-get을 단일 노드에서 처리할 수 있게 된다. 단, Hash Tag를 남용해 {user}:1001, {user}:1002처럼 쓰면 모든 키가 "user" 슬롯 하나에 몰려 핫 샤드가 된다.
분산 환경의 잠금은 단순하지 않다. Primary 장애 후 Replica가 승격되는 순간, 아직 복제되지 않은 잠금이 사라지면서 두 클라이언트가 동시에 잠금을 획득할 수 있다. Redlock은 5개의 독립된 Redis 인스턴스에서 과반수(3개) 획득을 요구함으로써 이 문제를 완화한다.
TTL 만료 순간의 Cache Stampede도 함정이다. 동시에 수백 개의 요청이 DB로 쏟아진다. PER(Probabilistic Early Recomputation) 알고리즘은 TTL이 충분히 남아 있어도 확률적으로 미리 갱신함으로써 한 클라이언트만 DB를 조회하게 만든다.
샤딩: 무엇을 기준으로 나눌 것인가
샤딩 전략 선택은 쿼리 패턴에서 출발해야 한다.
| 방식 | 균등 분산 | 범위 쿼리 | 리샤딩 | 핫샤드 방지 |
|---|---|---|---|---|
| Hash | ✅ | ❌ | 어려움 | ✅ |
| Range | ❌ | ✅ | 쉬움 | ❌ |
| Directory | 제어 가능 | 보통 | 유연 | 수동 제어 |
| Consistent Hash | ✅ | ❌ | 최소 이동 | 보통 |
Hash Sharding은 균등 분산에 강하지만 샤드 수가 바뀌면 대부분의 키가 재배치된다. Range Sharding은 시간 기반 조회에 적합하지만 최신 데이터 샤드에 쓰기가 집중되는 핫샤드 문제가 따라온다. Consistent Hashing은 노드 추가 시 K/N개 키만 이동하므로 동적 확장에 유리하다.
샤드 수를 늘려야 할 때의 온라인 리샤딩 절차도 중요하다 — 더블 쓰기 기간(이전 샤드와 새 샤드 모두 쓰기) → 백그라운드 데이터 복사 → 점진적 트래픽 전환(1% → 10% → 50% → 100%) → 이전 샤드 정리. 한 번에 전환하면 위험하다.
글로벌 분산: 지연과 일관성의 교환
서울과 미국 동부 사이 RTT는 약 200ms다. 글로벌 분산 시스템에서 강한 일관성을 유지하려면 모든 쓰기가 이 왕복 지연을 감수해야 한다. Google Spanner가 TrueTime API(GPS + 원자 시계)로 이를 해결했지만, 전용 하드웨어가 필요하다.
대부분의 실무 선택은 비동기 복제 + 최종 일관성이다. 서울 Primary에 쓰고 즉시 응답한 뒤, 비동기로 도쿄와 미국에 복제한다. 서울 장애 시 복제되지 않은 데이터가 유실될 수 있다(RPO > 0). 같은 대륙 내(서울↔도쿄, 30ms)에서는 동기 복제를 허용하고, 다른 대륙 간에는 비동기를 쓰는 준동기(semi-synchronous) 방식이 현실적 절충점이다.
동시 수정 충돌은 CRDT로 해결한다. G-Counter 방식으로 좋아요 수를 집계하면 서울과 도쿄가 동시에 다른 값을 기록해도 병합 결과는 항상 동일하다. 순서 없이 합쳐도 같은 값이 나오는 수학적 보장이다.
정리
- 파이프라인 아키텍처는 “코드 중복과 정확성(Lambda)” vs “단일 코드와 일관성(Kappa)” 사이의 선택이다.
- 시계열 데이터는 시간이 지날수록 해상도를 낮추는 Rollup으로 수십 배 공간을 절감한다.
- Redis Cluster의 Hash Tag는 Multi-get을 최적화하지만 과용하면 핫 샤드를 만든다. Cache Stampede는 PER 알고리즘으로 완화한다.
- 샤딩 전략은 쿼리 패턴에서 출발한다. 리샤딩은 더블 쓰기와 점진적 전환으로 온라인 처리한다.
- 글로벌 복제는 지연과 일관성의 교환이다. 비동기 복제 + CRDT가 현실적인 균형점이다.
다음 글에서는 이 데이터 시스템 위에서 동작하는 신뢰성 설계 — 장애를 전제하고 설계하는 Fault Tolerance 패턴을 추적한다.