[가상 면접 사례로 배우는 대규모 시스템 설계 기초] 8장 URL 단축기 설계를 읽고 난 뒤에, 이를 직접 구현해보려고 한다. 다만 해당 장에서 제공하는 기능 요구사항을 조금은 변경하여 고려해볼 예정이다.
- 서버 설계 스토리 : https://jjunhub.tistory.com/11
- 서버 구현 스토리 : https://jjunhub.tistory.com/12
- Spring Boot 3.4.2
- Java 17
- Spring Data JPA
- H2 Database
- Lombok
- MySQL 8.0
- Redis
사용자가 입력한 원본 URL을 단축된 URL로 변경하여 제공하는 서비스이다. 이 서비스는 하루에 10억명의 방문자가 존재하며, 서로 다른 원본 URL을 10억개 이상 저장할 수 있어야한다. 사용자는 단축된 URL로 접속할 경우, 원본 URL로 redirect된다. 사용자가 단축된 URL로 접속할 때마다, 그 횟수를 날짜별로 저장한다. 사용자는 shortUrlId를 통해서 전체 URL 주소와 기타 정보를 확인할 수 있다. 사용자는 각 shortUrlId 별 일자 별 통계를 제공받을 수 있다.
- 사용자가 입력한 원본 URL을 단축된 URL로 변경하여 제공한다.
이 때 단축된 URL은 다음과 같은 구조를 띈다. “서버 호스트 주소 + /r + /{shortUrlId}”
예시 : www.google.com/test/abcdedasdasdsads -> www.jjunhub.com/s/1abd2ef - 사용자가 하나의 원본 URL로 여러 번 단축된 URL로 변경을 시도한다면, 항상 다른 shortUrlId 값이 제공되어야한다.
- shortUrlId는 alphanumberic한 문자열로 구성되어야하며 최대한 짧은 값을 가져야한다.
- 사용자는 단축된 URL로 접속할 경우, 원본 URL로 redirect 된다.
- 사용자가 단축된 URL로 접속할 경우, 그에 대한 횟수를 날짜별로 저장한다.
- 사용자는 shortUrlId를 통해서 전체 URL 주소와 기타 정보를 확인할 수 있다.
- 사용자는 각 shortUrlId 별 일자 별 통계를 제공받을 수 있다. 이는 shortUrlId와 날짜 정보를 통해서 요청을 받는다.
-
POST
/s- Feature
- 사용자는 해당 API를 통해 원본 URL을 입력하면, 단축된 URL을 제공받는다.
- 서버 측에서는 해당 API를 통해 shortUrlId를 생성하고, 원본 URL과 함께 DB에 저장해야한다.
- 서버 측에서는 shortUrlId를 생성할 때, alphanumberic한 문자열로 생성해야한다.
- 서버 측에서는 shortUrlId를 생성할 때, 중복되지 않는 값을 생성해야한다.
- 서버 측에서는 shortUrlId를 생성할 때, 가능한 짧은 값을 생성해야한다.
- Request
{ "originalUrl": "https://jjunhub.tistory.com/10" } - Response
{ "data" : { "shortUrlId": "ab1267c", "originalUrl": "https://jjunhub.tistory.com/10", "createdAt": "2021-06-07T11:38:16+0000" } }
- Feature
-
GET
/r/{shortUrlId}- Feature
- 사용자는 해당 API를 통해 원본 URL로 redirect된다.
- 서버 측에서는 해당 API로 접속하는 횟수를 통계 데이터로 저장해야한다.
- Request
- None
- Response
- originalUrl로 Redirect 302
- Feature
-
GET
/g/{shortUrlId}- Feature
- 사용자는 해당 API를 통해서 원본 URL과 기타 정보를 확인할 수 있다.
- Request
- None
- Response
{ "data" : { "shortUrlId": "ab1267c", "originalUrl": "https://jjunhub.tistory.com/10", "createdAt": "2021-06-07T11:38:16+0000" } }
- Feature
-
GET
/st/{shortUrlId}?date=2025-02-26- Feature
- 사용자는 해당 API를 통해 각 shortUrlId date에 해당하는 통계 정보를 획득할 수 있다.
- Request
- None
- Response
{ "data" : { "shortUrlId": "abc", "date": "2025-02-26", "clickCount": 100 } }
- Feature
- 중복되지 않는 shortUrlId를 생성할 때, 어떤 알고리즘을 사용할 것인가?
- 10억개 이상 저장될 수 있으며, 숫자와 문자로 이루어진 shortUrlId를 생성해야 한다.
- 숫자와 문자로 이루어진 shortUrlId는 0
9, az, A~Z의 문자로 구성되므로 총 10 + 26 + 26 = 62개의 문자로 구성된다. - 따라서 Base62 Encoding을 사용하여 shortUrlId를 생성하는 것이 적절하다고 판단하였다.
- Base62 Encoding은 한 자리마다 약 62개의 문자를 사용하므로, 8자리의 shortUrlId를 생성하면 62^8 = 218,340,105,584,896(21조)개의 문자를 생성할 수 있다.
- 21조 / 10억 = 21,834일 동안 서비스를 운영할 수 있고, 이는 약 60년에 해당하므로 충분한 시간이다.
- 따라서 10억개 이상을 저장해야하는 구조에 적합하도록 8자리의 shortUrlId를 생성하여 사용자에게 제공하였다.
- (Random 값 + Salt) + Hashing + Base62 Encoding
- Random 값과 Salt를 결합하여 Hashing과 Base62 인코딩을 진행하고 그 결과가 DB에 이미 존재하지 않는다면 shortUrlId로 저장하고, 존재한다면 다시 Random 값을 생성하여 Hashing을 진행한다.
- 장점: shortUrlId의 길이가 거의 일정하게 유지되고, 사용자가 추측하기 어렵다.
- 단점: 충돌 해결을 위해, hashing + salt를 여러 번 진행해야 할 수도 있다.
- DB의 Unique ID + Base62 Encoding
- DB에서 unique ID를 불러와서 Base62로 encoding하여 shortUrlId를 생성한다.
- 장점: 충돌이 발생할 확률이 매우 낮다.
- 단점: shortUrlId의 길이가 일정하지 않고, unique ID가 일정하게 증가한다면 사용자가 추측하기 쉬울 수 있다.
두 가지 방법 중, 1번 방법을 선택하여 구현하였다. DB에 unique ID를 불러오기 위해서는 여러 서버를 위해 unique ID를 관리하는 테이블이 추가로 필요하다. 따라서 1번 방법을 선택하여 구현하였다.
shortUrlId의 날짜별로 통계를 저장해야하는데 어떻게 저장할 것인가?
- 날짜별로 통계를 저장하기 위해서는 날짜별로 shortUrlId의 클릭 수를 저장해야 한다.
- 이를 위해서는 날짜별로 shortUrlId의 클릭 수를 저장할 수 있는 DB 스키마를 설계해야 한다.
- 날짜별로 shortUrlId의 클릭 수를 저장하는 DB 스키마는 다음과 같다.
- short_url_id: varchar(30)
- click_date: date
- click_count: bigint
- 이 과정에서 (shortUrlId, date)를 복합키 형태의 PK로 설정하여 데이터를 저장한다.
- 복합키로 인해서, 중복되지 않는 데이터를 저장할 수 있으며 클러스터드 인덱스를 사용하여 조회 성능을 향상시킬 수 있다.
shortUrlId의 클릭 수를 날짜별로 저장하고, 이를 통계로 집계해야하는데 어떻게 집계할 것인가?
- redis에 저장된 shortUrlId의 클릭 수를 약 1시간마다 집계하여, DB에 저장한다.
- 이를 위해서는 shortUrlId의 클릭 수를 집계하는 스케줄러를 도입했다.