
이전 포스트: [대규모 시스템 설계 스터디] 7장 정리
다음 포스트: [대규모 시스템 설계 스터디] 9장 정리
URL 단축 서비스는 긴 URL을 짧은 형태로 변환하고, 사용자가 단축 URL에 접근하면 다시 원래 주소로 이동시키는 서비스다.
예를 들어
https://example.com/very/long/address
↓
https://tinyurl.com/2TX
처럼 긴 주소 대신 짧은 URL을 사용할 수 있도록 만든다.
겉으로 보면 단순하게 문자열 하나를 짧게 바꾸는 기능처럼 보이지만, 실제 대규모 서비스에서는
등을 함께 고려해야 한다.
우선 URL 단축 서비스가 제공해야 할 핵심 기능을 정리한다.
사용자가 긴 URL을 입력하면 짧은 URL을 생성한다.
Long URL
↓
URL Shortener
↓
Short URL
사용자가 생성된 단축 URL에 접근하면 원본 URL로 이동시킨다.
Short URL
↓
URL Shortener
↓
Original URL
서비스 규모가 커지더라도 계속 요청을 처리할 수 있어야 한다.
즉
대량 Request
+
서버 장애
+
데이터 증가
상황에서도 서비스가 정상적으로 동작하도록 설계해야 한다.
설계 전에 어느 정도 규모의 시스템인지 계산해본다.
이번 장에서는 다음과 같이 가정한다.
하루 URL 생성
= 1억 개
이를 초당 요청으로 환산하면 약
100,000,000 / 86,400
≈ 1,160 Write / sec
이다.
URL 단축 서비스는 URL을 만드는 요청보다 기존 URL을 조회하는 요청이 훨씬 많다고 가정한다.
읽기와 쓰기의 비율을
Read : Write
= 10 : 1
로 잡으면
Read QPS
≈ 11,600
정도가 된다.
즉 URL 단축 서비스는 쓰기보다 읽기가 많은 시스템이다.
이 특징은 뒤에서 캐시를 도입하는 중요한 근거가 된다.
하루에 1억 개의 URL을 만들고 이를 10년 동안 보관한다고 가정한다.
1억 × 365 × 10
≈ 3,650억 Records
원본 URL 평균 크기를 약 100Byte로 잡으면
3,650억 × 100Byte
≈ 36.5TB
정도의 저장 공간이 필요하다.
실제 시스템에서는 추가적인 메타데이터와 인덱스 등의 공간도 필요하겠지만, 개략적인 규모를 파악하기 위한 계산이다.
URL 단축 서비스에서는 크게 두 가지 API가 필요하다.
URL 생성
+
URL 조회
REST 형태로 생각해볼 수 있다.
사용자가 원본 URL을 전달하면 새로운 단축 URL을 생성한다.
POST /api/v1/data/shorten
요청 예시
{
"longUrl": "https://example.com/articles/2026/very-long-address"
}
서버는 이에 대응하는 단축 URL을 반환한다.
https://tinyurl.com/abc1234
사용자가 단축 URL에 접근하면 서버에서 원본 URL을 조회한다.
개념적으로는
GET /{shortKey}
와 같이 단축 키를 이용한 요청으로 볼 수 있다.
예를 들어
https://tinyurl.com/abc1234
에 접근하면 서버는
abc1234
↓
Original URL 조회
↓
https://example.com/...
과정을 거친다.
이후 브라우저에 Redirect 응답을 반환한다.
사용자가 단축 URL을 클릭하면 다음과 같은 흐름이 발생한다.
Short URL 요청
↓
URL Shortener
↓
Original URL 조회
↓
Redirect Response
↓
Original Website
Redirect 응답에서는 대표적으로 301 또는 302 상태 코드를 사용할 수 있다.
해당 URL이 새로운 URL로 영구적으로 이동했다는 의미다.
Short URL
↓
301
↓
Original URL
브라우저나 중간 캐시가 Redirect 결과를 저장할 수 있다.
따라서 이후 동일한 단축 URL에 접근할 때 URL 단축 서버를 거치지 않고 기존에 저장된 Redirect 정보를 활용할 가능성이 있다.
URL Shortener 요청 감소
→ 서버 부하 감소
하지만 사용자가 URL 단축 서버를 다시 방문하지 않는다면 클릭 횟수를 서버에서 직접 집계하기 어려워질 수 있다.
일시적인 Redirect를 나타낸다.
Short URL
↓
302
↓
Original URL
단축 URL을 계속 유지하면서 목적지로 보내야 하는 상황에서 활용할 수 있다.
요청이 URL Shortener를 거치는 구조라면
등을 처리하기 편하다.
다만 302라고 해서 항상 절대로 캐시되지 않는다고 단정할 수는 없으며, 실제 캐시 동작은 응답 헤더 등의 정책에도 영향을 받을 수 있다.
URL Shortener가 기억해야 할 핵심 데이터는 다음과 같다.
Short Key → Long URL
예를 들어
2TX
→
https://example.com/very/long/address
형태다.
이를 Key-Value 구조로 바라보면 매우 단순하다.
Key = 2TX
Value = Original URL
처음에는 단축 문자열 자체에서 원본 URL을 다시 복원하는 방식이라고 생각할 수도 있다.
하지만 여기서 다루는 구조는 그렇지 않다.
zn9edcu
↓
Database 조회
↓
Original URL
즉 zn9edcu라는 단축 키를 이용해서 DB에 저장해둔 원본 URL을 찾는 방식이다.
따라서 서버는 다음과 같은 매핑 정보를 저장해야 한다.
short_key
↔
long_url
애플리케이션 내부의 Hash Table을 사용하면 매우 빠르게 데이터를 조회할 수 있다.
하지만 일반적인 Hash Table은 메모리에 존재하기 때문에 대규모 URL 데이터를 영구적으로 저장하는 용도로는 한계가 있다.
따라서 원본 URL 매핑 정보는 데이터베이스에 저장하고, 자주 조회하는 데이터만 캐시에 보관하는 구조를 생각할 수 있다.
Database
→ 영구 저장
Cache
→ 빠른 조회
단순한 테이블을 만든다면 다음과 같은 구조를 생각할 수 있다.
ID
short_key
long_url
예를 들어
| ID | short_key | long_url |
|---|---|---|
| 1001 | abc1234 | https://example.com/... |
와 같은 형태다.
이때 short_key는 중복되면 안 된다.
서로 다른 두 원본 URL이 동일한 단축 키를 가지면 어떤 URL로 Redirect해야 할지 판단할 수 없기 때문이다.
따라서 DB 수준에서도 UNIQUE 제약조건을 사용할 수 있다.
CREATE UNIQUE INDEX idx_short_key
ON url_mapping(short_key);
즉 애플리케이션 로직뿐 아니라 데이터베이스에서도 최종적으로 중복을 막는다.
핵심 문제는 다음과 같다.
긴 URL을 어떤 짧은 문자열과 연결할 것인가?
단축 키는 다음 문자들을 사용한다고 가정한다.
0 ~ 9 → 10개
a ~ z → 26개
A ~ Z → 26개
총
10 + 26 + 26
= 62개
의 문자를 사용할 수 있다.
약 3,650억 개 이상의 URL을 표현해야 한다고 가정한다.
6자리 Base62 문자열은 표현 가능한 경우의 수가 부족하지만
62^7
은 수조 개의 조합을 만들 수 있다.
따라서 약 7자리 정도의 단축 키를 사용할 수 있다.
abc123A
z9K20Qa
...
이번 장에서는 크게 두 가지 방법을 비교한다.
1. Hash + Collision Resolution
2. Unique ID + Base62
두 방법 모두 짧은 키를 만들 수 있지만 접근 방식이 다르다.
원본 URL에 해시 함수를 적용한다.
예를 들어
Long URL
↓
MD5 / SHA / CRC32
↓
Hash Value
와 같은 구조다.
문제는 일반적인 Hash 결과가 단축 URL에 사용하기에는 너무 길다는 것이다.
따라서 결과 중 일부만 사용한다.
전체 Hash
↓
앞 7글자 추출
↓
abc1234
Hash 결과를 일부만 사용하면 서로 다른 URL이 같은 단축 키를 만들 가능성이 생긴다.
예를 들어
URL A
→ abc1234
URL B
→ abc1234
가 발생할 수 있다.
따라서 충돌을 확인하고 다시 생성하는 과정이 필요하다.
예를 들어 다음과 같이 처리할 수 있다.
Long URL
↓
Hash
↓
abc1234
↓
DB 확인
↓
이미 존재
↓
Salt 추가
↓
다시 Hash
↓
k9x82Qa
과정은 다음과 같다.
애플리케이션에서 미리 DB를 조회했다고 해서 충돌이 완전히 사라지는 것은 아니다.
여러 서버가 동시에 같은 Key가 비어 있다고 판단할 수도 있기 때문이다.
따라서 실제 저장 단계에서도
Short Key 생성
↓
INSERT
↓
UNIQUE 충돌?
↙ ↘
No Yes
↓ ↓
완료 새 Key 생성
형태로 처리할 수 있다.
즉 DB의 UNIQUE 제약조건이 마지막 안전장치 역할을 한다.
Short Key의 존재 가능성을 빠르게 판단하기 위해 Bloom Filter와 같은 자료구조를 보조적으로 고려할 수도 있다.
새 Short Key
↓
Bloom Filter
↓
절대 없음
→ 저장 시도
있을 수도 있음
→ 추가 확인
Bloom Filter는
"없다"
→ 확실하게 없음
"있다"
→ 실제로는 없을 수도 있음
이라는 특징을 가진다.
따라서 불필요한 조회를 줄이기 위한 보조 수단으로 활용할 수 있다.
다만 최종적인 중복 여부는 실제 저장소의 상태를 확인해야 한다.
두 번째 방법은 원본 URL을 바로 Base62로 변환하는 것이 아니다.
먼저 각 URL에 전역적으로 유일한 숫자 ID를 부여한다.
Long URL
↓
Unique ID Generator
↓
2009215674938
그리고 이 숫자를 Base62 문자열로 변환한다.
2009215674938
↓
Base62
↓
zn9edcu
이 zn9edcu가 Short Key가 된다.
Base62는 62개의 문자를 숫자처럼 사용하는 방식이다.
예를 들어
0~9
a~z
A~Z
를 하나의 숫자 체계로 사용한다.
10진수를 62로 계속 나누면서 나머지를 문자로 변환한다.
예를 들어 11157을 변환하면
11157 ÷ 62
→ 몫 179
→ 나머지 59
179 ÷ 62
→ 몫 2
→ 나머지 55
2 ÷ 62
→ 몫 0
→ 나머지 2
나머지를 대응되는 문자로 바꾸고 역순으로 읽으면 Short Key가 만들어진다.
예시에서는
2TX
와 같은 결과를 얻을 수 있다.
Long URL
↓
Unique Numeric ID 생성
↓
Base62 변환
↓
Short Key
↓
DB 저장
예를 들어
Original URL
https://en.wikipedia.org/wiki/Systems_design
에
ID
2009215674938
을 발급한다.
이 숫자를 Base62로 변환해
zn9edcu
라는 Short Key를 만든다.
그러면 DB에는 다음과 같이 저장된다.
| ID | short_key | long_url |
|---|---|---|
| 2009215674938 | zn9edcu | https://en.wikipedia.org/wiki/Systems_design |
최종 URL은
https://tinyurl.com/zn9edcu
가 된다.
Long URL
↓
Hash
↓
일부 문자 추출
↓
충돌 검사
↓
Short Key
URL 자체를 이용해 Key를 만든다.
하지만 충돌 관리가 필요하다.
Long URL
↓
Unique Numeric ID
↓
Base62
↓
Short Key
유일한 숫자 ID가 보장된다면 Base62 결과 역시 유일하게 만들 수 있다.
대신 유일한 숫자 ID를 생성하는 시스템이 필요하다.
지난 장에서 학습했던 분산 ID 생성기와도 연결할 수 있는 부분이다.
이번에는 Base62 방식을 이용한다고 가정한다.
전체적인 URL 생성 과정은 다음과 같다.
Long URL 입력
↓
기존 등록 여부 확인
↙ ↘
있음 없음
↓ ↓
기존 URL 반환 Unique ID 생성
↓
Base62
↓
DB 저장
↓
Short URL 반환
조금 더 구체적으로 보면
과정이다.
URL 단축 서비스는 쓰기보다 읽기 요청이 훨씬 많은 서비스다.
앞에서
Read : Write
= 10 : 1
이라고 가정했다.
따라서 사용자가 단축 URL을 클릭할 때마다 항상 DB를 조회하면 DB 부하가 커질 수 있다.
이를 줄이기 위해 Cache를 사용할 수 있다.
자주 사용되는
Short Key
→
Original URL
매핑을 Redis 등의 캐시에 저장한다.
전체적인 흐름은 다음과 같다.
Short URL 요청
↓
Load Balancer
↓
Web Server
↓
Cache 조회
↙ ↘
Hit Miss
↓ ↓
Redirect DB 조회
↓
Cache 저장
↓
Redirect
캐시에 Short Key가 존재한다면
zn9edcu
↓
Cache
↓
Original URL
즉시 원본 URL을 찾아 Redirect할 수 있다.
DB 조회가 필요하지 않기 때문에 응답 속도가 빠르고 DB 부하도 줄어든다.
캐시에 데이터가 없다면 DB에서 조회한다.
Cache Miss
↓
Database
↓
Original URL 발견
↓
Cache 저장
↓
Redirect
다음번 동일한 URL 요청에서는 캐시에서 바로 처리할 수 있다.
Cache에도 없고 DB에도 해당 Short Key가 존재하지 않는다면 정상적인 URL이 아니다.
Short Key
↓
Cache 없음
↓
DB 없음
↓
오류 반환
잘못 입력했거나 존재하지 않는 단축 URL일 수 있으므로 적절한 오류 응답을 반환한다.
여기서 Load Balancer가 Cache를 조회한다고 생각하면 안 된다.
Load Balancer의 역할은 요청을 여러 웹 서버로 분산하는 것이다.
Client
↓
Load Balancer
↙ ↘
Web Server 1 Web Server 2
↓ ↓
└── Cache / DB ──┘
실제 Short Key 조회는 웹 서버나 관련 서비스가 수행한다.
지금까지의 내용을 하나로 연결하면 다음과 같은 구조를 생각할 수 있다.
Client
↓
Load Balancer
↓
┌────────────────┐
│ Web Servers │
└────────────────┘
↓ ↓
Cache Database
URL 생성 요청에서는
Client
↓
Web Server
↓
ID Generator
↓
Base62
↓
Database
과정을 사용하고,
URL 조회 요청에서는
Client
↓
Web Server
↓
Cache
↓ Miss
Database
구조를 사용할 수 있다.
기본적인 URL Shortener를 만들었다면 다음 단계는 확장성을 고려하는 것이다.
악의적인 사용자나 Bot이 짧은 시간에 매우 많은 URL 생성 요청을 보낼 수 있다.
Bot
↓↓↓↓↓↓↓↓↓↓
URL 생성 Request
이 경우 앞에서 학습한 Rate Limiter를 이용해 사용자 또는 IP 단위로 URL 생성 횟수를 제한할 수 있다.
웹 계층을 Stateless하게 구성하면 트래픽에 따라 웹 서버를 추가할 수 있다.
Web Server 1
Web Server 2
Web Server 3
...
Load Balancer는 이 서버들에 요청을 분산한다.
데이터가 계속 증가한다면 하나의 DB로 처리하기 어려워질 수 있다.
이 경우
Replication
또는
Sharding
등의 방법을 고려할 수 있다.
URL 단축 서비스에서는 단순히 Redirect만 하는 것이 아니라 클릭 데이터를 분석할 수도 있다.
예를 들어
등을 수집하면 서비스 분석에도 활용할 수 있다.
URL Shortener가 대규모 서비스라면
Web Server 장애
Database 장애
Cache 장애
등의 상황도 고려해야 한다.
따라서
등을 함께 설계해야 한다.
이번 장의 전체 흐름을 연결하면 다음과 같다.
긴 URL을 짧게 만들고 싶음
↓
Short Key 필요
↓
어떻게 생성할 것인가?
↓
Hash
vs
Unique ID + Base62
↓
Short Key 생성
↓
DB에
Short Key ↔ Long URL 저장
↓
사용자가 Short URL 클릭
↓
Load Balancer
↓
Web Server
↓
Cache 조회
↓
Cache Hit
→ 바로 Redirect
↓
Cache Miss
→ DB 조회
→ Cache 저장
→ Redirect
↓
트래픽 증가
↓
Rate Limiter
Web Server Scale-out
DB Replication / Sharding
결국 URL 단축 서비스의 핵심은 단순히 문자열을 짧게 만드는 것이 아니다.
원본 URL에 대응하는 짧고 유일한 식별자를 생성하고, 이를 안정적으로 저장한 뒤 대량의 Redirect 요청을 빠르게 처리하는 시스템을 만드는 것
이라고 이해할 수 있다.
처음에는 URL 단축 서비스라고 하면
긴 URL
↓
Hash
↓
짧은 URL
정도로 생각했다.
하지만 실제로는 Hash 결과에서 원래 URL을 복원하는 것이 아니라
Short Key
↓
DB / Cache 조회
↓
Original URL
이라는 구조라는 점이 가장 먼저 정리되었다.
또 Short Key를 만드는 방법도 하나만 존재하는 것이 아니었다.
Hash 방식
→ URL 자체를 이용
→ Collision 처리 필요
Base62 방식
→ Unique ID를 먼저 생성
→ 짧은 문자열로 인코딩
처럼 서로 다른 접근 방법이 존재한다.
특히 지난 장에서 배운 분산 ID 생성기를 이용하면 이번 장의 Base62 방식에서 필요한 Unique Numeric ID를 생성할 수 있다는 점에서 이전 내용과 연결되는 느낌이 들었다.
또 URL 단축 서비스는 생성보다 Redirect 요청이 훨씬 많기 때문에
Database
+
Cache
구조를 사용하여 읽기 요청을 빠르게 처리한다는 점도 중요했다.
결국 이번 장 역시 하나의 기능만 보는 것이 아니라
API
→ ID 생성
→ DB
→ Cache
→ Load Balancer
→ Rate Limiter
→ Scale-out
처럼 이전에 학습한 여러 시스템 구성 요소들이 하나의 실제 서비스 안에서 어떻게 연결되는지를 확인할 수 있었던 장이었다.