3

구글 맵

위치 갱신, 경로 안내, 지도 표시 세 기능을 DAU 10억 규모로 설계한다. 핵심은 "무거운 건 미리 계산해서 CDN/객체 저장소에 두고, 실시간성이 필요한 부분만 스트리밍으로 처리한다"는 것.

1단계 · 문제 이해와 설계 범위

기능 요구사항

사용자 위치 갱신, 경로 안내(ETA 포함), 지도 표시. 운전 모드 중심이지만 여러 이동 수단 고려. 교통 상황 반영.

비기능 요구사항

정확도(잘못된 경로 안내 금지), 부드러운 렌더링, 모바일 데이터·배터리 최소 사용, 고가용성·확장성.

지도 101 — 알고 가야 할 배경지식

라우팅 타일(routing tile) — 전 세계 도로를 하나의 그래프로 두면 메모리·성능이 감당 안 된다. 그래서 지오해시처럼 그래프를 격자 단위로 잘라 라우팅 타일로 만들고, 타일끼리는 참조로 연결해 필요한 타일만 메모리에 올린다. 또 상세도에 따라 3계층(지역 도로 → 간선 도로 → 고속도로, 뒤로 갈수록 타일 크기가 큼)으로 나눠, 먼 거리는 상위 계층 타일로 탐색해 효율을 높인다.

예시로 보면 · 강남역에서 해운대까지

전국 골목길을 전부 그래프에 올려 탐색하면 노드가 수천만 개라 감당이 안 된다. 대신 이렇게 나눠 탐색한다.

  • 출발지 근처: 골목까지 있는 지역 도로 타일로 가까운 고속도로 IC까지
  • 중간 구간: 고속도로만 있는 큰 타일로 경부고속도로를 따라 탐색
  • 도착지 근처: 다시 지역 도로 타일로 내려와 목적지까지

사람이 길 찾는 방식과 같다. "일단 고속도로 타고, 나가서 골목 찾자."

개략적 규모 추정

항목계산결과
지도 타일 저장 용량줌 21 기준 약 4.4조 개 × 타일당 100KB ≈ 440PB. 지구 표면의 대부분(바다·사막·숲 등)은 압축률이 매우 높아 실제로는 크게 줄고, 줌 레벨 전체를 합쳐도약 100PB 수준
위치 갱신 QPSDAU 10억 × 주 35분 사용 → 하루 약 50억 분. 1초마다 전송하면 약 300만 QPS15초 단위 일괄 전송 시 약 20만 QPS, 최대 약 100만 QPS(평균 × 5)

2단계 · 개략적 설계

flowchart LR
  M[모바일 클라이언트] -->|위치 일괄 전송| LB[로드밸런서]
  M -->|경로 요청| LB
  M -->|타일 요청| CDN[CDN]
  LB --> LS[위치 서비스]
  LB --> NS[경로 안내 서비스]
  LS --> UDB[(사용자 위치 DB)]
  NS --> GDB[(지오코딩 DB)]
  NS --> RT[(라우팅 타일
객체 저장소)] CDN --> S3[(사전 생성 지도 타일
객체 저장소)]

① 위치 서비스

② 경로 안내 서비스

③ 지도 표시

3단계 · 상세 설계

데이터 모델

데이터저장 방식포인트
라우팅 타일객체 저장소(S3 등) + 캐시도로 데이터를 오프라인 파이프라인으로 가공해 생성. 인접 리스트를 직렬화한 파일, 지오해시로 찾음. DB에 둘 필요 없음
사용자 위치카산드라파티션 키 user_id, 클러스터링 키 timestamp → 사용자별 시간순 조회
지오코딩레디스 같은 키-값 저장소읽기 빈번, 쓰기 드묾 → 인메모리가 적합
사전 생성 지도 타일객체 저장소 + CDN줌 레벨·지오해시 기준으로 키 구성

지도 표시 최적화 — 벡터 타일

래스터 이미지 대신 경로·다각형 같은 벡터 정보를 보내고 클라이언트(WebGL)가 렌더링. 압축이 잘 되고 줌 전환이 훨씬 부드러움.

경로 안내 서비스 내부

flowchart TB
  C[클라이언트] --> RP[경로 계획 서비스]
  RP --> GC[지오코딩 서비스]
  RP --> SP[최단 경로 서비스]
  RP --> ETA[ETA 서비스]
  RP --> RK[순위 결정 서비스]
  SP --> RT[(라우팅 타일)]
  ETA --> TDB[(교통 DB)]
  K[[카프카: 위치 스트림]] --> UP[갱신 서비스]
  UP --> TDB
  UP --> RT
  
예시로 보면 · 최단 경로와 ETA를 나눈 이유

강남역에서 판교까지 후보가 경부고속도로, 분당수서로, 국도 세 개라고 하자. 이 후보 목록은 새 도로가 뚫리지 않는 한 오늘이나 내일이나 같다. 그래서 최단 경로 서비스는 교통을 무시하고 후보만 뽑고, 결과를 캐시한다.

반면 "지금 경부는 45분, 분당수서로는 30분"은 매 순간 바뀐다. 이건 ETA 서비스가 실시간 교통으로 계산한다. 변하지 않는 것과 변하는 것을 분리해야 캐시를 쓸 수 있다는 게 요점.

적응형 ETA와 경로 재탐색

교통 상황이 바뀌면 영향받는 활성 내비게이션 사용자를 찾아 ETA/경로를 다시 보내야 한다. 사용자별로 경로상의 모든 타일을 들고 있으면 탐색 비용이 크므로, 사용자 위치 타일부터 목적지를 포함할 때까지 상위 계층 타일을 올라가며 저장한다. 그러면 "교통 변화가 생긴 타일이 사용자의 가장 큰 타일 안에 있는가"만 확인하면 돼 필터링이 빨라진다.

예시로 보면 · 판교 IC 사고

판교 IC 근처에서 사고가 났다. 지금 내비를 켠 수백만 명 중 누구에게 경로 재계산을 보내야 할까?

  • 사용자마다 경로상의 작은 타일 수백 개를 저장하면 → 수백만 명 × 수백 개를 뒤져야 함
  • 대신 "현재 위치 타일 → 그걸 포함하는 더 큰 타일 → … → 목적지까지 포함하는 가장 큰 타일" 몇 개만 저장
  • "사고 난 타일이 이 사용자의 가장 큰 타일 안에 있나?"만 확인. 강원도에서 운전 중인 사람은 가장 큰 타일에 판교가 없으니 바로 제외

클라이언트 전달 프로토콜

방식평가
모바일 푸시 알림페이로드 크기 제한(iOS 4KB), 웹 앱 미지원 → 부적합
롱 폴링가능하지만 서버 자원 소모 큼
웹소켓양방향, 적은 오버헤드 → 책의 선택
SSE서버→클라이언트 단방향으로 충분하면 역시 가능

면접 포인트 — ① 타일은 "미리 만들고 CDN", ② 위치는 "일괄 전송 + 쓰기 최적 DB + 스트림 재활용", ③ 경로는 "최단 경로(캐시 가능)와 ETA(실시간)를 분리"한다는 세 문장으로 요약하면 전체 구조가 설명된다. 추가 논의 거리로는 다중 경유지 경로 같은 기능 확장이 있다.

4

분산 메시지 큐

전통적인 메시지 큐에 "데이터 장기 보관"과 "반복 소비"를 더한, 카프카 스타일의 이벤트 스트리밍 플랫폼을 설계한다. 성능의 핵심은 디스크 순차 쓰기와 일괄 처리, 신뢰성의 핵심은 복제와 ACK 설정.

왜 메시지 큐인가

결합도 완화

컴포넌트가 서로를 직접 알지 않아도 되어 독립적으로 갱신 가능.

규모 확장성

생산자·소비자를 트래픽에 맞춰 각각 늘리고 줄일 수 있음.

가용성

한 컴포넌트가 죽어도 나머지는 큐를 통해 계속 동작.

성능

비동기 통신으로 응답을 기다리지 않음.

메시지 큐 vs 이벤트 스트리밍 플랫폼: RocketMQ·ActiveMQ·RabbitMQ·ZeroMQ는 전통적 메시지 큐, 카프카·펄서는 이벤트 스트리밍 플랫폼으로 분류되지만 경계는 점점 흐려지고 있다. 이 장은 장기 보관·반복 소비가 가능한 쪽을 설계한다.

1단계 · 문제 이해와 설계 범위

기능 요구사항

생산자는 전송, 소비자는 소비. 메시지 반복 소비 가능. 오래된 데이터는 삭제(예: 2주 보관). 메시지 크기는 KB 수준. 생산 순서대로 소비. 전달 방식(최대 한 번/최소 한 번/정확히 한 번) 설정 가능.

비기능 요구사항

용도에 따라 높은 대역폭 또는 낮은 지연을 선택 가능. 트래픽 급증에 대응하는 분산 확장성. 데이터는 디스크에 지속·다중 복제.

전통적 큐와의 차이: 전통적 큐는 소비되면 메시지를 지우고 메모리 위주로 보관하며 순서 보장이 약하다. 여기서는 디스크에 오래 보관하고 순서를 보장해야 한다.

2단계 · 개략적 설계

메시지 모델

토픽 · 파티션 · 브로커

소비자 그룹 규칙 — 하나의 파티션은 같은 그룹 안에서 한 소비자만 읽을 수 있다. 그래서 순서가 보장되지만, 그룹 내 소비자 수가 파티션 수보다 많으면 노는 소비자가 생긴다. 파티션 수가 병렬 소비의 상한이다.

예시로 보면 · 주문 이벤트 토픽, 파티션 3개

hash(주문자ID) % 3으로 파티션을 고르면, 한 사용자의 "주문 생성 → 결제 → 취소" 이벤트는 항상 같은 파티션에 순서대로 쌓인다. 취소가 생성보다 먼저 처리될 일이 없다.

  • 배송 서비스 그룹에 소비자 3개 → 파티션 하나씩 담당. 4개로 늘리면 하나는 논다. 처리량을 올리려면 소비자보다 파티션을 먼저 늘려야 한다.
  • 정산 서비스 그룹은 같은 메시지를 따로 처음부터 다 읽는다. 이게 그룹 간 pub-sub.

전체 구조

flowchart LR
  P[생산자] --> B1
  P --> B2
  subgraph 브로커 클러스터
    B1[브로커 1
파티션들] B2[브로커 2
파티션들] end B1 --> CG[소비자 그룹] B2 --> CG B1 -.-> DS[(데이터 저장소)] B1 -.-> SS[(상태 저장소
소비 오프셋)] B1 -.-> MS[(메타데이터 저장소
토픽 설정)] CO[조정 서비스
주키퍼 / etcd] -.-> B1 CO -.-> B2

3단계 · 상세 설계

높은 대역폭을 위한 세 가지 원칙:

  1. 디스크 순차 접근: 회전식 디스크도 순차 쓰기는 매우 빠르고, OS 페이지 캐시도 적극 활용.
  2. 메시지 구조 불변: 생산자 → 큐 → 소비자로 갈 때 메시지 형식을 바꾸지 않아 불필요한 복사·변환을 없앰.
  3. 일괄 처리 우선: 네트워크 왕복과 디스크 쓰기 비용을 여러 메시지로 분산.

데이터 저장소 — 왜 WAL인가

트래픽 특성: 읽기·쓰기 모두 빈번, 갱신·삭제 없음(보관 기간 지나면 통째로 삭제), 순차 읽기·쓰기 위주.

선택지평가
DB (관계형/NoSQL)읽기·쓰기 모두 대규모로 빈번한 패턴에 맞추기 어려움 → 병목
쓰기 우선 로그(WAL)추가(append)만 하는 파일. 순차 쓰기에 최적 → 채택
예시로 보면 · 카톡 대화방

새 메시지는 항상 맨 아래에 붙기만 하고, 중간 메시지를 고치지 않는다. 디스크도 끝에만 붙이면 헤드가 왔다갔다 하지 않아 빠르다.

다만 파일 하나가 끝없이 커지면 오래된 걸 지우기 어려우니 예를 들어 1GB마다 파일을 끊는다(세그먼트). 보관 기간이 2주면 2주 지난 세그먼트 파일을 통째로 삭제하면 끝. 행 단위 DELETE가 필요 없다.

메시지 자료 구조

필드설명
key파티션 결정용(없으면 무작위). hash(key) % numPartitions
value페이로드
topic, partition소속 토픽·파티션
offset파티션 내 위치. (topic, partition, offset)으로 메시지 식별
timestamp, size, crc저장 시각, 크기, 무결성 검증용 체크섬

옵션 필드로 태그(필터링용) 등을 추가할 수 있다.

일괄 처리와 트레이드오프

배치를 크게 하면 대역폭이 오르지만 지연이 늘어난다. 지연이 중요한 서비스는 배치를 작게, 대역폭이 중요하면 크게 + 파티션 수를 늘려 보완한다.

생산자 흐름

소비자 흐름 — 푸시 vs 풀

푸시풀 (채택)
장점낮은 지연소비 속도를 소비자가 결정, 소비자가 느려도 큐가 대신 버팀. 일괄 처리에 적합
단점생산 속도가 소비 속도를 넘으면 소비자 과부하메시지가 없으면 헛도는 요청 → 롱 폴링으로 완화

소비자 재조정 (rebalancing)

예시로 보면 · 소비자 2번이 죽었을 때
  • 소비자 1·2·3이 파티션 0·1·2를 하나씩 맡고 있음
  • 코디네이터가 2번의 하트비트가 끊긴 걸 감지하고 재조정 시작
  • 리더 소비자가 "1번이 파티션 0·1, 3번이 파티션 2" 같은 새 배치안을 만들어 코디네이터에 전달, 코디네이터가 모두에게 공유
  • 1번은 상태 저장소에 기록된 파티션 1의 마지막 오프셋부터 이어서 읽음

상태 저장소 · 메타데이터 저장소

복제와 ISR

ACK 설정동작특성
ACK=allISR 전체가 받아야 응답가장 강한 지속성, 지연 가장 큼
ACK=1리더가 저장하면 응답리더 장애 시 유실 가능, 지연 개선
ACK=0응답을 기다리지 않음, 재시도 없음최저 지연, 유실 감수 (지표 수집 등)

소비자는 리더에서만 읽는다: 설계·운영이 단순하고, 한 파티션은 그룹 내 한 소비자만 읽으니 리더 연결 수가 적으며, 인기 파티션이 아니면 부하도 크지 않기 때문.

예시로 보면 · 리더 A, 팔로어 B·C
  • ACK=all: A·B·C 모두 저장해야 "OK". A가 바로 죽어도 B나 C에 있으니 유실 없음. 가장 느림.
  • ACK=1: A만 저장하면 "OK". B·C가 복제하기 전에 A가 죽으면 그 메시지는 사라짐.
  • ACK=0: 보내고 끝. CPU 사용률 같은 지표처럼 한두 개 빠져도 되는 데이터용.

ISR은 "B가 네트워크 문제로 한참 뒤처지면 all에서 B는 기다리지 말자"는 장치다. 뒤처진 레플리카를 ISR에서 빼서 느린 한 대 때문에 전체가 멈추지 않게 한다.

규모 확장성

대상방법
생산자그룹 조정이 필요 없어 인스턴스를 그냥 추가·삭제
소비자소비자 그룹 재조정으로 처리
브로커장애 시 컨트롤러가 사라진 레플리카를 다른 브로커에 재배치. 레플리카 수는 최소 개수 이상 유지, 같은 파티션 레플리카는 한 노드에 두지 않음. 브로커 추가 시엔 새 브로커에 레플리카를 먼저 복제한 뒤 기존 것을 제거(일시적으로 레플리카 수 초과 허용)
파티션늘릴 때: 기존 데이터는 옮기지 않고 새 메시지만 새 파티션에도 저장. 줄일 때: 해당 파티션은 새 메시지를 받지 않고, 보관 기간이 지난 뒤 삭제(그전까지 소비자는 읽을 수 있음)
예시로 보면 · 파티션 3개 → 4개

키 계산이 hash(key) % 4로 바뀌니 같은 주문자라도 새 메시지는 다른 파티션으로 갈 수 있다. 기존 데이터를 재배치하지 않는 대신, 소비자는 옛 파티션의 남은 메시지와 새 파티션 메시지를 모두 읽는다.

수 TB를 옮기는 것보다 훨씬 싸지만, 그 시점에는 키 단위 순서 보장이 흔들릴 수 있다는 점을 면접에서 같이 언급하면 좋다.

메시지 전달 방식

방식구현적합한 곳
최대 한 번생산자 ACK 기다리지 않음(ACK=0), 소비자는 처리 전에 오프셋 먼저 갱신지표 모니터링처럼 일부 유실 허용
최소 한 번ACK=1 또는 all + 재시도, 소비자는 처리 완료 후 오프셋 커밋 → 중복 가능중복 제거가 가능하거나 멱등 처리 가능한 대부분의 경우
정확히 한 번구현 복잡, 성능 비용 큼결제·회계 등 중복이 치명적인 곳
예시로 보면 · 오프셋을 언제 커밋하느냐

소비자가 메시지 100번을 가져와 포인트 적립을 처리한다.

  • 최대 한 번: 오프셋을 먼저 101로 커밋 → 적립. 적립 도중 죽으면 재시작 후 101번부터 읽으니 100번은 적립 안 됨(유실).
  • 최소 한 번: 적립 → 오프셋 커밋. 커밋 직전에 죽으면 100번을 또 읽어 두 번 적립(중복). 그래서 "주문번호로 이미 적립했는지 확인" 같은 멱등 처리를 붙인다.
  • 정확히 한 번: 처리와 커밋을 트랜잭션처럼 묶어야 해서 복잡하고 느림. 결제처럼 중복이 절대 안 되는 곳에만.

고급 기능

면접 포인트 — "파티션 = 순서와 병렬성의 단위", "WAL + 세그먼트 = 순차 I/O", "풀 방식 + 소비자 그룹 재조정", "ISR과 ACK로 지연/지속성 트레이드오프 조절" 네 가지를 연결해 설명하면 된다. 추가 논의 거리로는 통신 프로토콜(AMQP, 카프카 프로토콜), 소비 실패 시 재시도(재시도 토픽), 오래된 데이터를 HDFS·객체 저장소로 아카이브하는 방안이 있다.

두 장 비교해서 한 번에 기억하기

관점3장 구글 맵4장 분산 메시지 큐
쓰기 폭증 대응클라이언트 일괄 전송 + 카산드라생산자 버퍼 일괄 전송 + WAL 순차 쓰기
분할 단위지오해시 기반 타일 (계층형)토픽 → 파티션 → 세그먼트
미리 계산지도 타일, 라우팅 타일, 최단 경로 캐시해당 없음 (대신 형식 불변으로 복사 최소화)
실시간 경로카프카로 위치 스트림 → 교통·ETA 갱신시스템 자체가 스트림 인프라
핵심 트레이드오프정확도 vs 데이터/배터리 사용량대역폭 vs 지연, 지속성 vs 지연(ACK)