URL 단축기 설계 — 가장 쉬워 보이지만 가장 자주 틀리는 문제
왜 쉬워 보이는 문제가 가장 자주 틀리는가
URL 단축기는 시스템 디자인 인터뷰 입문 문제로 자주 나옵니다. 그런데 역설적으로 이 문제에서 얕은 답변이 유독 많이 나옵니다. "해시 함수로 짧은 코드를 만들고 DB에 저장하면 끝"이라는 인상 때문에 깊이 파고들 여지가 없다고 생각하기 쉽습니다. 실제로는 짧은 코드를 어떻게 생성할지 하나만 놓고도 트레이드오프가 여러 겹입니다. 이 챕터에서는 앞서 다룬 요구사항 정리, 추정, 캐싱, DB 챕터의 내용을 이 한 문제에 실제로 적용해봅니다.
요구사항 정리부터
기능 요구사항은 단순합니다. 긴 URL을 넣으면 짧은 코드를 반환하고, 짧은 코드로 접속하면 원래 URL로 리다이렉트합니다. 비기능 요구사항이 이 문제의 실제 난이도를 결정합니다. 리다이렉트는 읽기 경로이고 생성은 쓰기 경로인데, 실제 서비스에서는 리다이렉트 요청이 생성 요청보다 압도적으로 많습니다. 대략 100:1 정도로 가정하고 시작하면 됩니다. 이 비율이 바로 "읽기 중심 시스템"이라는 설계 방향을 결정합니다 — 캐싱이 이 문제의 핵심이 될 것이라는 신호입니다.
추정을 간단히 해보면, 하루 1억 건의 리다이렉트가 발생하는 서비스라면 평균 QPS는 약 1,150이고, 생성 요청은 그 1/100인 초당 약 11건입니다. 쓰기는 여유롭고 읽기가 병목이라는 그림이 확정됩니다.
짧은 코드는 어떻게 만드는가
두 가지 접근이 있습니다. 하나는 긴 URL을 해시(MD5 등)해서 앞 7자리를 쓰는 방식, 다른 하나는 자동 증가하는 숫자 ID를 Base62로 인코딩하는 방식입니다.
import string
BASE62 = string.digits + string.ascii_lowercase + string.ascii_uppercase
def encode_base62(num: int) -> str:
if num == 0:
return BASE62[0]
chars = []
while num > 0:
num, rem = divmod(num, 62)
chars.append(BASE62[rem])
return "".join(reversed(chars))
# 전역 카운터(또는 분산 ID 생성기)에서 발급받은 고유 정수를 인코딩
# 예: 125214 -> "wZ1" (7자리면 62^7 ≈ 3.5조 개의 조합)해시 방식은 같은 URL을 여러 번 줄이면 같은 코드가 나온다는 장점이 있지만, 해시 충돌이 나면 뒷자리를 늘리거나 재시도해야 해서 로직이 복잡해집니다. Base62 인코딩 방식은 충돌이 원천적으로 없다는 게 장점이지만, 전역적으로 겹치지 않는 정수 ID를 어떻게 발급할지가 새로운 문제가 됩니다. 서버가 한 대면 auto-increment로 끝나지만, 서버가 여러 대라면 ID 발급 자체를 별도 서비스(예: 범위를 미리 배분받는 ID 생성기)로 분리해야 합니다. "짧은 코드 생성"이라는 단순해 보이는 문제 하나가 결국 분산 ID 생성이라는 별도 설계 주제로 이어지는 겁니다.
리다이렉트 경로 — 이 문제의 진짜 승부처
생성 경로는 초당 11건이라 사실 거의 어떻게 짜도 문제가 안 됩니다. 리다이렉트 경로가 진짜 설계 대상입니다. 초당 1,150건의 조회가 전부 DB로 간다면, 인기 있는 몇 개의 링크(뉴스 기사, 바이럴 게시물)에 요청이 집중되면서 그 행에 대한 DB 부하가 튈 수 있습니다. 여기서 캐싱 챕터의 캐시-어사이드 패턴이 그대로 적용됩니다. 짧은 코드를 키로, 원본 URL을 값으로 Redis에 캐싱하고, TTL은 넉넉하게 잡습니다 — URL은 한번 만들어지면 거의 바뀌지 않으므로 무효화보다 적중률을 우선하는 게 맞습니다.
flowchart LR Client["클라이언트: GET /abc123"] --> LB["로드밸런서"] LB --> App["애플리케이션 서버"] App -->|캐시 히트| Cache["Redis"] App -->|캐시 미스 시| DB["DB (Primary + Read Replica)"] Cache -.->|미스 시 채움| DB
삭제와 만료 — 잊기 쉬운 요구사항
면접관이 굳이 짚어주지 않으면 놓치기 쉬운 부분이 만료 정책입니다. 링크에 유효기간이 있다면(예: 1년 후 자동 삭제), 이걸 어떻게 처리할지도 설계에 넣어야 합니다. 매 요청마다 만료 여부를 확인하는 건 비용이 적지만, 만료된 데이터가 DB에 계속 쌓여 테이블이 무한정 커지는 문제가 남습니다. 배치 작업으로 주기적으로 만료된 행을 정리하는 방식을 언급하면, 이 시스템을 "당장 동작하는 것"이 아니라 "1년 후에도 운영 가능한 것"으로 생각했다는 인상을 줍니다. 이런 디테일이 URL 단축기 같은 단순한 문제에서 답변의 깊이를 가르는 지점입니다.