Introduction
FAANG 기업의 시스템 디자인 인터뷰에서 흔히 나오는 이 질문, 여러분은 자신 있게 대답할 수 있으신가요? 단순해 보이는 이 질문 뒤에는 수많은 기술적 도전과 함정이 도사리고 있습니다. 초당 1000개의 요청을 처리하면서도 안정적으로 작동하는 시스템을 설계하는 것은 결코 쉬운 일이 아니죠.
오늘은 이 난제를 단 5분 만에 명쾌하게 해결하는 방법을 소개하려고 합니다. 고가용성, 낮은 지연 시간, 그리고 보안까지 - URL 단축기 하나로 시스템 디자인의 핵심 개념들을 모두 꿰뚫어 볼 수 있습니다.
이 글을 읽고 나면, 여러분도 FAANG 면접관 앞에서 자신 있게 화이트보드를 들고 설명할 수 있을 겁니다. "자, 이렇게 설계하면 됩니다. 우리의 URL 단축기는 초당 1000개의 요청도 거뜬히 처리할 수 있죠."
시스템 디자인 면접의 압박감에 숨이 막히셨나요? 이제 그 부담을 날려버릴 시간입니다. 가보실까요 :)
TinyURL이나 Bitly와 같은 확장 가능하고 안전한 URL 단축기 서비스를 처음부터 설계한다면?
TinyURL이나 Bitly와 같은 URL 단축기를 처음부터 설계하라는 시스템 디자인 면접 질문에 대한 답변을 정리해봤어요. 설계 요구사항, 아키텍처, 구성요소 설계부터 고성능을 위한 확장성과 보안 모범 사례까지 모든 것을 다루려고 합니다.
범위 정의: 기능적 요구사항과 비기능적 요구사항
먼저 이 시스템의 기능적 요구사항과 비기능적 요구사항을 정의해야 합니다.
기능적 요구사항은 두 가지입니다:
- 긴 URL이 주어졌을 때, 짧은 URL을 생성해야 합니다.
- 짧은 URL이 주어졌을 때, 사용자를 긴 URL로 리디렉션해야 합니다.
우리 서비스의 비기능적 요구사항은 낮은 지연 시간(빠른 응답)과 높은 가용성(항상 온라인 상태)을 우선시하는 것입니다.
문제를 명료하게 하기 위해 면접관에게 물어봐야 할 질문들
시스템의 규모에 대해 확실히 하기 위해 면접관에게 물어볼 수 있는 몇 가지 질문들이 있습니다. 대표적으로 아래와 같은데요:
- 사용량: 초당 생성해야 할 URL의 수를 추정해 주세요 (예를 들어 1000개라고 가정합시다).
- 문자: 숫자와 문자(영숫자)만 사용할 수 있나요, 아니면 다른 기호도 사용 가능한가요? (영숫자만 사용한다고 가정하겠습니다).
- 고유성: 여러 사용자가 같은 긴 URL을 제출하더라도 매번 고유한 짧은 URL을 생성해야 하나요? (이 설계에서는 그렇게 하겠습니다).
추정: 데이터 계산
이제 정보가 주어졌으니 이를 바탕으로 URL을 얼마나 짧게 만들어야 하는지 계산해야 합니다. 물론 가능한 한 짧게 만드는 게 좋겠죠. 하지만 매년 생성되는 URL의 수 역시 고려해야 합니다.
먼저 상당한 기간 동안 필요한 고유 URL의 수를 추정해 봅시다. 일반적인 접근 방식은 최소 몇 년간의 운영을 계획하는 것입니다. 간단히 10년으로 계산해 보겠습니다.
1년을 초로 환산하면: 60초/분 × 60분/시간 × 24시간/일 × 365일/년 = 31.536백만 초
10년을 초로 환산하면: 31.536백만 × 10 = 315.36백만 초
10년 동안의 초당 생성되는 총 URL 수: 1,000 × 315.36백만 = 3,153.6억 개의 고유 URL
이는 우리 데이터베이스가 초당 1000개의 쓰기를 처리해야 하며, 매년 1000 × 60 × 60 × 24 × 365 = 315억 개의 URL이 생성된다는 의미입니다. 일반적으로 쓰기보다 읽기가 10배 더 많다고 가정하면, 초당 10 × 1000 = 10,000개 이상의 읽기가 발생할 것입니다.
이제 10년 동안의 볼륨에 충분한 고유한 짧은 URL을 제공할 수 있는 문자 수를 파악해야 합니다. 문자 집합의 크기가 62개라고 할 때, URL 식별자의 길이는 다음과 같이 계산할 수 있습니다:
- 62¹ = 62개의 고유 URL (1문자)
- 62² = 3,844개의 고유 URL (2문자)
- ...등등.
이 계산을 계속하면 62⁷(약 3.5조)가 우리가 예상한 3,153.6억 개의 URL보다 큰 첫 번째 값임을 알 수 있습니다.
따라서 앞으로 10년 동안의 예상 성장을 지원하려면 우리의 단축된 URL은 최소 7자가 필요합니다.
고수준 아키텍처
우리 시스템은 다음과 같은 주요 구성요소를 가질 것입니다:
- 사용자: 긴 URL을 단축하거나 짧은 URL을 보내 긴 URL로 리디렉션해야 하는 사용자들이 있을 것입니다.
- 로드 밸런서: 모든 요청은 로드 밸런서를 통과하며, 이는 높은 가용성과 부하의 균형을 보장하기 위해 여러 웹 서버 인스턴스에 트래픽을 분산시킵니다.
- 웹 서버: 이 서버 복제본들은 들어오는 HTTP 요청을 처리하는 역할을 합니다.
- URL 단축 서비스: 짧은 URL 생성, URL 매핑 저장, 리디렉션을 위한 원본 URL 검색 등 핵심 로직을 포함하는 URL 단축 서비스도 필요합니다.
- 데이터베이스: 짧은 URL과 긴 URL 간의 연결을 저장합니다. 데이터베이스를 설계하기 전에 단축된 URL의 잠재적 저장 요구사항을 고려해야 합니다.
각 URL은 고유 식별자(약 7바이트), 긴 URL(최대 100바이트), 사용자 메타데이터(약 500바이트로 추정)를 포함할 것입니다. 이는 URL당 최대 1000바이트가 필요하다는 의미입니다. 10년 동안 예상 볼륨으로 볼 때, 이는 약 315테라바이트의 데이터에 해당합니다.
더 나아가기 전에, 먼저 단일 웹 서버의 API 설계에 대해 생각해 봅시다.
API 설계
서비스의 기본 API 작업을 정의해 봅시다. 기능적 요구사항에 명시된 대로 REST API를 사용할 것이며, 두 개의 엔드포인트가 필요합니다.
1. 짧은 URL 생성 (POST /urls)
입력: 긴 URL을 포함하는 JSON 페이로드 {"longUrl": "https://example.com/very-long-url"}
출력: 단축된 URL을 포함하는 JSON 페이로드 {"shortUrl": "https://tiny.url/3ad32p9"}와 201 Created 상태 코드.
요청이 잘못되었거나 형식이 잘못된 경우 400 Bad Request 응답을 반환하고, 요청된 URL이 이미 시스템에 존재하는 경우 409 Conflict로 응답합니다.
2. 긴 URL로 리디렉션 (GET /urls/{shortUrlId})
입력: shortUrlId 경로 매개변수 출력: 301 Moved Permanently 응답과 JSON 본문에 새로 생성된 짧은 URL { "shortUrl": "https://tiny.url/3ad32p9" }
301 상태 코드는 브라우저에 정보를 캐시하도록 지시하며, 이는 다음에 사용자가 짧은 URL을 입력할 때 브라우저가 서버에 도달하지 않고 자동으로 긴 URL로 리디렉션한다는 의미입니다.
그러나 각 요청의 분석을 추적하고 모든 요청이 시스템을 통과하도록 하려면 대신 302 상태 코드를 사용하세요.
데이터베이스: 단축된 URL 저장하기
다음 부분은 데이터베이스 계층입니다. 이 계층은 짧은 URL과 긴 URL 사이의 매핑을 저장합니다. 빠른 읽기 및 쓰기 작업에 최적화되어야 합니다.
스키마는 간단할 수 있습니다: 짧은 URL ID에 대한 기본 키와 긴 URL, 그리고 가능하다면 생성 메타데이터에 대한 필드가 있습니다.
여기서 우리는 주로 데이터베이스에 대한 읽기에 대해 생각해야 합니다. 일반적으로 초당 1000개의 쓰기가 있다면, 적어도 초당 10-100,000개의 읽기가 있다고 가정할 수 있습니다.
이 경우, 빠른 읽기와 쓰기를 지원하는 고성능 데이터베이스를 사용해야 합니다. 이는 NoSQL 데이터베이스(MongoDB와 같은 문서 저장소, Cassandra와 같은 와이드 칼럼 저장소, 또는 DynamoDB와 같은 키-값 저장소)를 사용해야 한다는 것을 의미합니다. 이들은 대규모 확장을 처리하도록 특별히 설계되었기 때문입니다.
ACID 준수는 되지 않지만, 여기서는 많은 JOIN이나 복잡한 쿼리를 수행하지 않을 것이고 ACID 규칙과 원자적 트랜잭션이 필요하지 않기 때문에 문제가 되지 않습니다.
URL 단축 서비스
이 시스템의 핵심 부분 중 하나는 URL 단축 서비스입니다. 이 서비스는 서로 다른 긴 URL이 같은 짧은 URL을 가리키는 충돌을 일으키지 않고 짧은 URL을 생성합니다.
이 서비스를 구현하는 데는 다양한 방법이 있습니다. 다음은 그 중 몇 가지입니다:
- 해싱: 긴 URL의 해시를 생성하고 그 일부를 식별자로 사용합니다. 그러나 해싱은 충돌을 일으킬 수 있습니다.
- Auto-increment ID: 데이터베이스 auto-increment ID를 사용하고 이를 짧은 문자열로 인코딩합니다. 이는 고유성을 보장하지만 예측 가능할 수 있습니다.
- 사용자 정의 알고리즘: 고유성과 예측 불가능성을 보장하기 위해 문자를 혼합하여 고유한 ID를 생성하는 사용자 정의 알고리즘을 설계합니다.
예를 들어, 충돌을 피하기 위한 매우 간단한 방법이 있습니다. 7자로 가능한 모든 키를 생성하고 이를 데이터베이스에 키로 저장할 수 있습니다. 여기서 키는 생성된 URL이고 값은 bool입니다. true이면 이 URL이 이미 사용 중이고, false이면 이 URL을 사용하여 새 매핑을 만들 수 있습니다.
따라서 사용자가 키 생성을 요청할 때마다 현재 사용 중이지 않은 URL을 이 데이터베이스에서 찾아 요청 본문의 긴 URL에 매핑할 수 있습니다.
이 경우 SQL 데이터베이스를 사용할까요, 아니면 NoSQL 데이터베이스를 사용할까요? 두 사용자가 긴 URL을 단축하도록 요청했고, 둘 다 이 데이터베이스에서 같은 키에 매핑되었다고 가정해 봅시다.
이 경우, URL은 두 요청 중 하나에만 매핑되고 다른 하나는 깨질 것입니다. 따라서 우리는 ACID 속성을 가진 SQL을 사용할 것입니다. 여기서 각 세션에 대해 트랜잭션을 생성하여 이러한 단계를 격리된 상태에서 수행할 수 있으며, 이 경우 이러한 문제가 발생하지 않을 것입니다.
고가용성 & 낮은 지연 시간
현재 우리 시스템은 당연히 초당 1000개의 URL 트래픽을 처리할 수 없을 것입니다.
캐싱
이를 더 확장 가능하게 만들기 위해, 먼저 인기 있는 URL을 빠르게 검색할 수 있도록 Redis와 같은 도구를 사용하여 인메모리 캐시에 저장하는 캐싱 계층이 필요합니다.
일부 URL이 다른 URL보다 훨씬 더 자주 액세스될 수 있으므로, 자주 액세스되는 항목을 우선시하는 제거 정책이 필요합니다. 이 시나리오에 적합한 두 가지 캐싱 제거 정책은 다음과 같습니다:
- LRU(Least Recently Used) 제거 정책: 가장 오랫동안 사용되지 않은 항목을 먼저 제거합니다. 이 정책은 URL 단축기에 효과적입니다. 가장 최근에 자주 액세스된 URL을 캐시에 유지하여 인기 있는 링크에 대한 액세스 시간을 크게 줄일 수 있기 때문입니다.
- TTL(Time To Live) 기반 제거 정책: 각 캐시 항목에 고정된 유효 기간을 할당합니다. 항목의 TTL이 만료되면 캐시에서 제거됩니다. 이 정책은 짧은 기간 동안만 인기 있는 URL의 캐싱을 처리하는 데 유용할 수 있습니다.
TTL은 또한 캐시 내용을 자동으로 새로 고치는 데 도움이 될 수 있으며, 더 효과적인 캐시 관리를 위해 LRU와 같은 다른 정책과 결합할 수 있습니다.
데이터베이스 확장: 복제와 샤딩의 결합
데이터베이스가 고가용성, 장애 허용성, 확장성을 지원하도록 복제 및 샤딩 전략을 구현해야 합니다.
우리의 7자 문자 세트가 3.5조 개의 고유 URL을 가지고 있다는 점을 고려하면, 키 기반 샤딩을 사용하여 URL 레코드를 여러 샤드에 균등하게 분산시킬 수 있습니다.
예를 들어 3개의 샤드에 분산시키면 각 샤드는 약 1.16조 개의 URL을 저장하게 됩니다. 이는 URL 수가 증가함에 따라 확장성을 보장합니다.
또한 각 샤드 내에서 마스터-슬레이브 복제를 구현하여 고가용성과 장애 허용성을 보장할 수 있습니다. 이 설정을 통해 노드 장애 발생 시 빠른 장애 조치와 복구가 가능합니다.
선택적으로, 서비스가 전 세계 사용자를 대상으로 하는 경우 지리적 샤딩과 복제를 고려하여 다른 지역에서의 지연 시간을 최소화하고 사용자 경험을 개선할 수 있습니다.
이러한 조합을 통해 서비스는 최소한의 다운타임과 빠른 응답 시간으로 대량의 URL 단축 및 리디렉션을 처리할 수 있습니다.
보안 고려사항
서비스를 위해 명심해야 할 몇 가지 보안 고려사항은 다음과 같습니다:
- 입력 유효성 검사: 사용자가 제출한 모든 URL을 정화(sanitize)해야 합니다. 유효한 프로토콜(HTTP, HTTPS 등)을 확인하고 URL이 잘 형성되었는지 확인해야 합니다. 이는 삽입 공격을 방지하는 데 도움이 됩니다.
- 속도 제한: 단일 소스가 할 수 있는 요청 수를 제한하여 DDoS 공격으로부터 서비스를 보호할 수 있습니다. 이를 위해 토큰 버킷 알고리즘을 고려해 볼 수 있습니다.
- 모니터링 및 로깅: 강력한 로깅 시스템(ELK 스택과 같은)이 필수적입니다. 이를 통해 병목 현상과 의심스러운 활동을 찾고 전반적인 시스템 건강을 확인하기 위해 로그를 분석할 수 있습니다.
- 난독화: 쉽게 예측할 수 있는 짧은 URL은 원하지 않습니다. 공격자가 유효한 링크를 추측하지 못하도록 생성 알고리즘에 무작위성을 추가합시다.
- 링크 만료: 선택적으로, 사용자가 단축된 URL에 만료 날짜를 설정할 수 있도록 하는 것을 고려할 수 있습니다. 이는 잠재적으로 악의적인 링크의 수명을 제한합니다.
여기서 논의한 각 구성 요소에 대해 더 자세히 알고 싶다면, 각 요소를 더 상세히 다루는 시스템 디자인 면접 개념 튜토리얼을 참고하실 수 있습니다.
Top 1% 개발자로 거듭나는 확실한 처방전, 데브필입니다.
댓글 3개
의견을 남겨주세요
로건
비공개 댓글 입니다. (메일러와 댓글을 남긴이만 볼 수 있어요)
데브필 DevPill (486)
@로건 피드백 감사합니다 :) 바로 수정완료했습니다. 번역 과정에서 여러 번 검수하는데 이번 건 놓쳤네요.. ^^ 항상 좋은 피드백 주셔서 감사합니다 로건님 :) 이외에도 궁금한 주제가 있다면 편하게 의견주세요!
로건
비공개 댓글 입니다. (메일러와 댓글을 남긴이만 볼 수 있어요)
의견을 남겨주세요