본문 바로가기
카테고리 없음

URL 단축 서비스란?

by SuldenLion 2026. 2. 25.
반응형
URL 단축 서비스 설계 | 소프트웨어 아키텍처 & 시스템
소프트웨어 아키텍처 & 시스템

URL 단축 서비스
설계 완전 분석

bit.ly, TinyURL, 그리고 수천만 건의 리다이렉트를 처리하는 서비스는 어떻게 설계될까? 요구사항 분석부터 해시 충돌 해결, 분산 캐시, DB 선택까지 — 실무 수준의 아키텍처를 파헤친다.

시스템 디자인 인터뷰 필수 문제 읽기 시간 약 15분 난이도 중급~고급
01

URL 단축 서비스란 무엇인가?

URL 단축 서비스는 긴 URL을 짧은 별칭(alias)으로 변환해주는 시스템이다. 예를 들어 https://www.example.com/very/long/path?with=many&query=params 같은 주소를 https://sho.rt/aB3kZ 처럼 간결하게 만들어준다.

사용자가 단축 URL에 접속하면 서버는 원본 URL을 조회하여 HTTP 리다이렉트로 이동시킨다. 표면적으로는 단순해 보이지만, 초당 수십만 건의 요청을 처리하고 전 세계에 일관된 응답을 돌려주는 시스템을 설계하는 것은 흥미로운 엔지니어링 도전이다.

~26B
bit.ly의 연간
처리 클릭 수 (추정)
7자
일반적인 단축 키
길이 (Base62 기준)
<10ms
캐시 히트 시
목표 응답 시간

이 서비스는 시스템 디자인 인터뷰에서 단골 문제로 등장한다. 단순한 외관 뒤에 해시 알고리즘, 충돌 처리, 분산 캐시, DB 선택, 수평 확장 등 시스템 설계의 핵심 개념이 고스란히 담겨 있기 때문이다.

02

요구사항 분석

기능 요구사항 (Functional Requirements)

🔗

URL 단축

긴 URL을 입력받아 고유하고 짧은 URL을 생성하여 반환한다.

↩️

리다이렉트

단축 URL로 접근 시 원본 URL로 HTTP 리다이렉트한다.

만료 (TTL)

선택적으로 만료 시간을 지정하고, 만료 후에는 404를 반환한다.

📊

접속 통계

클릭 수, 국가, 브라우저, 리퍼러 등의 분석 데이터를 수집한다.

✏️

커스텀 별칭

사용자가 원하는 단축 키워드를 직접 지정할 수 있다.

👤

사용자 관리

가입한 사용자는 자신이 만든 단축 URL을 조회·삭제할 수 있다.

비기능 요구사항 (Non-Functional Requirements)

항목 목표 수치 이유
가용성 99.99% 리다이렉트 실패는 즉각적인 UX 손상
읽기 지연 p99 < 50ms 브라우저 이동 경험에 직접적 영향
쓰기 지연 p99 < 500ms 단축 URL 생성은 빈도가 낮음
읽기:쓰기 비율 100:1 ~ 1000:1 읽기 최적화가 절대적 우선순위
URL 유일성 충돌 확률 0%에 수렴 잘못된 URL 노출은 심각한 버그
저장 용량 5년 기준 약 1억 건 URL 당 평균 500byte → 약 50GB

💡 규모 추정 (Back-of-the-envelope): 일일 1억 건 리다이렉트 요청 → 초당 약 1,160 RPS (평균), 피크 시 약 5,000 RPS. 일일 100만 건 단축 URL 생성 → 초당 약 12 WPS. 읽기가 쓰기의 약 100배. 이 비율이 아키텍처 전반의 의사결정 방향을 결정한다.

03

핵심 API 설계

REST API 방식으로 설계한다. 두 가지 핵심 엔드포인트만으로 서비스의 본질을 구현할 수 있다.

POST /api/v1/urls — URL 단축 생성

Request / Response JSON
// 요청 (Request Body) { "longUrl" : "https://example.com/very/long/path?id=42", "customKey" : "my-link", // 선택 사항 "expiresAt" : "2025-12-31T23:59:59Z" // 선택 사항 } // 응답 (201 Created) { "shortUrl" : "https://sho.rt/aB3kZ", "shortKey" : "aB3kZ", "longUrl" : "https://example.com/very/long/path?id=42", "expiresAt" : "2025-12-31T23:59:59Z", "createdAt" : "2024-01-15T10:23:00Z" }

GET /{shortKey} — 리다이렉트

HTTP Response
// 성공 시 HTTP/1.1 301 Moved Permanently Location: https://example.com/very/long/path?id=42 // 존재하지 않는 키 또는 만료된 키 HTTP/1.1 404 Not Found { "error": "URL not found or expired" }
04

단축 키 생성 전략 — 해시 vs. Base62

서비스의 심장부는 "원본 URL에서 고유한 단축 키를 어떻게 만드는가"이다. 크게 두 가지 접근법이 존재하며, 각각 장단점이 다르다.

접근법 1: 해시 기반 (MD5 + Truncation)

URL을 MD5로 해싱한 후 앞 7자를 잘라 사용한다. 구현이 간단하지만 충돌(Collision) 문제를 반드시 처리해야 한다. 같은 URL이 들어오면 항상 같은 키를 반환하는 멱등성(idempotency)은 자연스럽게 얻을 수 있다.

Python 의사코드
import hashlib def generate_key(long_url): hash_hex = hashlib.md5(long_url.encode()).hexdigest() key = hash_hex[:7] # 앞 7자만 사용 # 충돌 처리: DB에 다른 URL이 이미 존재하면 재시도 while db_exists(key) and db_get_url(key) != long_url: long_url += "_retry" # salt 추가 후 재해싱 hash_hex = hashlib.md5(long_url.encode()).hexdigest() key = hash_hex[:7] return key

접근법 2: Base62 인코딩 + 자동 증가 ID (권장)

DB의 auto-increment ID를 Base62(0~9, a~z, A~Z) 문자 집합으로 인코딩하는 방식이다. 7자리 Base62는 62의 7승 ≈ 3.5조 가지의 고유한 키를 표현할 수 있다. 충돌이 원천적으로 불가능하며, 순차 ID의 예측 가능성 문제는 UUID를 시드로 사용해 보완한다.

Python 의사코드
CHARSET = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" BASE = 62 def encode_base62(num): result = [] while num > 0: result.append(CHARSET[num % BASE]) num //= BASE return "".join(reversed(result)).zfill(7) # 사용 예시 db_id = db_insert_and_get_id(long_url) # 예: 123456789 short_key = encode_base62(db_id) # → "8m0Kx1a"
전략 충돌 멱등성 예측 가능성
MD5 + Truncation 가능 (처리 필요) O (같은 URL → 같은 키) 낮음
Base62 + Auto-ID 불가능 X (매번 새 키) 높음 (순차 예측)
Base62 + UUID 시드 불가능 X 낮음
🏆

실무 권장: Base62 + 분산 ID 생성기

Twitter의 Snowflake ID나 UUID v7 같은 분산 ID 생성기를 사용하면, 충돌 없이 예측 불가능한 키를 대규모 환경에서도 안정적으로 생성할 수 있다. 이 방식이 오늘날 대형 URL 단축 서비스의 사실상 표준이다.

05

전체 시스템 아키텍처

읽기 집약적(Read-Heavy) 특성에 최적화된 아키텍처를 설계한다. 단축 URL 생성(쓰기)과 리다이렉트(읽기) 경로를 분리하는 것이 핵심이다.

클라이언트
웹 브라우저
모바일 앱
3rd-party API
CDN / LB
CDN (CloudFront)
Load Balancer (L7)
API 서버
Redirect Service
Write Service
Analytics Service
캐시
Redis Cluster
Bloom Filter
DB
Primary DB (쓰기)
Read Replicas (읽기)
Message Queue (분석)

리다이렉트 요청 처리 흐름

클라이언트
CDN / LB
Redirect API
Redis 캐시
301 / 302
캐시 미스 시: DB Read Replica 조회 → 캐시 갱신 → 응답
06

데이터베이스 설계 및 선택

스키마 설계

SQL DDL
CREATE TABLE urls ( id BIGINT PRIMARY KEY AUTO_INCREMENT, short_key VARCHAR(10) UNIQUE NOT NULL, -- "aB3kZ" long_url TEXT NOT NULL, -- 원본 URL user_id BIGINT REFERENCES users(id), -- nullable expires_at TIMESTAMP NULL, -- TTL created_at TIMESTAMP DEFAULT NOW() ); -- 조회 최적화 인덱스 CREATE INDEX idx_short_key ON urls(short_key); CREATE INDEX idx_expires ON urls(expires_at) WHERE expires_at IS NOT NULL;

관계형 vs. NoSQL

기준 관계형 DB (PostgreSQL) NoSQL (DynamoDB)
접근 패턴 PK 조회로 충분 Key-Value 조회에 최적화
확장성 수직 확장 + Read Replica 네이티브 수평 확장 (샤딩 내장)
일관성 Strong Consistency Eventual Consistency
UNIQUE 제약 DB 레벨에서 보장 애플리케이션 레벨 처리 필요
추천 시나리오 초기 구축, 안정성 우선 수억 건 이상 초대규모 트래픽

대부분의 서비스 규모에서는 PostgreSQL + Read Replica 구성으로 충분하다. 초당 수만 건 이상의 쓰기가 발생하는 극단적 규모라면 Cassandra나 DynamoDB를 고려한다. 핵심은 "지금 필요한 복잡성"과 "미래 확장 여지" 사이의 균형을 잡는 것이다.

07

리다이렉트 전략 — 301 vs. 302

HTTP 상태 코드 선택은 단순한 세부사항처럼 보이지만, 분석 정확도와 서버 부하에 큰 영향을 미친다.

코드 의미 브라우저 동작 서버 부하 분석 정확도
301 Moved Permanently 결과 캐시 → 이후 서버 우회 낮음 부정확 (클릭 누락)
302 Found (임시 이동) 캐시 없음 → 매번 서버 요청 높음 정확 (모든 클릭 기록)
⚖️

실무적 판단 기준

클릭 통계와 분석이 비즈니스 핵심이라면 302를 선택한다. 서버 비용 절감이 최우선이라면 301 + CDN 캐시 조합을 사용한다. 많은 서비스가 기본값은 302로 설정하고, 비공개 링크는 301로 운용하는 혼합 전략을 취한다.

08

캐싱 전략

리다이렉트 서비스의 성능은 캐시 히트율이 결정한다. URL 접근 패턴은 파레토 법칙을 강하게 따른다 — 전체 URL의 20%가 전체 트래픽의 80%를 차지한다. 이 특성 덕분에 캐시는 극적인 효과를 발휘한다.

Redis 캐시 설계

Redis 로직 — Python 의사코드
# 키 구조: "url:{shortKey}" → 원본 URL # 예시: "url:aB3kZ" → "https://example.com/..." def redirect(short_key): cache_key = f"url:{short_key}" # 1단계: Redis 조회 long_url = redis.get(cache_key) if not long_url: # 캐시 미스 # 2단계: DB Read Replica 조회 record = db_replica.get(short_key) if not record: raise NotFoundException() long_url = record.long_url # 3단계: 캐시에 저장 (TTL 24시간) redis.setex(cache_key, 86400, long_url) # 4단계: 비동기 클릭 이벤트 발행 message_queue.publish("click.events", {short_key, timestamp, ip}) return redirect_302(long_url)

Bloom Filter로 존재하지 않는 키 차단

악의적 사용자나 봇이 무작위 단축 키로 반복 요청을 보내면, 캐시 미스가 연속으로 발생하여 DB에 불필요한 부하가 걸린다. Bloom Filter를 메모리에 두면, DB를 조회하기 전에 "이 키가 존재할 가능성이 없다"는 것을 O(1)로 판별할 수 있다. False Positive는 발생할 수 있지만, False Negative는 절대 발생하지 않으므로 안전하게 사용 가능하다.

09

확장성 — 대용량 트래픽 대응

수평 확장 (Horizontal Scaling)

Redirect Service는 상태(state)가 없는 stateless 서비스로 설계한다. 모든 상태는 Redis와 DB에 저장하기 때문에, 로드밸런서 뒤에 인스턴스를 무한히 추가하는 것만으로 확장이 가능하다.

DB 샤딩 (Sharding)

단일 DB로 감당할 수 없는 규모에 도달하면 short_key 기준으로 샤딩한다. short_key의 첫 글자를 해시하여 여러 DB 노드에 분산하는 방식이 일반적이다. 단, 샤딩은 운영 복잡도를 크게 높이므로 Read Replica + 캐시 최적화로 최대한 미루는 것이 현명하다.

Rate Limiting

단축 URL 생성 API에는 IP 기반 또는 사용자 기반의 Rate Limiting을 적용한다. Redis의 sliding window 카운터를 활용하면 분산 환경에서도 효율적으로 요청 수를 제한할 수 있다.

🌐

CDN 활용

인기 단축 URL의 301 응답을 엣지 노드에 캐싱하면, 원본 서버에 요청이 도달하지 않아 트래픽을 극적으로 줄일 수 있다.

🔀

쓰기/읽기 분리

Write Service와 Redirect Service를 별도 배포 단위로 분리하면 독립적인 스케일링과 장애 격리가 가능하다.

비동기 처리

클릭 이벤트 기록은 메시지 큐(Kafka, SQS)에 발행하고 비동기로 처리하여 리다이렉트 응답 시간에 영향을 주지 않는다.

10

분석 & 모니터링

URL 단축 서비스의 비즈니스 가치는 단순 리다이렉트가 아닌, 링크 성과에 대한 인사이트를 제공하는 데서 나온다. 분석 파이프라인은 메인 리다이렉트 경로와 철저히 분리되어야 한다.

클릭 이벤트 스키마 (JSON)
{ "event" : "url.clicked", "short_key" : "aB3kZ", "timestamp" : "2024-01-15T10:23:45.123Z", "ip_hash" : "sha256(ip + salt)", // 개인정보 익명화 "country" : "KR", // GeoIP 조회 "user_agent" : "Mozilla/5.0 ...", "referrer" : "https://twitter.com", "device" : "mobile" // UA 파싱 결과 }

이 이벤트는 Kafka 같은 메시지 큐로 발행되고, 컨슈머가 ClickHouse 또는 BigQuery에 배치로 적재한다. 실시간 집계가 필요하다면 Apache Flink나 Spark Streaming을 사용한다. 분석 서비스의 장애가 리다이렉트 서비스에 영향을 주어서는 절대 안 된다 — 큐를 활용한 비동기 분리가 그 이유다.

11

정리 및 핵심 교훈

URL 단축 서비스는 "단순한 CRUD 앱"처럼 보이지만, 대규모로 운영할 때는 시스템 설계의 핵심 원칙이 총집합된다. 이 설계 과정에서 배울 수 있는 핵심 교훈을 정리하면 다음과 같다.

📐

읽기/쓰기 비율을 먼저 파악하라

100:1의 읽기 편향은 캐시 최우선 전략과 Read Replica를 당연한 선택으로 만든다. 숫자가 아키텍처를 결정한다.

🔑

키 생성 전략은 확장성의 기반

Base62 + 분산 ID 생성기 조합은 충돌 없이 수십억 건을 처리할 수 있는 검증된 방식이다.

🧱

관심사를 분리하라

리다이렉트, 쓰기, 분석을 독립 서비스로 분리하면 각각 독립적으로 확장·배포·격리할 수 있다.

🐢

복잡성은 필요할 때만

샤딩은 단일 DB가 한계에 도달할 때 도입한다. 지금 필요 없는 복잡성은 내일의 부채가 된다.

📦

비동기는 강력한 무기

클릭 이벤트를 큐에 발행하는 것만으로 분석 시스템 장애로부터 핵심 경로를 완전히 보호할 수 있다.

🎯

캐시 히트율이 전부다

파레토 법칙 덕분에 LRU 캐시를 잘 설계하기만 해도 DB 부하의 80%를 차단할 수 있다.

💬 인터뷰 팁: 면접관은 완벽한 답을 원하지 않는다. 요구사항을 명확히 하고, 트레이드오프를 인식하며, 단계적으로 설계를 발전시키는 과정을 보고 싶어한다. "이 방식의 단점은 X이고, 규모가 커지면 Y로 전환할 수 있습니다" — 이런 사고 방식이 핵심이다.

소프트웨어 아키텍처 & 시스템 시리즈  |  URL 단축 서비스 설계

반응형

댓글