대규모 서비스 설계의 공통 언어
동영상 스트리밍부터 위치 기반 서비스까지, 7개 시스템 설계 문제에서 반복되는 핵심 패턴과 트레이드오프 결정 원칙을 추적한다.
- 01 시스템 설계의 모든 결정은 하나의 질문으로 귀결된다
- 02 트래픽 진입점 설계 — DNS부터 스토리지까지
- 03 시스템 설계 면접의 공통 문법
- 04 대규모 서비스 설계의 공통 언어
- 05 대규모 데이터 시스템, 무엇을 포기하고 무엇을 얻는가
- 06 시스템은 어떻게 실패하지 않는가
- 07 아키텍처 결정은 어떻게 기억되는가
YouTube, Twitter, WhatsApp, Google Drive, Google Search, Ticketmaster, Uber. 겉으로는 완전히 다른 서비스처럼 보이지만, 설계 문서를 나란히 놓고 보면 같은 패턴이 반복된다. 그 패턴을 읽을 수 있으면, 처음 보는 시스템 설계 문제도 빈 종이에서 시작하지 않아도 된다. 반복되는 결정들의 공통 언어는 무엇인가?
쓰기 경로와 읽기 경로를 분리하라
모든 대규모 시스템에서 첫 번째로 마주치는 결정은 쓰기와 읽기를 같은 경로로 처리할 것인가다. 대부분의 경우 답은 “아니다”이다.
동영상 스트리밍에서 업로드(쓰기)는 S3 Presigned URL로 API 서버를 우회하고, 스트리밍(읽기)은 CDN 엣지에서 처리한다. 소셜 피드에서 게시물 작성(쓰기)은 Fan-out Worker를 통해 비동기로 처리되고, 피드 조회(읽기)는 Redis List에서 O(1)으로 반환된다. 검색 엔진에서 크롤링과 색인 구축(쓰기)은 별도 파이프라인이고, 검색 쿼리(읽기)는 캐시 → 색인 서버 순으로 처리된다.
이 분리는 단순한 최적화가 아니다. 쓰기와 읽기의 요구사항이 근본적으로 다르기 때문이다. 쓰기는 정확성이 중요하고, 읽기는 속도가 중요하다. 같은 경로에서 둘을 함께 처리하면 둘 다 타협하게 된다.
동기를 비동기로 전환하는 시점
“업로드 즉시 응답, 처리는 백그라운드”라는 문장이 7개 챕터 중 4개에 등장한다. Kafka가 중간에 자리잡는 이유도 여기 있다.
동영상 트랜스코딩은 원본 1개에서 6개 해상도 파일을 만든다. 이 작업을 업로드 요청과 동기로 처리하면 사용자는 수십 분을 기다려야 한다. Kafka에 이벤트를 발행하고 즉시 응답을 반환하면, 트랜스코딩은 백그라운드에서 Worker Pool이 병렬로 처리한다. 소셜 피드의 Fan-out도 같다. 게시물 1개가 팔로워 200명의 캐시를 업데이트해야 한다면, 그 작업을 동기로 처리하면 작성 응답 시간이 팔로워 수에 비례해 늘어난다.
비동기 전환의 대가는 최종 일관성(eventual consistency) 이다. 동영상은 업로드 직후 수 분간 재생할 수 없고, 게시물은 작성 직후 일부 팔로워의 피드에 아직 없을 수 있다. 이 지연이 UX에 허용 가능한가를 판단하는 것이 설계자의 역할이다. 조회수 집계, 좋아요 카운터, 피드 업데이트는 모두 “몇 분 지연 허용”으로 분류된다. 예약 시스템의 재고 감소는 “즉시, 원자적으로”로 분류된다.
Redis가 반복해서 등장하는 이유
7개 시스템 모두에서 Redis가 사용된다. 그런데 역할이 매번 다르다.
동영상 스트리밍: 조회수 버퍼 (INCR "view:{video_id}")
소셜 피드: 피드 캐시 (LPUSH "feed:{user_id}" post_id)
채팅 시스템: Presence 관리 (SET "presence:{user_id}" EX=30)
클라우드 저장: 권한 캐시 (폴더 권한 상속 결과)
예약 시스템: 재고 원자적 감소 (DECR + Lua Script)
위치 서비스: 공간 인덱스 (GEOADD + GEORADIUS)
공통점은 하나다 — DB 앞에서 충격을 흡수한다. DB는 정확하지만 느리다. Redis는 빠르지만 영속성을 보장하지 않는다. 이 두 특성을 조합하는 방식이 반복된다: Redis에 먼저 쓰고, DB에는 배치로 반영한다. Redis가 죽으면 DB에서 재구성할 수 있는 데이터에만 이 패턴을 적용한다.
Redis는 메모리 기반이라 비용이 비싸고, 재시작 시 데이터가 사라질 수 있다. AOF/RDB 영속성을 켜면 쓰기 성능이 떨어진다. “빠른 임시 저장소”와 “느린 영속 저장소”를 명확히 구분하고, Redis에는 재구성 가능하거나 손실 허용되는 데이터만 올리는 것이 원칙이다.
동시성 문제는 항상 같은 세 가지 방법으로 푼다
예약 시스템에서 같은 좌석을 두 명이 동시에 예약하려는 시나리오는, 사실 모든 시스템에 변형된 형태로 존재한다. 동시에 같은 좋아요를 누르거나, 동시에 같은 파일을 수정하거나, 동시에 재고를 소진하는 상황이다.
해결책은 항상 세 범주 중 하나다.
첫째, DB 제약 (Pessimistic Lock 또는 Unique Constraint). SELECT FOR UPDATE나 유니크 인덱스로 DB가 직접 충돌을 거부한다. 정확하지만 트래픽이 많으면 락 경합으로 병목이 생긴다.
둘째, 낙관적 잠금 (Optimistic Lock). 버전 번호를 함께 저장하고, 업데이트 시 버전이 읽어온 것과 같을 때만 성공한다. 충돌이 드문 경우 효율적이지만, 충돌이 잦으면 재시도 폭풍이 발생한다.
셋째, Redis 원자적 연산 (DECR, SETNX, Lua Script). Redis의 단일 스레드 특성을 활용해 Race Condition 없이 처리한다. 예약 시스템의 DECR "inventory:{event_id}"가 대표적이다. 속도가 빠르지만 Redis 장애 시 재고 상태가 불일치할 수 있어, DB 유니크 제약을 최후 안전망으로 병용한다.
셀럽 문제 — 핫스팟은 항상 특별 취급이 필요하다
소셜 피드의 Fan-out 챕터에서 “셀럽(팔로워 1만 이상)은 Pull 방식으로 전환”하는 Hybrid 설계가 등장한다. 이 패턴은 다른 시스템에서도 반복된다.
검색 엔진의 인기 쿼리 상위 1%가 전체 쿼리의 80%를 처리한다는 롱테일 법칙도 같은 맥락이다. Redis 캐시에 인기 쿼리 결과만 올려도 대부분의 부하가 흡수된다. 동영상 스트리밍에서도 인기 콘텐츠만 CDN 엣지에 프리워밍하면 충분하다. 위치 서비스에서는 자주 계산되는 경로를 캐싱한다.
정리
- 쓰기/읽기 경로 분리는 선택이 아니라 대규모 시스템의 전제조건이다.
- Kafka를 통한 비동기 전환은 응답 시간과 처리량을 분리하는 기본 도구다. 대가는 최종 일관성이다.
- Redis는 DB 충격 흡수제다. 재구성 가능한 데이터만 올리고, 영속성은 DB에 위임한다.
- 동시성 문제는 DB 제약, 낙관적 잠금, Redis 원자적 연산 세 범주로 해결하며, 중요도에 따라 중첩 적용한다.
- 핫스팟(셀럽, 인기 쿼리, 인기 콘텐츠)은 항상 별도 처리 경로가 필요하다. 균등 분산 가정은 현실과 맞지 않는다.
다음 글에서는 이 패턴들이 데이터 집약 파이프라인에서 어떻게 변형되는지, 그리고 배치 처리와 스트림 처리를 어떻게 조합하는지 추적한다.