Chapter 08
URL 단축기 설계
bit.ly 같은 단축 URL 서비스. 해시 vs base62 인코딩, 충돌 처리, 리다이렉트 방식(301/302), 캐시.
URL 단축기는 3개 부품으로 만든다 — ID + base62 + 캐시
URL 단축기는 “긴 URL ↔ 짧은 키”의 매핑 문제다. 읽기:쓰기 = 10:1로 트래픽이 한쪽에 쏠리기 때문에, 모든 설계 결정이 읽기 경로 최적화로 흐른다. 아래 3개 부품을 조합하면 끝.
- ① 짧은 키 만들기 — 분산 ID 생성기(7장 Snowflake) + base62 인코딩. 충돌 검사 불필요.
- ② 매핑 저장 (쓰기 경로) — (id, shortKey, longUrl)을 DB에 INSERT.
- ③ 빠른 조회 (읽기 경로) — Cache-aside 패턴: Hot path는 캐시에서 1ms, Cold path는 DB → 캐시 적재.
왜 어려운가 — 사용 패턴과 규모
단축은 단순해 보이지만 사용 패턴(누가 어떻게 쓰는가)과 규모(36.5TB)가 모든 설계를 끌고 간다.
기본 기능
- 단축 URL 생성 — 긴 URL을 입력하면 훨씬 짧은 URL을 반환
- URL 리다이렉션 — 짧은 URL로 접속하면 원래 긴 URL로 보내준다
- 짧은 URL은 숫자·영문 알파벳으로 구성되고, 가능한 짧고 읽기 쉬워야 한다
면접에서 확정할 질문
- 트래픽 규모 (일일/초당 단축 요청 수, 읽기:쓰기 비율)
- 짧은 URL의 길이 / 알파벳 종류 / 사용자 정의 별칭(custom alias) 지원?
- URL 만료 정책 (영구? TTL?)
- HTTPS 강제? 분석 데이터 수집?
누가 쓰는가 — 단축자와 클릭자
시스템의 부하를 가늠하려면 누가 어떻게 쓰는지부터 명확히 한다. 단축자(URL을 줄이는 사람)와 클릭자(짧은 URL을 클릭하는 사람)는 보통 다른 사람이고, 빈도도 한참 다르다.
생성은 1번, 클릭은 N번. 그래서 클릭(읽기) 경로가 시스템의 90%를 차지하고, 아래의 “읽기:쓰기 = 10:1”이 자연스럽게 도출된다.
이 챕터의 표준 요구사항
| 항목 | 값 | 계산 |
|---|---|---|
| 일일 단축 URL 생성 수 | 1억 건 | 전제 |
| 초당 쓰기 (write QPS) | ≈ 1,160 | 10⁸ ÷ 86,400초 |
| 읽기:쓰기 비율 | 10 : 1 | 전제 |
| 초당 읽기 (read QPS) | ≈ 11,600 | 1,160 × 10 |
| 10년간 누적 레코드 | 3,650억 | 10⁸ × 365 × 10 |
| 평균 URL 길이 | 100 byte | 전제 |
| 10년 저장 용량 | ≈ 36.5 TB | 3,650억 × 100B |
→ 3,650억 레코드 × 36.5TB는 단일 메모리/단일 DB로 감당이 불가능하다. 자연히 샤딩·캐시·고성능 ID 발급이 필요해진다.
큰 그림 — API · 정책 · 단축 흐름
부품 하나씩 들어가기 전에, 시스템의 외형(API)과 핵심 정책(301 vs 302)을 잡는다. 단축 흐름은 한 장 다이어그램으로.
API 엔드포인트
- URL 단축 —
POST /api/v1/data/shorten- 요청:
{ longUrl: string } - 응답: 단축된 shortUrl
- 요청:
- URL 리다이렉션 —
GET /api/v1/shortUrl- 응답: HTTP 301 또는 302로 longUrl로 redirect
URL 리다이렉션 — 301 vs 302
| 코드 | 의미 | 캐싱 | 특징 |
|---|---|---|---|
| 301 | Permanent Redirect | 브라우저가 영구 캐싱 | 서버 부담 ↓ · 클릭 분석 불가 |
| 302 | Found (Temporary) | 매번 서버 거침 | 서버 부담 ↑ · 클릭 분석 가능 |
서버 부하 절감이 우선이면 301, 클릭 추적·A/B 테스트가 필요하면 302. bit.ly 같은 분석 위주 서비스는 기본적으로 302를 선호한다.
단축 흐름 한눈에
긴 URL이 들어가면 짧은 URL이 나온다. 그 사이의 “변환 로직”이 다음 섹션의 주제다.
부품 ① — 짧은 키 만들기 (ID + base62)
짧은 키는 결국 “긴 URL 또는 정수 ID를 짧은 문자열로 매핑”하는 문제다. 여기에 두 가지 큰 접근법이 있다 — 어떤 길로 갈지가 이 부품의 핵심 결정.
데이터 모델
1단계에서 본 36.5TB는 인메모리에 못 담는다. 관계형 DB의 단순한 테이블이 자연스러운 출발점:
| 컬럼 | 타입 | 설명 |
|---|---|---|
| id | BIGINT (PK) | 단조 증가 ID (Snowflake 등) |
| shortURL | VARCHAR(7) | base62 인코딩된 짧은 키 (인덱스) |
| longURL | VARCHAR(2048) | 원래 URL |
Q. 그럼 longURL은 어떻게 알아내?
A. 이 테이블이 진실의 원천(source of truth)이다. shortKey만 보고 longURL을 “계산”하는 게 아니라, 테이블에서 찾아오는 것(lookup)이다. base62는 정수 ↔ 짧은 문자열 변환만 담당하지, longURL과는 무관하다.
해시 값 길이 — 7자가 정답인 이유
- 알파벳:
[0-9]+[a-z]+[A-Z]= 62자 (= base62) - 7자 → 62⁷ ≈ 3.5조 가지
- 10년치 트래픽(3,650억)을 흡수하고도 충분한 여유. 6자(62⁶ ≈ 568억)는 10년 안에 고갈
해시 함수 후보
| 해시 | 출력 | 특징 |
|---|---|---|
| CRC32 | 32 bit | 빠름, 충돌 잦음 (≈ 42억 가지) |
| MD5 | 128 bit | 충돌 거의 없음, 길다 (32 hex) |
| SHA-1 | 160 bit | 충돌 더 적음, 더 길다 (40 hex) |
어느 쪽이든 출력의 일부(앞 7자)만 잘라 쓰면 충돌이 발생한다. 그래서 “충돌을 어떻게 다룰 것인가”가 핵심.
접근법 ① — 해시 후 충돌 해소
- longUrl을 해시 (CRC32/MD5/SHA-1 등)
- 앞 7자를 잘라 shortKey로 사용
- DB에 같은 shortKey가 있으면 충돌 → longUrl 뒤에 미리 정한 문자열을 붙여 다시 해시 → 반복
- 장점: 같은 longUrl은 항상 같은 shortKey로 결정적
- 단점: 매 요청마다 DB 조회로 충돌 검사 → 비쌈. 트래픽 증가에 따라 조회 비용도 증가
접근법 ② — base62 변환 (유일 ID 생성기 사용)
Q. 왜 굳이 새로운 ID를 만드나? longURL을 바로 해시하면 안 되나?
A. 접근법 ①처럼 longURL을 직접 해시하면 충돌 검사라는 비싼 단계를 피할 수 없다. 매 요청마다 DB를 뒤져야 한다. 그래서 longURL은 무시하고 유일성이 보장된 ID를 새로 발급받는 쪽이 훨씬 싸다.
- 분산 ID 생성기(7장 Snowflake 등)에서 정수 ID 발급
- 그 정수를 base62로 변환 → shortKey
- 충돌 검사 불필요 (ID 자체가 유일)
- 장점: 충돌 없음, DB 조회 없음, 빠름
- 단점: ID가 단조 증가해 다음 키 추측이 쉬움 (보안 우려). 같은 longUrl이라도 매번 다른 shortKey 발급 가능
base62는 “해시 결과를 줄이는 압축”이 아니다
base62 = 정수를 짧은 문자열로 바꾸는 진법 변환. 알파벳을 [0-9] 10개에서 [0-9a-zA-Z] 62개로 늘려, 같은 숫자를 더 적은 자릿수로 표현한다. 입력은 해시가 아니라 정수.
ID 생성기는 “해시”가 아니다
CRC32 같은 해시는 충돌이 가능. Snowflake는 타임스탬프 + 머신 ID + 일련번호로 결정적 유일성을 보장한다. 확률적이 아니라 설계적으로 충돌이 없으니 DB 검사 단계가 사라진다.
두 접근법 비교
| 차원 | ① 해시 후 충돌 해소 | ② base62 변환 |
|---|---|---|
| 충돌 처리 | DB 조회로 매번 검사 필요 | 불필요 (ID 유일) |
| 길이 보장 | 고정 7자 | ID 크기에 따라 가변 (점점 길어짐) |
| 결정성 | 같은 longUrl → 같은 short | 호출마다 다른 short 가능 |
| 보안 (다음 키 추측) | 어려움 (해시 기반) | 쉬움 (단조 증가) |
| 성능 | 충돌 검사로 변동 | 예측 가능, 빠름 |
| 필요한 인프라 | 해시 함수만 | 분산 ID 생성기 (7장) |
책은 ② base62 변환을 권장한다. 7장에서 만든 ID 생성기를 그대로 재사용할 수 있어 챕터 간 자연스러운 연결.
인터랙티브 — base62 변환 직접 해보기
🔢 Base62 변환기
알파벳 [0-9a-zA-Z] 62자를 사용. 숫자 ↔ base62 양방향 변환. BigInt 기반이라 Snowflake ID 같은 큰 수도 처리한다.
| 길이 | 62ⁿ | 대략 |
|---|---|---|
| 5자 | 916,132,832 | 916.1백만 |
| 6자 | 56,800,235,584 | 56.8억 |
| 7자 | 3,521,614,606,208 | 3.5조← 책의 선택 |
| 8자 | 218,340,105,584,896 | 218.3조 |
| 9자 | 13,537,086,546,263,552 | 13537.1조 |
부품 ② — 단축 파이프라인 (쓰기 경로)
Shortener 서비스 안에서 일어나는 일을 3단계 변환 파이프라인으로 본다. 각 단계가 받는 값과 내놓는 값이 또렷이 보이도록.
핵심: ① ID 생성기가 유일성을 보장 → ② base62로 짧게 만든 뒤 → ③ DB가 진실의 원천. 충돌 검사 불필요.
부품 ③ — Cache-aside 읽기 경로
읽기:쓰기 = 10:1이므로 읽기 경로 최적화가 핵심이다. 답은 “캐시 먼저, DB는 fallback”이라는 cache-aside 패턴. Hot path와 Cold path를 나란히 본다.
핵심: 캐시는 처음엔 비어있지만, 미스가 한 번 나면 다음부턴 Hot path. 시간이 갈수록 Hot path 비중이 자연히 커지는 게 cache-aside의 장점.
두 방향의 비대칭 — 의도된 설계
단축기(쓰기)와 리다이렉션(읽기)이 동시에 빠를 필요는 없다. 트래픽이 한쪽으로 쏠리기 때문이다.
| 방향 | 방법 | 속도 |
|---|---|---|
| shortURL → longURL (리다이렉션) | shortKey로 인덱스 조회 + 캐시 | ✅ 빠름 (~1ms) |
| longURL → shortURL (역검색) | longURL 컬럼에 인덱스 없음 → 풀 스캔 | ❌ 사실상 불가 |
Q. longURL을 알아도 shortURL을 찾기는 어렵겠네?
A. 맞다. 인덱스를 한 방향(shortKey)으로만 걸기 때문. 추가로 접근법 ②는 같은 longURL이라도 호출마다 다른 shortURL을 발급할 수 있어 유일한 답이 존재하지 않을 수도 있다. 의도된 비대칭이고, 읽기 패턴(10:1)에 맞춘 설계다.
마무리 — 운영에서 짚을 것들
핵심 설계가 끝났으니, 운영·확장 측면에서 추가로 다룰 주제들을 짚는다.
① 처리율 제한 장치 (Rate Limiter)
- 악의적 사용자가 짧은 URL을 무한 발급해 ID 공간을 갉아먹는 것 방지
- IP 기반 / 사용자 기반 처리율 제한 (4장과 연결)
② 웹 서버 규모 확장
- Shortener·Redirect 서비스를 stateless로 설계 → 로드밸런서 뒤 다중 인스턴스로 수평 확장
③ DB 규모 확장
- 샤딩: shortKey 또는 id 기준으로 파티셔닝 (5장 컨시스턴트 해싱 활용 가능)
- 복제: 읽기 분산용 read replica
④ 데이터 분석 솔루션
- 클릭 수, 출처(Referer), 지역, 디바이스 등 수집 → 데이터 파이프라인 (Kafka → 데이터 웨어하우스)
- 302 리다이렉션 사용해야 분석 가능 — 트레이드오프 재확인
⑤ 가용성·일관성·안정성
- 가용성: 멀티 AZ/멀티 리전 배포, 캐시·DB 복제
- 일관성: 단축 URL 발급은 강한 일관성, 분석 데이터는 결과적 일관성으로 충분
- 안정성: 단일 장애점 제거, 헬스 체크 + 자동 복구
핵심 한 줄
URL 단축기는 “긴 URL ↔ 짧은 키” 매핑 문제이고, 짧은 키는 분산 ID + base62 인코딩으로 충돌 없이 빠르게 만든다. 이후 모든 확장은 읽기 경로(캐시) 최적화에 집중된다.