URL Shortener (TinyURL)

System Design

Understand it step by step (한국어로 이해 → 영어로 말하기)

긴 URL을 짧은 코드(예: bit.ly/aZ8kQp)로 바꿔주고, 그 짧은 링크를 누르면 원래 URL로 보내주는 서비스다. 핵심은 두 가지: 짧은 코드를 안 겹치게 빨리 만드는 것, 그리고 리다이렉트(읽기)를 아주 빠르게 처리하는 것. 읽기가 쓰기보다 압도적으로 많은 전형적인 읽기 위주 시스템이다.
  1. 1

    기능 요구사항은 딱 2개다. (1) 긴 URL을 받아 짧은 코드를 돌려준다. (2) 짧은 코드로 오면 원래 URL로 301/302 리다이렉트한다. 선택으로 커스텀 별칭(alias)과 만료(TTL), 클릭 수 집계.

    🔧 도구:301 (permanent)302 (temporary)

    🗣 영어로 말해

    Two core features: shorten a long URL into a code, and redirect a code back to the original.

    checking microphone…

  2. 2

    비기능 요구사항: 리다이렉트는 매우 빨라야 한다(p99 ~50ms 미만). 읽기가 쓰기보다 훨씬 많다(약 100:1). 일관성보다 가용성을 택한다 — 링크는 항상 열려야 하고, 새로 만든 링크가 1~2초 늦게 퍼져도 괜찮다. 한 번 만든 코드는 절대 다른 URL로 바뀌면 안 된다(불변).

    ⚖️ Trade-off: 강한 일관성을 포기하고 가용성을 택한다. 새 링크가 잠깐 안 보이는 건 참지만, 리다이렉트가 죽는 건 안 된다.

    🔧 도구:availability over consistencyimmutable mapping

    🗣 영어로 말해

    Reads dominate writes about 100 to 1, so I optimize for fast, highly available redirects.

    checking microphone…

  3. 3

    스케일 추정: 하루 1억 개 새 링크 생성이면 쓰기 약 1,000 QPS. 읽기는 100배라 약 100,000 QPS(피크는 2~3배). 링크 하나당 약 500바이트, 5년이면 1억x365x5 ≈ 1,800억 행, 약 90TB. 한 대 DB로는 절대 안 되니 샤딩이 전제다.

    🔧 도구:1B-scale rows~90TBsharding required

    🗣 영어로 말해

    Roughly a thousand writes and a hundred thousand reads per second, so a single DB won't fit.

    checking microphone…

  4. 4

    API는 단순하다. 생성은 POST /urls 에 longUrl을 보내면 shortCode를 받는다. 리다이렉트는 GET /{code}. 데이터 모델은 사실상 키-값 한 장: code가 파티션 키, 그 안에 longUrl, createdAt, expireAt, 만든 사람. code로만 조회하므로 RDB의 인덱스보다 키-값 분산 저장소가 자연스럽다.

    🔧 도구:DynamoDBCassandrakey-value store

    POST /urls   { "longUrl": "...", "alias?": "...", "ttl?": 86400 }
      -> 201 { "shortCode": "aZ8kQp" }
    GET  /{code} -> 302 Location: <longUrl>
    
    // table (DynamoDB / Cassandra)
    //   PK: code (string, 7 chars)   <- only access pattern
    //   longUrl, createdAt, expireAt(TTL), ownerId
    🗣 영어로 말해

    It's basically one key-value table: short code is the partition key, value is the long URL.

    checking microphone…

  5. 5

    상위 설계: 클라이언트 → CDN/로드밸런서 → 짧은 서비스(stateless, 여러 대). 읽기 경로는 서비스가 먼저 Redis 캐시를 보고(핫 링크 90%+가 여기서 끝남), 없으면 키-값 DB에서 읽고 캐시에 채운 뒤 302를 돌려준다. 쓰기 경로는 코드를 발급해 DB에 한 줄 쓴다. 클릭 집계는 응답 경로를 막지 않게 Kafka로 비동기로 흘려 워커가 따로 센다.

    🔧 도구:CDNRedisKafkaasync workersstateless service

    🗣 영어로 말해

    Read path hits Redis first, falls back to the DB, and clicks go async through Kafka.

    checking microphone…

  6. 6

    딥다이브 1 — 코드를 어떻게 안 겹치게 만드나? 핵심 어려운 부분이다. 후보 3개를 비교한다. ① 긴 URL을 해시(MD5)해서 앞 7자 → 충돌이 나서 충돌 처리가 필요. ② DB auto-increment ID를 base62로 인코딩 → 충돌은 없지만 ID가 순차라 다음 코드를 예측당하고, 단일 카운터가 병목. ③ 중앙 ID 발급기가 1,000개씩 ID 구간(range)을 각 서버에 미리 떼어주고, 서버는 그 안에서 로컬로 base62 인코딩 → 충돌 없음, DB 왕복 거의 없음, 서버 추가도 쉬움. 그래서 ③을 택한다.

    ⚖️ Trade-off: 랜덤이 아니라 구간 기반이라 코드 순서에 약한 패턴이 생긴다. 추측 방지가 중요하면 구간 안에서 순서를 섞거나 랜덤 7자 + 충돌 재시도로 바꾼다.

    ✅ Fix: 62진법(a-z A-Z 0-9) 7자리면 62^7 ≈ 3.5조 개라 5년치 1,800억 개를 충분히 덮는다. 발급기가 구간을 미리 떼어주니 코드 생성에 DB 락도, 매번 왕복도 필요 없다.

    🔧 도구:base62Snowflake / ID allocatorranged ID leasingZookeeper/DB sequence

    // base62: 7 chars = 62^7 ~= 3.5 trillion codes
    String encode(long id) { /* id -> [a-zA-Z0-9]{7} */ }
    
    // each server leases a range, no per-write DB round-trip
    range = idAllocator.lease(1000);  // e.g. [4_000_000, 4_001_000)
    code  = encode(range.next());     // local, no lock
    🗣 영어로 말해

    I hand each server a pre-allocated range of IDs, then base62-encode locally, so codes never collide.

    checking microphone…

  7. 7

    딥다이브 2 — 핫 키와 읽기 폭발. 한 링크가 갑자기 바이럴되면(예: 광고 링크) 그 code 하나에 초당 수만 요청이 한 캐시 노드로 몰린다. 매핑은 불변이라 캐시 TTL을 길게(시간 단위) 잡아도 안전하고, CDN 엣지에서 302를 캐시해 원본 서버까지 안 오게 한다. 캐시가 비는 순간 동시에 DB로 몰리는 캐시 스탬피드는 single-flight(키별로 한 요청만 DB를 채우고 나머지는 그 결과를 기다림)로 막는다.

    ⚖️ Trade-off: 엣지 캐시 TTL이 길면 삭제/만료된 링크가 잠깐 더 살아 있을 수 있다. 그건 짧은 TTL의 무효화 신호나 만료 체크로 받아들인다.

    ✅ Fix: 불변이라는 성질이 무기다. 캐시 무효화를 신경 쓸 필요가 없어 TTL을 길게 잡고, 핫 키는 엣지에서 끝내고, 콜드 스타트 순간만 single-flight로 한 번만 DB를 읽는다.

    🔧 도구:CDN edge cachesingle-flightlong TTL (immutable)consistent hashing

    🗣 영어로 말해

    Mappings are immutable, so I cache aggressively at the CDN edge and use single-flight on misses.

    checking microphone…

  8. 8

    트레이드오프 + 모니터링: 가용성을 위해 강한 일관성을 포기했다 — 새 링크가 모든 리전에 퍼지기까지 1~2초 지연이 있다. 또 클릭 집계는 비동기라 실시간이 아니라 약간 늦다. 모니터링은 리다이렉트 p99 지연, 캐시 적중률(목표 90%+), DB 샤드별 QPS와 핫 샤드 쏠림, ID 구간 소진 속도, 5xx 비율, Kafka 컨슈머 랙을 본다.

    ⚖️ Trade-off: 강한 일관성과 실시간 정확한 클릭 수를 포기하고, 항상 빠르게 열리는 링크를 얻었다. 읽기 위주 서비스에선 맞는 교환이다.

    🔧 도구:cache hit ratep99 latencyshard QPS skewKafka consumer lag

    🗣 영어로 말해

    I gave up strong consistency for availability, and I watch redirect p99, cache hit rate, and hot shards.

    checking microphone…

8단계 영어를 다 말하면 → 이 메커니즘 전체를 영어로 설명할 수 있게 된다.

🎙 이 카드 AI랑 음성 대화 · 기록 저장