안녕하세요, 장동호입니다!
이번 글에서는 최근 실무 면접에서 받았던 질문 중 하나인 “단축 URL 서비스를 설계한다면 어떻게 할 것인가?”에 대해 복기해보려 합니다. 단축 URL은 많이 사용하는 서비스 중 하나지만, 직접 설계한다고 하니 생각보다 고민할 부분이 많았고, 면접 당시 부족했던 부분들도 돌아보며 정리해 보았습니다.
단축 URL 서비스란?
단축 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_code → original_url 조회
3.
캐시 hit 시 바로 리다이렉션
4.
캐시 miss 시 RDBMS에서 조회 후 결과를 Redis에 캐싱한 다음 리디렉션 수행
정리
짧은 면접이었지만, 실무에서 마주할 수 있는 다양한 문제 상황과 그에 대한 대처 방안을 고민해볼 수 있는 좋은 기회였습니다. 단순히 기능 구현에 그치지 않고, 왜 이 방식이 적절한가, 서비스가 커졌을 때 어떤 문제가 발생할 수 있는가, 그에 대한 대안은 무엇인가와 같은 질문들에 스스로 답해보는 과정이 중요하다는 걸 느꼈습니다. 기능 하나를 설계할 때에도 다양한 관점을 갖고 접근하는 사고방식을 몸에 익히기 위해 계속해서 고민하고 연습해야겠습니다.