URL 단축기 서버 설계하기 - 1편
부제 : [가상 면접 사례로 배우는 대규모 시스템 설계 기초] 8장 실습
개요
[가상 면접 사례로 배우는 대규모 시스템 설계 기초]의 8장 URL 단축기 설계를 읽고 난 뒤에, 이를 직접 구현해보려고 한다. 다만 해당 장에서 제공하는 기능 요구사항을 조금은 변경하여 고려해보았다.
서비스 설명
요약
사용자가 입력한 원본 URL을 단축된 URL로 변경하여 제공하는 서비스이다. 이 서비스는 하루에 10억명의 방문자가 존재하며, 서로 다른 원본 URL을 10억개 이상 저장할 수 있어야한다. 사용자는 단축된 URL로 접속할 경우, 원본 URL로 redirect된다. 사용자는 단축된 URL로 접속할 경우, 그에 대한 횟수를 날짜별로 저장한다. 사용자는 shortUrlId를 통해서 전체 URL 주소와 기타 정보를 확인할 수 있다. 사용자는 각 shortUrlId 별 일자 별 통계를 제공받을 수 있다.
기능 요구 사항
- 사용자가 입력한 원본 URL을 단축된 URL로 변경하여 제공한다.
이 때 단축된 URL은 다음과 같은 구조를 띈다. “서버 호스트 주소 + /r + /{shortUrlId}”
예시 : http://www.google.com/test/abcdedasdasdsads -> http://www.jjunhub.com/s/1abd2ef - 사용자가 하나의 원본 URL로 여러 번 단축된 URL로 변경을 시도한다면, 항상 다른 shortUrlId 값이 제공되어야한다.
- shortUrlId는 영어문자와 숫자로 이루어진한 문자열로 구성되어야하며 최대한 짧은 값을 가져야한다.
- 사용자는 단축된 URL로 접속할 경우, 원본 URL로 **redirect** 된다.
- 사용자가 단축된 URL로 접속할 경우, 그에 대한 횟수를 날짜별로 저장한다.
- 사용자는 shortUrlId를 통해서 전체 URL 주소와 기타 정보를 확인할 수 있다.
- 사용자는 각 shortUrlId 별 일자 별 통계를 제공받을 수 있다. 이는 shortUrlId와 날짜 정보를 통해서 요청을 받는다.
고려해야할 사항
- 이 서비스는 하루에 10억명의 방문자가 존재한다.
- 이 서비스는 서로 다른 원본 URL을 10억개 이상 저장할 수 있어야한다.
- 일자 별 통계를 요청할 떄, 최대한 빠르게 확인할 수 있어야한다.
- 일자 별 통계는 전날까지의 통계만 확인하여도 괜찮다.
- 하나의 원본 URL을 여러 번 반복하여 단축한다면, 그 때마다 다른 shortUrlId를 제공해야한다.
구현 전 고려 사항
1. 트래픽 계산하기
하루에 10억번이면, 1시간에 4100만이고, 1분에 69만이며, 1초에 1만 정도이다.
즉, 1초당 만 건의 요청을 무리없이 처리할 수 있도록 서비스를 구성해야한다.
2. 도메인 로직 설계
이 부분은 2번째로 고려되었지만, 흐름 상 다음 블로그 글에 작성하였다.
3. 기술 스택 선정
이 서비스는 대규모 트래픽을 받아들일 수 있어야하며, 서로 다른 원본 URL을 10억개 이상 저장할 수 있어야한다.따라서 다음과 같은 기술 스택을 고려해볼 수 있다.
- Spring Boot
- Spring Boot는 빠른 개발을 위한 프레임워크이며, 다양한 라이브러리와 통합이 가능하다.
- Spring Boot는 대규모 트래픽을 받아들일 수 있는 서비스를 구현할 수 있다.
- 가장 자주 사용한 프레임워크이므로, 추가로 공부할 필요가 없다. ( 메인 이유 )
- MySQL
- MySQL은 대규모 트래픽을 받아들일 수 있는 DB이며, 다양한 기능을 제공한다.
- MySQL은 서로 다른 원본 URL을 10억개 이상 저장할 수 있다.
- Redis
- Redis는 대규모 트래픽을 받아들일 수 있는 캐시 서버이며, 빠른 속도로 데이터를 조회할 수 있다.
- Redis는 shortUrlId의 원본 URL의 매핑 정보를 캐시로 저장할 수 있다.
- Redis는 shortUrlId의 클릭 수를 날짜별로 저장할 수 있다.
4. DB 구조
하나의 원본 URL에 대해서 요청이 들어올 때마다, 중복 없이 매번 새로운 shortUrlId를 생성해야한다.
이 과정에서 굳이 pk를 auto generated하게 설정할 필요가 없을 것 같아서 shortUrlId 자체를 pk로 설정하였다.
또한 shortUrlId별로 날짜별 클릭 수를 제공해야하는데, 이 과정에서 (short_url_id, date)를 복합키로 설정하여 데이터를 저장하였다.
이를 통해 중복되지 않는 데이터를 저장할 수 있으며, 클러스터드 인덱스를 사용하여 조회 성능을 향상시킬 수 있다고 판단하였다.
5. 서버 코드 단 아키텍처 설계
서버 코드는 포트앤어댑터 패턴을 통해서 각 계층을 효율적으로 분리하기로 하였다. 이유는 다음과 같다.
- 간편한 의존성 주입을 통해서 테스트 코드 작성에 용이하도록
- 다양한 외부 서비스를 도입하기 편하도록
- 서비스가 간단해서
6. 캐시 관련 구현 방법
캐시를 통해서 shortUrlId -> original URL로의 매핑을 제공해야하면서 shortUrlId마다 클릭 수를 일 단위로 기록해야하는데, 이 과정에서 캐시를 어떤 구조로 사용할 지 고민하였다.
글로벌 캐시 Redis, Memcached
- Redis나 Memcached를 사용하여 shortUrlId -> original URL로의 매핑과 shortUrlId마다 클릭 횟수를 일 단위로 기록한다.
- 장점: 빠른 속도로 데이터를 조회할 수 있다.
- 단점: 캐시 저장소가 다운되면 데이터가 손실될 수 있다. 트래픽이 몰릴 경우, 성능 이슈가 발생할 수 있다.
인메모리 캐시 Caffeine
- 서버의 메모리에 shortUrlId -> original URL로의 매핑과 shortUrlId마다 클릭 횟수를 일 단위로 기록한다.
- 장점: Redis보다 빠르게 데이터를 조회할 수 있다.
- 단점: 서버가 다운되면 데이터가 손실될 수 있다. 각각의 서버에서 Cache Miss로 인한 성능 이슈가 발생할 수 있다.
두 가지 방법 중, 처음에는 인메모리 캐시를 고려하였다. 그 이유로는 간단히 각자 노드에서 shortUrlId과 originalUrl의 매치를 캐시로
구현하고, 각 노드에서 저장된 조회수는 배치를 통해서 데이터를 DB에 적제하면 된다고 생각했기 때문이다. 그러나 이런 방법에는 큰 문제가 있었다.
만약 약 5억 개의 다양한 shortUrlId가 존재할 때, 각 노드에서 캐시 데이터를 따로 관리한다고 가정하면, shortUrlId가 없을 때마다 DB에 요청을 보내서 데이터를 가져와야한다.
이 구조에서는 자칫하면 만 단위의 트래픽이 동시에 DB에 도달할 수 있다. 따라서 이 Cache Miss를 줄이기 위해 글로벌 캐시의 구조로 변설계를 변경했다.
캐시를 이렇게 공통으로 사용하게 되면 Cache Miss가 최대 1/(노드의 개수)까지 줄어드는 효과를 얻을 수 있다.
7. 통계 관련 정보 업데이트
임무는 통계 정보를 일자 별로 저장해야하는 것이다. 서버 개발자로써 사용자가 들어올 때마다 이 정보를 DB에 +1씩 요청을 한다면.. 다시 취준을 시작해야할 것이다.
가능한 DB에는 적게 부하를 주면서, 통계 정보는 정확하게 기록해야한다. 이를 위해서 Redis의 캐시 기능을 통해 shortUrlId에 대한 요청이 올 때마다, 그 값을 캐시에서 찾아오면서 레디스에 클릭 수를 1 증가시킨다.
그리고 주기적으로 동작하는 스프링 스케쥴링을 통해서 이 클릭 수를 종합하여, MySQL 데이터베이스에 적재한다.
여기서 중요한 것은 스케쥴링의 주기라고 생각한다.
만약 요구 사항이 실시간으로 각 URL에 대한 통계 정보를 제공해야한다면, 스케쥴링 주기를 짧게 해서 최대한 정보의 정확도를 높여야할 것이다. 하지만 스케쥴링 주기가 짧으면 짧을 수록, 서버에 대한 부하를 증가시킬 수 있다. 따라서 요구 사항이 명확하지 않다면, 트레이드 오프를 통해서 정확도와 서버 부하를 적절히 선택해야한다.
우리는 하루 전까지의 통계만 제공하면 되니까, 우리는 하루 단위로 스케쥴링을 돌리면 된다.
8. 전체 인프라 구성
인프라 구성에 있어서, 사실 프론트 쪽은 설계한 근거가 부족하다. 일단 프론트 쪽에서는 충분한 트래픽을 수용할 수 있을 것이라고 가정하고, 서버 쪽에 집중하였다. 서버 쪽에서는 다음과 같은 구조로 구성하였다.
이렇게 앞 단에 LoadBalancer를 두고, 서버 노드를 적절하게 N개 늘려가며 트래픽을 분산받아 처리할 수 있도록 해야한다.
또한 뒤에 Redis도 필요에 따라 단일 노드와 클러스터 중에 선택하는 것이 바람직할 것이다.
9. 부하 테스트 방법
내가 생각한 방법이 정말 하루에 10억 번의 요청을 견딜 수 있는지 검증하려면, 직접 그만한 트래픽을 날려보는 수밖에 없다. 트래픽을 날리는 방법으로 2가지 정도를 고려해보고 시도해보았다.
- JMeter
-> 실제로 사용해보니, 내 호스트 컴퓨터 환경의 영향으로 인해 요청이 잘 전송되지 않는 것 같은 느낌이 들었다. - Apache Bench
-> JMeter에 비해서, 가시적으로 결과를 얻을 수 있었다.
Apache Bench 당첨!
구현
https://github.com/jjunhub/short-url-generator
GitHub - jjunhub/short-url-generator: [가상 면접 사례로 배우는 대규모 시스템 설계 기초] 8장 URL 단축기
[가상 면접 사례로 배우는 대규모 시스템 설계 기초] 8장 URL 단축기 설계를 읽고.. Contribute to jjunhub/short-url-generator development by creating an account on GitHub.
github.com
마치며..
2편은 구현 과정 중에 있엇던 트러블 슈팅과 성능 측정 결과를 작성할 예정이다!