Search

단축 URL 서비스를 어떻게 설계할 것인가?

생성일
2025/06/02
URL
안녕하세요, 장동호입니다!
이번 글에서는 최근 실무 면접에서 받았던 질문 중 하나인 “단축 URL 서비스를 설계한다면 어떻게 할 것인가?”에 대해 복기해보려 합니다. 단축 URL은 많이 사용하는 서비스 중 하나지만, 직접 설계한다고 하니 생각보다 고민할 부분이 많았고, 면접 당시 부족했던 부분들도 돌아보며 정리해 보았습니다.

단축 URL 서비스란?

단축 URL 서비스를 한 번 사용해본 경험이 있어 어떤 서비스인지에 대한 개념은 익숙했습니다. 하지만 막상 설계를 하려니 고려할 점이 굉장히 많았습니다.
URL을 단축하는 대표적인 서비스로는 bit.ly 등이 있고, 주로 긴 URL을 짧고 공유하기 쉽게 만들어주는 서비스입니다.
입력: https://example.com/very/long/path/to/some/resource 출력: https://short.ly/abc123
TypeScript
복사

요구사항 정리

먼저 단축 URL 서비스의 기본 요구사항은 다음과 같았습니다.
원본 URL을 입력받아 짧은 URL 생성

 API 설계

단축 URl 생성 서비스를 만들기 위해서 몇 개의 API가 필요할까요?
그리고, 각각의 API의 응답과 요청 메시지까지 적어주세요.
메서드
경로
설명
POST
/shorten
긴 URL을 단축 URL로 생성
GET
/{short_code}
단축 URL → 원본 URL로 리다이렉트
처음에는 면접관님들께 단축 URL 생성을 위한 API 한 개만 필요하다고 말씀드렸습니다.
하지만 곰곰이 생각해보니, 실제 서비스에서는 단축 URL을 생성하는 것뿐만 아니라, 생성된 단축 URL을 통해 원본 URL로 리디렉션하는 기능도 필요하다는 것을 깨달았습니다.
그래서 최소한 다음 두 가지 API가 필요하다고 답변드렸습니다.

1. 단축 URL 생성 API (POST /shorten)

요청 (Request)

Method: POST
URL: /shorten
Headers:
Content-Type: application/json
TypeScript
복사
Body:
{ "original_url": "https://example.com/long/path" }
TypeScript
복사

응답 (Response)

성공 (201 Created)
{ "short_url": "https://short.ly/my-custom", "original_url": "https://example.com/long/path", }
TypeScript
복사
실패 (400 Bad Request): URL 형식이 잘못됨

2. 단축 URL 리다이렉트 API (GET /{short_code})

요청 (Request)

Method: GET
URL 예시: /abc123

응답 (Response)

성공 (302 Found)
Headers:
Location: https://example.com/long/path
TypeScript
복사
실패 (404 Not Found): 존재하지 않는 코드
실패 (410 Gone): 만료된 단축 URL
리다이렉션을 많이 실무에서 많이 사용해보지 않아 단축 URL 리다이렉트 API 응답을 구성하는 데 큰 애를 먹었던 기억이 납니다.
면접 당시에는 301 상태 코드와 302 상태 코드가 혼동이 와서 301 상태 코드(영구 리다이렉션)를 사용해야 할지, 302 상태 코드(임시 리다이렉션)를 사용해야 할지 명확히 답변하지 못했던 점이 아쉬웠습니다.
단축 URL 서비스에서는 보통 302 상태 코드를 많이 사용합니다. 왜냐하면, 단축 URL이 언제든지 원본 URL이 바뀔 수 있고, 추후에 리디렉션 대상이 변경될 가능성이 있기 때문입니다.
이 경험 덕분에 HTTP 상태 코드별 의미와 리다이렉션 처리 방식을 정확히 공부하는 계기가 되었고, 앞으로는 API 설계 시 이러한 세세한 부분도 꼼꼼히 챙겨야겠다는 생각을 했습니다.

 단축 코드 생성 전략

단축 코드는 어떤 방식으로 생성할건가요?
단축 URL의 핵심은 긴 URL을 고유한 “짧은 문자열”로 변환하는 것이며, 이 코드를 어떻게 만들지에 따라 서비스의 성능이 달라집니다.

1. 랜덤 문자열 생성 (Random Code)

방식

숫자+영문(예: Base62) 조합으로 고정 길이 랜덤 문자열 생성
예: abc123, Xy7Z9

장점

충돌 가능성이 낮고 간단함
사용자 식별이 어려워 보안 측면에 좋음

단점

중복 방지를 위해 매번 DB 체크 필요 (성능 부담)
사용자가 기억하기 어려움

2. Base62 인코딩 (자동 증가 ID 기반)

방식

DB의 auto-increment된 정수 ID를 Base62로 인코딩해 short code로 변환
예:
ID: 125
Base62: cb → https://short.ly/cb

장점

충돌 검사 필요 없음
짧은 길이 보장

단점

추측이 쉬움 → 보안에 민감한 경우 부적합

3. 해시 기반 (Hashing)

방식

원본 URL을 해싱하여 고정 길이 코드 생성 (예: SHA256, MD5 등)
예: md5("https://example.com/...")[:6]

장점

같은 URL → 같은 코드
코드 길이 고정

단점

짧게 자를 경우 충돌 가능성 있음
같은 URL이면 무조건 동일 코드 → 추적당할 위험

4. UUID 기반

UUID (고유 식별자)에서 앞 6~8자리 잘라 사용
: uuid4().hex[:8] → d3f91a7b

장점

중복 거의 없음
글로불 분산 환경에서 유용

단점

코드가 비교적 길고 무작위
인지성 낮음 (의미 없는 문자열)
면접 당시 저는 랜덤 문자열을 생성하고, DB에서 충돌이 있는지 검사하는 방식을 사용한다고 말씀드렸습니다. 그 외에도 여러 방식들이 있었지만, 면접 당시 다양한 접근법을 충분히 설명하지 못한 점이 아쉬웠고, 앞으로는 어떤 문제를 해결할 때 다양한 방법들을 충분히 고려하고 비교해보는 연습이 필요하다고 느꼈습니다.

DB 스키마 설계

컬럼명
타입
설명
인덱스 사용 이유
id
BIGINT (PK)
내부용 ID (Auto-Increment)
short_code
VARCHAR(10), UNIQUE INDEX
단축 코드 (ex: aZ8k1X)
랜덤 코드 기반 조회 시 빠른 검색 및 중복 방지
original_url
TEXT
원본 URL
expires_at
DATETIME, INDEX
만료일
만료 처리 스케줄링 쿼리에 사용
created_at
DATETIME
생성 시각
updated_at
DATETIME
수정 시각
원본 URL, 단축 코드, 생성일 등을 영구적으로 저장하기 위해 RDBMS 사용
단축 URL → 원본 URL 빠른 리디렉션 조회(성능 최적화)를 위해 Redis 도입 가능
캐시 미스 시 RDBMS 조회 후 Redis에 다시 저장
면접 당시 id, short_code, original_url 세 가지 필드만 말씀드리고, 면접관님께서 힌트를 주셔서 뒤늦게 expires_at 필드가 필요하다는 것을 알게 됐습니다.
단축 URL 서비스에서는 단순히 단축 코드와 원본 URL만 저장하는 것 외에도, 특정 기간 동안만 유효한 URL을 지원하거나, 오래된 URL을 자동으로 만료 처리하는 기능이 자주 요구되기 때문입니다.
expires_at 필드를 두면, 만료된 URL에 대한 요청을 처리할 때 적절한 안내를 하거나 삭제 정책을 구현하는 데 도움이 됩니다.
또한, RDBMS와 Redis 중 어떤 저장소를 선택할지에 대한 질문도 받았습니다. 면접 당시 RDBMS를 사용하는 것에 대한 명확한 확신이 없어 즉답을 피했던 기억이 납니다.
RDBMS는 데이터의 영속성과 무결성을 보장하기 때문에, 단축 URL 원본 데이터 저장에 적합합니다. 반면, Redis는 인메모리 기반으로 빠른 조회 속도를 제공하며 TTL 기능을 활용해 만료 처리를 쉽게 할 수 있어, 조회 성능 향상을 위한 캐시 용도로 많이 활용됩니다.
따라서 단축 URL 서비스에서는 핵심 데이터를 RDBMS에 저장하면서, Redis를 단축 코드 조회 캐시로 병행 운영하는 하이브리드 구조가 이상적일 수 있습니다.
면접에서는 이러한 선택 기준과 각 저장소의 장단점, 그리고 실제 서비스 상황에서 어떻게 적용할 수 있는지 명확히 설명하는 것이 좋았을 것 같습니다.

만료 URL 처리 전략

일정 주기로 만료된 URL을 비활성화하거나 삭제하는 백그라운드 작업을 운영합니다.
예를 들어, 하루 1번 cron job으로 expires_at < NOW() 인 데이터를 삭제 처리합니다.
이 과정에서 expires_at 필드에 인덱스를 설정해두면, 만료된 데이터를 빠르게 탐색할 수 있어 배치 작업의 성능을 크게 향상시킬 수 있습니다.

사용자 요청 흐름

1. 단축 URL 생성 요청 (POST)

1.
사용자가 original_url을 서버로 전달
2.
Auto-Increment + Base62 방식으로 단축 코드 생성
3.
RDBMS에 URL 데이터 저장
4.
Redis에도 캐싱 (캐시 미스 방지)
5.
클라이언트에 short_url 응답

2. 단축 URL 접근 요청 (GET)

1.
사용자가 브라우저에서 https://sho.rt/abc123 접속
2.
서버는 Redis에서 short_codeoriginal_url 조회
3.
캐시 hit 시 바로 리다이렉션
4.
캐시 miss 시 RDBMS에서 조회 후 결과를 Redis에 캐싱한 다음 리디렉션 수행

정리

짧은 면접이었지만, 실무에서 마주할 수 있는 다양한 문제 상황과 그에 대한 대처 방안을 고민해볼 수 있는 좋은 기회였습니다. 단순히 기능 구현에 그치지 않고, 왜 이 방식이 적절한가서비스가 커졌을 때 어떤 문제가 발생할 수 있는가그에 대한 대안은 무엇인가와 같은 질문들에 스스로 답해보는 과정이 중요하다는 걸 느꼈습니다. 기능 하나를 설계할 때에도 다양한 관점을 갖고 접근하는 사고방식을 몸에 익히기 위해 계속해서 고민하고 연습해야겠습니다.

참고 자료