이 글에서 얻는 것
- URL Shortener의 핵심 요구(낮은 지연/고가용성/키 생성/중복·충돌)를 설계 요소로 분해할 수 있습니다.
- “읽기 경로(redirect)”와 “쓰기 경로(단축 생성)”를 각각 어떤 저장소/캐시 전략으로 풀어야 하는지 기준이 생깁니다.
- 운영에서 터지는 문제(핫 키, 스탬피드, 악성 트래픽, 통계 수집 비용)를 미리 고려한 설계를 만들 수 있습니다.
0) 문제 정의: 무엇을 만들 건가
URL Shortener는 크게 두 가지 기능이 핵심입니다.
- 긴 URL을 짧은 코드로 바꿔준다(생성)
- 짧은 코드를 원래 URL로 리다이렉트한다(조회/redirect)
여기에 옵션으로 보통 아래가 붙습니다.
- 커스텀 별칭(custom alias)
- 만료(expire)
- 클릭 통계/분석(analytics)
- 악성/스팸 방지(보안/레이트리밋)
1) API 설계(최소)
1-1) 단축 생성
POST /api/shorten- body:
{ longUrl, customAlias?, expireAt? } - response:
{ code, shortUrl, expireAt? }
- body:
1-2) 리다이렉트
GET /{code}→301또는302로Location: <longUrl>
실무 감각:
- 영구 리다이렉트면 301, 실험/단기면 302를 쓰는 경우가 많습니다(캐시/SEO 영향 포함).
2) 데이터 모델(최소)
핵심은 “code가 유니크”이고, 조회가 빠른 형태입니다.
url_mappingcode(PK 또는 unique)long_urlcreated_atexpire_at(nullable)owner_id(nullable, 커스텀/관리 기능이 있으면)
통계는 별 테이블/별 스토리지로 분리하는 편이 좋습니다(쓰기 폭발을 막기 위해).
3) Key(코드) 생성 전략: 설계의 중심
코드 생성은 크게 3가지 계열로 나뉩니다.
3-1) 증가하는 ID → Base62 인코딩(가장 단순/효율적)
- DB 시퀀스/auto increment 또는 Snowflake로 숫자 ID 생성
- 이를 Base62로 바꿔 짧은 문자열로 표현
장점:
- 충돌이 사실상 없음, 생성이 빠름
- 길이가 예측 가능(트래픽이 늘어도 점진적으로 늘어남)
단점/주의:
- 코드가 “추측 가능”해서 크롤링/스캔 위험이 커질 수 있음(보안/레이트리밋 필요)
- 멀티 리전/분산에서 ID 생성 전략을 잘 잡아야 함(Snowflake 등)
3-2) 랜덤/UUID/해시 기반(추측 어려움)
장점:
- 추측이 어렵고, 외부 노출에 상대적으로 안전
단점:
- 충돌 확률이 0이 아니므로 “충돌 처리(재시도/유니크 제약)”가 필요
- 코드 길이가 길어지기 쉬움
3-3) 커스텀 별칭
사용자가 원하는 코드(my-page)를 지정합니다.
- 반드시 unique 제약이 필요
- 금칙어/길이 제한/예약어(관리자 경로 등) 정책이 필요
4) 읽기 경로(redirect) 설계: 캐시가 승패를 가른다
리다이렉트는 보통 QPS가 매우 높습니다. 그래서 기본은:
- Redis/메모리 캐시 조회
- miss면 DB 조회
- 캐시에 적재(TTL 포함)
핵심 포인트:
- 캐시 키는
code그대로 또는url:v1:{code}형태로 안정적으로 - 만료가 있는 경우 TTL을
expire_at과 정합 맞추기 - 존재하지 않는 code에 대한 요청이 폭주할 수 있으니 “없는 것 캐싱(negative caching)”을 고려
- 핫 키 만료 순간 스탬피드가 날 수 있어 TTL 지터/싱글플라이트 같은 완화책을 고려
5) 쓰기 경로(단축 생성) 설계: 충돌/중복을 다룬다
쓰기 경로는 상대적으로 QPS가 낮지만, 정합성이 중요합니다.
- code 생성 → insert 시도 → unique 충돌이면 재시도
- 커스텀 별칭은 “먼저 조회”로만 막지 말고, DB unique 제약으로 최종 보장
- 같은 longUrl을 여러 번 줄였을 때 “항상 같은 코드”를 주는지(중복 제거) 정책을 정해야 합니다
- 중복 제거를 원하면 longUrl 해시(정규화 포함)를 별 컬럼로 두고 unique 처리하는 방식도 가능
- 대신 사용자별/만료별 정책이 복잡해집니다
6) 확장성/가용성: 어디가 병목이 되나
- 읽기 병목: 캐시 hit 비율, Redis/QPS, DB read replica
- 쓰기 병목: 코드 생성기(ID 생성), DB unique insert 경합
- 샤딩:
code(또는 해시된 code)로 샤딩하면 분산이 쉽습니다
가용성:
- 캐시 장애 시 DB로 우회(지연은 증가하지만 서비스 유지)
- DB 장애 시 “단축 생성은 제한”하고 “리다이렉트만 유지” 같은 모드 전환(Graceful Degradation)도 고려
7) 통계(Analytics): 동기 쓰기는 피한다
클릭 통계를 동기 DB 업데이트로 쌓으면 쓰기가 폭발합니다. 보통은 “이벤트로 남기고 비동기로 집계”합니다.
- redirect 시
click_event를 Kafka/Queue에 발행 - 배치/스트리밍으로 집계 테이블에 반영
8) 보안/운영에서 자주 터지는 것
- 악성 리다이렉트/피싱: 도메인 차단/검증, 신고/차단 프로세스
- 코드 스캐닝: 레이트 리밋, 봇 탐지, 추측 가능한 코드 전략이면 특히 중요
- Open redirect 남용: 내부 도메인 화이트리스트 정책이 필요한 경우도 있음(서비스 성격에 따라)
연습(추천)
- Base62 인코더를 만들고, “증가 ID → code”로 단축 생성/리다이렉트를 구현해보기
- Redis 캐시를 붙이고 hit/miss 로그로 실제 효과를 확인해보기(negative caching 포함)
- 클릭 이벤트를 큐로 발행하고 “시간당 클릭 수”를 비동기 집계하는 흐름을 만들어보기
💬 댓글