티스토리 뷰
여러분은 매일 수많은 웹사이트 링크를 접하고 공유합니다. 이메일, 메신저, SNS를 통해 전달되는 링크 중에는 길고 복잡한 것들이 많죠. 이때마다 '이 긴 주소를 어떻게 줄일 수 없을까?' 혹은 '다른 사람들은 어떻게 깔끔한 짧은 링크를 만들까?'라는 생각을 해보셨을 겁니다. 바로 이때 등장하는 것이 단축 URL이며, Bitly, TinyURL과 같은 서비스들은 이미 우리에게 익숙합니다.
하지만 단순히 링크를 줄이는 것을 넘어, 나만의 단축 URL 서비스 만들기를 직접 시도해본다면 어떨까요? 이 글은 단순한 링크 단축 기능을 넘어, 단축 URL 서버 구축의 전 과정을 깊이 있게 탐구하며, 여러분이 직접 자신만의 링크 단축 서버 개발에 도전할 수 있도록 돕는 실용적인 가이드가 될 것입니다.
이 가이드는 기본적인 웹 동작 원리를 이해하는 일반인부터 백엔드 개발에 관심 있는 초급 개발자, 그리고 더 나아가 실제 서비스를 고민하는 분들까지, 각자의 수준에 맞춰 단축 URL 서비스의 세계를 탐험할 수 있도록 구성했습니다. 단축 URL의 핵심 원리부터 데이터베이스 설계, 코드 구현, 그리고 실제 배포 및 운영까지, 전반적인 과정을 이해하고 직접 구축해보는 즐거움을 경험하시길 바랍니다.

1. 단축 URL의 모든 것: 개념, 필요성, 그리고 작동 원리
일상생활에서 긴 웹 주소(URL)는 때때로 불편함을 초래합니다. 메신저로 친구에게 영화 예고편 링크를 보낼 때, 혹은 소셜 미디어에 중요한 기사 링크를 공유할 때, 긴 URL은 메시지의 가독성을 떨어뜨리고 때로는 스팸처럼 보이기도 합니다. 여기서 단축 URL은 마치 긴 도로를 지름길로 바꿔주는 내비게이션처럼, 길고 복잡한 URL을 짧고 간결하게 바꿔주는 역할을 합니다. 단순히 주소를 짧게 만드는 것을 넘어, 이 기술은 사용자 경험을 향상시키고, 공유를 간편하게 만들며, 심지어 마케팅 효과를 측정하는 데도 핵심적인 역할을 합니다.
1.1. 단축 URL의 개념과 필요성: 왜 우리는 짧은 링크를 원하는가?
단축 URL(Shortened URL)은 원본의 길고 복잡한 웹 주소를 훨씬 짧고 기억하기 쉬운 형태로 변환한 것입니다. 예를 들어, https://www.example.com/products/category/electronics/item/super-duper-widget-premium-edition-for-pros 와 같은 주소를 https://yours.is/abcde 와 같이 바꾸는 것이죠.
이러한 단축 URL은 여러 면에서 유용하며, 나만의 단축 URL 서비스 만들기를 통해 이러한 장점들을 직접 제어할 수 있습니다:
- 가독성 및 미관: 길고 의미 없는 문자열의 나열 대신, 짧고 깔끔한 링크는 메시지를 더욱 전문적이고 신뢰성 있게 만듭니다.
- 공유 용이성: 소셜 미디어(X/트위터 등)처럼 글자 수 제한이 있는 플랫폼에서 긴 URL은 큰 제약이 됩니다. 단축 URL은 이러한 제한을 해결하여 더 많은 내용을 담을 수 있게 합니다. 또한, 수기로 입력하거나 구두로 전달할 때도 훨씬 편리합니다.
- 클릭 추적 및 분석: 대부분의 단축 URL 서비스는 사용자들이 링크를 클릭한 횟수, 시간, 지역 등의 데이터를 수집하여 통계 정보를 제공합니다. 이는 마케팅 캠페인의 성과를 측정하거나 사용자 행동을 분석하는 데 매우 중요한 자료가 됩니다. 예를 들어, 어떤 광고 채널을 통해 유입이 많이 되었는지 파악하여 효율적인 예산 집행에 도움을 줍니다.
- QR 코드 활용: QR 코드는 긴 텍스트보다 짧은 텍스트를 담을 때 훨씬 더 간결하고 인식률이 높습니다. 단축 URL은 QR 코드와 결합하여 오프라인에서 온라인으로의 연결을 매끄럽게 만듭니다.
1.2. 단축 URL 리다이렉션 원리: 지름길을 안내하는 웹의 마법
그렇다면 사용자가 짧은 URL을 클릭했을 때, 원래의 긴 URL로 어떻게 연결될까요? 이 과정은 '리다이렉션(Redirection)', 즉 '재방향' 또는 '전환'이라는 핵심 원리를 기반으로 합니다. 비유하자면, 단축 URL은 '지름길 안내 표지판'이고, 단축 URL 서버는 그 표지판을 보고 원래 목적지로 가는 길을 알려주는 '정보 데스크'와 같습니다.
단축 URL 리다이렉션 원리는 다음과 같은 단계로 진행됩니다:
- 사용자 클릭: 사용자가
https://yours.is/abcde와 같은 단축 URL을 클릭합니다. - 서버 요청: 웹 브라우저는 이 단축 URL에 해당하는 요청을 단축 URL 서비스 서버(
yours.is)로 보냅니다. - 데이터베이스 조회: 단축 URL 서버는 요청받은
abcde라는 단축 코드(Short Code)를 자신의 데이터베이스에서 찾습니다. 이 데이터베이스에는abcde에 해당하는 원본 URL(예:https://www.example.com/products/...)이 저장되어 있습니다. - 리다이렉션 응답: 서버는 데이터베이스에서 찾은 원본 URL을 가지고, 브라우저에게 "이 주소는 이제 저기로 옮겨졌으니, 저쪽으로 다시 가세요!"라는 명령을 내립니다. 이때 HTTP 상태 코드(Status Code)를 사용하여 명령을 전달합니다.
- HTTP 301 (Moved Permanently): "영구적으로 이동되었습니다."라는 의미입니다. 브라우저는 이 단축 URL이 영원히 특정 원본 URL로 연결될 것임을 학습하고, 다음부터는 단축 URL을 거치지 않고 직접 원본 URL로 이동하려고 시도할 수 있습니다. 이는 검색 엔진 최적화(SEO) 측면에서 원본 URL의 랭킹을 보존하는 데 유리합니다.
- HTTP 302 (Found / Moved Temporarily): "일시적으로 발견되었습니다." 또는 "잠시 다른 곳으로 이동되었습니다."라는 의미입니다. 브라우저는 다음에 같은 단축 URL을 클릭할 때는 다시 서버에 요청하여 새로운 원본 URL을 받을 준비를 합니다. 클릭 통계를 정확히 집계하거나, 필요에 따라 원본 URL을 변경할 가능성이 있을 때 주로 사용됩니다.
- (참고: HTTP 307(Temporary Redirect)과 308(Permanent Redirect)도 있지만, 301/302가 가장 흔하며 개념 설명에 적합합니다.)*
- 원본 URL로 이동: 브라우저는 서버로부터 받은 리다이렉션 명령(원본 URL)을 해석하고, 해당 원본 URL로 다시 요청을 보내 최종적으로 웹페이지에 접속합니다.
이러한 일련의 과정은 눈 깜짝할 사이에 이루어지며, 사용자 입장에서는 단지 짧은 링크를 클릭했는데 원래 웹페이지가 나타나는 것처럼 느껴집니다. 이 원리를 이해하는 것이 단축 URL 서버 구축의 첫걸음이자 가장 중요한 부분입니다.
2. 나만의 단축 URL 서버 구축을 위한 핵심 설계 및 고려사항
이제 단축 URL의 작동 원리를 이해했으니, 실제로 나만의 단축 URL 서비스 만들기를 위한 기술적인 설계에 대해 알아보겠습니다. 직접 단축 URL 서버 구축을 계획할 때 가장 중요한 것은 핵심 기능을 안정적이고 효율적으로 구현하는 것입니다. 이 섹션에서는 단축 URL 데이터베이스 설계, 고유한 단축 코드 생성 방법, 리다이렉션 처리, 그리고 서비스의 확장성을 위한 고려사항들을 심도 있게 다룹니다. 이는 링크 단축 서버 개발의 근간이 되는 설계 과정입니다.
2.1. 단축 URL 데이터베이스 설계: 서비스 정보의 심장부
단축 URL 서버의 핵심은 바로 '단축 코드'와 '원본 URL'의 짝을 저장하고 관리하는 데이터베이스입니다. 효율적인 데이터베이스 설계는 서비스의 성능과 안정성을 좌우합니다.
필수적으로 고려해야 할 필드(컬럼)들은 다음과 같습니다:
id(Primary Key): 각 단축 URL을 고유하게 식별하는 번호입니다. 보통 자동 증가(Auto-increment) 정수형으로 설정합니다. 이id를 기반으로 단축 코드를 생성하는 방식이 많이 사용됩니다.original_url(원본 URL): 사용자가 단축하고자 하는 실제 웹 주소입니다. 이 필드는 텍스트(VARCHAR/TEXT) 타입으로, 충분히 긴 URL을 저장할 수 있도록 길이를 넉넉하게 지정해야 합니다 (예: 2048자 이상).short_code(단축 코드): 생성된 짧은 코드 (예:abcde). 이 코드를 통해 원본 URL을 조회하므로, 데이터베이스 내에서 고유(Unique)해야 하며, 빠른 검색을 위해 인덱스(Index)를 반드시 생성해야 합니다. 길이는 서비스의 필요에 따라 5~10자 정도로 설정할 수 있습니다.created_at(생성 시각): 단축 URL이 생성된 시점을 기록합니다.DATETIME또는TIMESTAMP타입으로 설정하여 데이터의 생성 이력을 관리합니다.expires_at(만료 시각 - 선택 사항): 단축 URL이 유효한 기간을 설정할 수 있다면 유용합니다. 만료된 링크는 더 이상 작동하지 않도록 할 수 있습니다.DATETIME타입.click_count(클릭 횟수 - 선택 사항): 해당 단축 URL이 클릭된 횟수를 기록하여 통계 정보를 제공하는 데 사용됩니다.INTEGER타입으로 설정합니다.
데이터베이스 선택:
- 관계형 데이터베이스(RDBMS): PostgreSQL, MySQL 등이 대표적입니다. 데이터의 일관성과 무결성이 중요하며, 복잡한 쿼리가 필요한 경우에 적합합니다.
original_url과short_code간의 1:1 관계를 명확히 표현할 수 있습니다. - NoSQL 데이터베이스: MongoDB, Redis 등이 있습니다. 대량의 비정형 데이터 처리나 매우 빠른 읽기/쓰기 성능이 필요한 경우에 고려할 수 있습니다. 예를 들어, Redis는 캐싱 용도로
short_code->original_url매핑을 저장하여 조회 속도를 극대화하는 데 활용될 수 있습니다.
작은 규모의 프로젝트나 학습 목적이라면 SQLite와 같은 파일 기반 RDBMS도 좋은 시작점이 될 수 있습니다. short_code 컬럼에 인덱스를 걸어두는 것은 수많은 단축 코드 중에서 특정 코드를 빠르게 찾아내는 데 필수적입니다. 인덱스가 없으면 매번 모든 데이터를 스캔해야 하므로 성능 저하가 발생합니다.
2.2. 고유한 단축 코드 생성 방법: 중복 없는 안전한 지름길 만들기
단축 코드는 서비스의 얼굴과 같습니다. 짧고, 기억하기 쉬우면서도, 무엇보다 고유해야 합니다. 두 개의 단축 URL이 같은 코드를 가지면 안 되기 때문입니다. 고유한 단축 코드를 생성하는 몇 가지 방법을 살펴보겠습니다.
- 순차 ID를 기반으로 Base62 인코딩:
- 가장 흔하고 안정적인 방법 중 하나입니다. 데이터베이스에 URL을 저장할 때 생성되는 자동 증가
id값을 활용합니다. - Base62는 0-9, A-Z, a-z (총 62개 문자)를 사용하여 숫자를 표현하는 방식입니다. 일반적인 10진수보다 더 짧은 길이로 큰 숫자를 표현할 수 있습니다.
- 예시: ID가 1000인 경우, 이를 Base62로 변환하면
g8이 됩니다. ID가 100000000인 경우,9m7i가 됩니다. - 장점: 중복될 일이 없고, 예측 가능하며, 구현이 비교적 쉽습니다.
- 단점: 순차적으로 생성되므로, 악의적인 사용자가 다음 단축 URL을 예측할 수 있다는 보안상의 약점이 있을 수 있습니다.
- 가장 흔하고 안정적인 방법 중 하나입니다. 데이터베이스에 URL을 저장할 때 생성되는 자동 증가
- 랜덤 문자열 생성:
- 소문자, 대문자, 숫자 등을 조합하여 완전히 무작위의 문자열을 생성합니다.
- 예시:
import random, string; ''.join(random.choices(string.ascii_letters + string.digits, k=7)) - 장점: 예측 불가능하여 보안상 유리합니다.
- 단점: 무작위이기 때문에 중복될 가능성이 존재합니다. 따라서 생성 후에는 반드시 데이터베이스에서 해당 코드가 이미 사용 중인지 확인하는 절차가 필요하며, 중복될 경우 재시도해야 합니다. 코드가 짧을수록 충돌 확률은 높아집니다.
- 충돌 방지 로직:
def generate_unique_short_code(length=7): characters = string.ascii_letters + string.digits # a-zA-Z0-9 while True: short_code = ''.join(random.choices(characters, k=length)) # 데이터베이스에서 short_code가 존재하는지 확인 if not is_short_code_exists_in_db(short_code): return short_code # 존재하면 다시 생성 (무한 루프 방지 로직 필요)
- 해싱(Hashing) 기법:
- 원본 URL을 특정 해시 함수(예: MD5, SHA256)에 넣어 고정된 길이의 해시 값을 생성한 후, 이 값의 일부를 단축 코드로 사용하는 방법입니다.
- 장점: 입력값에 따라 결과가 고유하게 달라지므로, 동일한 URL에 대해서는 항상 같은 단축 코드를 생성할 수 있습니다.
- 단점: 해시 충돌(Hash Collision)의 가능성이 있으며, 해시 값 자체가 너무 길 수 있으므로 특정 길이로 잘라야 합니다. 자르는 과정에서 충돌 가능성이 더 높아집니다. 실제 단축 URL 서비스에서는 잘 사용되지 않습니다.
가장 보편적이고 효율적인 방법은 순차 ID를 이용한 Base62 인코딩과 랜덤 문자열 생성 후 중복 확인을 조합하는 것입니다. 예를 들어, 특정 길이의 랜덤 문자열을 기본으로 사용하되, 짧은 URL의 길이가 너무 짧아 충돌 가능성이 높아지면 길이를 늘리거나, 아니면 id를 기반으로 한 코드를 폴백(fallback)으로 사용하는 전략을 고려할 수 있습니다.
2.3. 리다이렉션 처리 전략: 올바른 HTTP 상태 코드의 중요성
이전 섹션에서 설명했듯이, 리다이렉션은 단축 URL 서버의 핵심 기능입니다. 이때 어떤 HTTP 상태 코드를 사용할지는 SEO(검색 엔진 최적화) 및 통계 추적에 중요한 영향을 미칩니다.
- 301 Moved Permanently: "이 리소스는 영구적으로 새 위치로 이동했습니다."
- 사용 시점: 단축 URL이 영구적으로 특정 원본 URL을 가리키도록 의도할 때 사용합니다. 예를 들어, 브랜드의 대표적인 짧은 링크를 만들 때.
- 장점: 검색 엔진이 원래 페이지의 순위(PageRank)를 새 URL로 전달하는 데 도움을 줍니다. 브라우저는 이 정보를 캐시하여 다음부터는 단축 URL을 거치지 않고 직접 원본 URL로 이동합니다.
- 단점: 캐싱 때문에 클릭 추적에 어려움이 있을 수 있습니다 (브라우저가 캐시된 정보를 사용하면 서버로 요청이 오지 않음). 또한, 한번 설정하면 단축 URL의 목적지를 변경하기 어렵습니다.
- 302 Found (과거에는 Moved Temporarily): "요청된 리소스는 일시적으로 다른 URL 아래에 위치합니다."
- 사용 시점: 클릭 통계를 정확히 수집해야 하거나, 나중에 단축 URL의 목적지(원본 URL)를 변경할 가능성이 있을 때 사용합니다. 대부분의 단축 URL 서비스가 이 방식을 사용합니다.
- 장점: 매번 서버에 요청이 오기 때문에 정확한 클릭 통계를 기록할 수 있습니다. 원본 URL을 자유롭게 변경할 수 있습니다.
- 단점: 검색 엔진이 원래 페이지의 순위 정보를 새 URL로 전달하지 않으므로, SEO 측면에서는 301보다 불리합니다.
대부분의 링크 단축 서버 개발 시에는 302 Redirect를 사용하여 유연성을 확보하고 정확한 통계를 집계하는 것을 선호합니다.
2.4. 확장성 및 보안 강화 고려사항: 안정적인 서비스 운영을 위해
서비스가 성장하면 더 많은 사용자와 더 많은 트래픽을 처리해야 합니다. 미리 확장성을 고려하지 않으면 서비스가 멈추거나 느려질 수 있습니다.
- 데이터베이스 최적화:
short_code필드에 인덱스를 잘 설정했는지 다시 확인합니다. 대량의 데이터를 처리할 때는 데이터베이스 서버를 분리하거나, Read Replica(읽기 전용 복제본)를 두어 읽기 부하를 분산하는 것도 고려해야 합니다. Redis와 같은 인메모리 데이터베이스를 활용하여 자주 조회되는 단축 코드와 원본 URL 매핑을 캐싱하면 조회 속도를 획기적으로 향상시킬 수 있습니다. - 로드 밸런싱: 여러 대의 서버에 트래픽을 분산시켜 특정 서버에 과부하가 걸리는 것을 방지합니다.
- API Rate Limiting: 악의적인 봇이나 스크립트가 무분별하게 단축 URL을 생성하거나 리다이렉션을 요청하는 것을 방지하기 위해, 특정 IP 주소나 사용자당 요청 횟수를 제한하는 기능입니다.
- URL 검증 및 필터링: 사용자가 단축하려는 URL이 피싱 사이트나 악성 코드를 유포하는 곳이 아닌지 검증하는 로직이 필요합니다. (예: Google Safe Browsing API 활용)
- HTTPS 적용: 모든 통신에 SSL/TLS 암호화를 적용하여 데이터 가로채기를 방지하고 사용자에게 신뢰를 줍니다.
- SQL 인젝션 및 XSS 방어: 사용자 입력값을 데이터베이스 쿼리에 직접 사용하거나 HTML에 출력할 때 항상 필터링하고 이스케이프 처리하여 보안 취약점을 방지해야 합니다.
이러한 고려사항들을 바탕으로 설계하면, 안정적이고 효율적이며 안전한 단축 URL 서버 구축을 위한 탄탄한 기반을 마련할 수 있습니다.
3. 파이썬 Flask 기반 단축 URL 서버 구현 예제: 핵심 로직 이해
이 섹션에서는 앞서 배운 개념들을 바탕으로 실제 파이썬 단축 URL 예제를 통해 간단한 단축 URL 서버를 만들어 보겠습니다. 우리는 가볍고 유연한 웹 프레임워크인 Flask를 사용하여, URL을 저장하고 단축 코드를 생성하며 리다이렉션을 처리하는 기본적인 흐름을 구현할 것입니다. 이를 통해 단축 URL 서버 구축의 핵심 로직을 직접 경험할 수 있습니다.
3.1. 개발 환경 설정: Flask 및 ShortUUID 설치
먼저 Python이 설치되어 있어야 합니다. 그 다음, 필요한 라이브러리들을 설치합니다.
# 가상 환경 생성 (권장)
python -m venv venv
source venv/bin/activate # macOS/Linux
# venv\Scripts\activate.bat # Windows
# Flask 및 기타 필요한 라이브러리 설치
pip install Flask ShortUUID
- Flask: 웹 애플리케이션 개발을 위한 마이크로 프레임워크.
- ShortUUID: 짧고 유니크한 ID를 생성하는 데 유용한 라이브러리. (이전 섹션의 랜덤 문자열 생성과 유사하며, 예측 불가능한 짧은 ID 생성에 강점)
3.2. 단축 URL 서버 핵심 코드 (app.py)
우리는 데이터베이스 대신, 간단한 예제를 위해 딕셔너리(Dictionary)를 사용하여 URL 매핑을 메모리에 저장할 것입니다. 실제 서비스에서는 관계형 데이터베이스(SQLite, PostgreSQL 등)를 사용해야 합니다.
import string
import random
from flask import Flask, request, redirect, url_for, render_template, flash
from shortuuid import ShortUUID # 짧고 유니크한 ID 생성 라이브러리
app = Flask(__name__)
app.secret_key = 'super_secret_key' # Flash 메시지를 위한 시크릿 키 (실제 서비스에서는 복잡하게 설정)
# 메모리 기반 데이터 저장 (실제 서비스에서는 DB 사용)
# { "short_code": "original_url" }
url_mapping = {}
# 이미 사용된 short_code를 추적하여 중복 방지 (실제 서비스에서는 DB 조회)
used_short_codes = set()
# ShortUUID 초기화 (알파벳 대소문자 + 숫자만 사용)
# 기본적으로 ShortUUID는 '-' 문자를 포함할 수 있으므로, 우리가 원하는 문자셋으로 커스터마이징
uuid_generator = ShortUUID(alphabet=string.ascii_letters + string.digits)
# --- 유틸리티 함수 ---
def generate_unique_short_code(length=7):
"""
고유한 단축 코드를 생성하는 함수.
ShortUUID를 사용하거나, 랜덤 문자열 생성 후 중복 확인 로직을 포함.
"""
while True:
# ShortUUID를 사용하여 고유 ID 생성.
# 길이를 지정하여 원하는 길이의 코드를 얻을 수 있습니다.
short_code = uuid_generator.random(length=length)
# 실제 DB에서는 DB 쿼리를 통해 중복 여부를 확인합니다.
# 여기서는 메모리 기반의 used_short_codes set을 사용합니다.
if short_code not in used_short_codes:
used_short_codes.add(short_code)
return short_code
# 중복된 경우 다시 시도 (매우 드물게 발생)
# --- Flask 라우트 (URL 경로) ---
@app.route('/', methods=['GET', 'POST'])
def index():
"""
메인 페이지: URL 단축 폼 제공 및 결과 표시
"""
if request.method == 'POST':
original_url = request.form['original_url']
# URL 유효성 검사 (간단한 예시)
if not (original_url.startswith('http://') or original_url.startswith('https://')):
flash('유효하지 않은 URL 형식입니다. "http://" 또는 "https://"로 시작해야 합니다.', 'error')
return render_template('index.html')
# 이미 단축된 URL인지 확인 (중복 단축 방지)
for short_code, mapped_url in url_mapping.items():
if mapped_url == original_url:
flash(f'이 URL은 이미 단축되었습니다: {url_for("redirect_to_long_url", short_code=short_code, _external=True)}', 'info')
return render_template('index.html')
# 고유한 단축 코드 생성
short_code = generate_unique_short_code()
# 매핑 저장
url_mapping[short_code] = original_url
# 단축된 URL 생성
# url_for 함수는 Flask 애플리케이션 내의 특정 라우트(redirect_to_long_url)로 가는 URL을 생성
# _external=True 를 사용하여 완전한 URL (예: http://127.0.0.1:5000/abcde)을 만듭니다.
shortened_url = url_for('redirect_to_long_url', short_code=short_code, _external=True)
flash(f'URL이 성공적으로 단축되었습니다! 단축 URL: <a href="{shortened_url}" target="_blank">{shortened_url}</a>', 'success')
return render_template('index.html', short_url=shortened_url)
return render_template('index.html')
@app.route('/<short_code>')
def redirect_to_long_url(short_code):
"""
단축 코드를 받아서 원래 URL로 리다이렉션하는 라우트
"""
original_url = url_mapping.get(short_code)
if original_url:
# HTTP 302 Found (Temporary Redirect)로 리다이렉션
# 302는 통계 추적 및 URL 변경 가능성을 위해 주로 사용됩니다.
# flash 메시지는 리다이렉트 후에는 사라지므로 여기에선 사용하지 않습니다.
return redirect(original_url, code=302)
else:
# 단축 코드를 찾을 수 없는 경우 404 Not Found 페이지 표시
return render_template('404.html', message=f'단축 코드 "{short_code}"에 해당하는 URL을 찾을 수 없습니다.'), 404
@app.errorhandler(404)
def page_not_found(e):
"""
404 에러 핸들러
"""
return render_template('404.html', message='요청하신 페이지를 찾을 수 없습니다.'), 404
if __name__ == '__main__':
app.run(debug=True) # 개발 모드: 코드 변경 시 자동 재시작 및 디버깅 정보 제공
3.3. HTML 템플릿 파일: 사용자 인터페이스 구성
templates 폴더를 만들고, 그 안에 index.html과 404.html 파일을 생성합니다.
templates/index.html:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>나만의 단축 URL 서비스</title>
<style>
body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; margin-top: 50px; background-color: #f4f7f6; color: #333; }
.container { background-color: #ffffff; padding: 30px 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 100%; max-width: 500px; text-align: center; }
h1 { color: #2c3e50; margin-bottom: 25px; }
form { display: flex; flex-direction: column; gap: 15px; }
input[type="url"] { padding: 12px; border: 1px solid #ddd; border-radius: 5px; font-size: 1em; width: calc(100% - 24px); }
input[type="submit"] { background-color: #3498db; color: white; padding: 12px 20px; border: none; border-radius: 5px; cursor: pointer; font-size: 1.1em; transition: background-color 0.3s ease; }
input[type="submit"]:hover { background-color: #2980b9; }
.flash-message { padding: 10px 20px; margin-top: 20px; border-radius: 5px; text-align: left; }
.flash-message.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
.flash-message.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
.flash-message.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
.short-url-display { margin-top: 25px; padding: 15px; background-color: #e9ecef; border-radius: 5px; text-align: left; word-wrap: break-word; }
.short-url-display a { color: #2980b9; text-decoration: none; font-weight: bold; }
.short-url-display a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>나만의 단축 URL 서비스</h1>
<form method="POST">
<input type="url" name="original_url" placeholder="여기에 긴 URL을 붙여넣으세요 (예: https://example.com/very/long/url)" required>
<input type="submit" value="단축하기">
</form>
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="flash-message {{ category }}">
{{ message | safe }} {# `safe` 필터는 HTML을 렌더링할 수 있게 합니다. 보안에 유의하세요. #}
</div>
{% endfor %}
{% endif %}
{% endwith %}
</div>
</body>
</html>
templates/404.html:
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>페이지를 찾을 수 없습니다 - 404</title>
<style>
body { font-family: Arial, sans-serif; display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100vh; margin: 0; background-color: #f4f7f6; color: #333; }
.container { background-color: #ffffff; padding: 30px 40px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
h1 { color: #e74c3c; margin-bottom: 15px; }
p { font-size: 1.1em; line-height: 1.6; }
a { color: #3498db; text-decoration: none; font-weight: bold; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<h1>404 - 페이지를 찾을 수 없습니다</h1>
<p>{{ message }}</p>
<p>혹은 <a href="/">메인 페이지로 돌아가기</a></p>
</div>
</body>
</html>
3.4. 코드 설명 및 실행 방법: 직접 경험해보기
app.py:url_mapping: 단축 코드와 원본 URL 매핑을 저장하는 딕셔너리입니다. 실제 서비스에서는 이 부분을 데이터베이스(SQLite,PostgreSQL,MySQL등) 연동 코드로 대체해야 합니다.generate_unique_short_code():ShortUUID라이브러리를 사용하여 지정된 길이(length=7)의 고유한 단축 코드를 생성합니다.string.ascii_letters + string.digits를alphabet으로 지정하여 영문 대소문자와 숫자만을 사용하도록 제한합니다. 이는 나만의 단축 URL 서비스 만들기의 핵심 기능 중 하나입니다.@app.route('/'): 사용자가 웹사이트의 루트(http://127.0.0.1:5000/)에 접속했을 때 단축 URL 생성 폼을 보여주고, POST 요청으로 URL이 제출되면generate_unique_short_code를 호출하여 단축 코드를 생성하고 매핑을url_mapping에 저장합니다.@app.route('/<short_code>'): 사용자가 단축 URL(http://127.0.0.1:5000/abcde)에 접속했을 때 해당short_code를 찾아url_mapping에서 원본 URL을 조회한 후,redirect(original_url, code=302)를 통해 302 리다이렉션을 수행합니다.app.secret_key: Flask의flash메시지를 사용하기 위한 필수 설정입니다.flash메시지: 사용자에게 성공, 오류, 정보 메시지를 표시하는 데 사용됩니다.render_template:templates폴더의 HTML 파일을 렌더링하여 사용자에게 보여줍니다.
- 실행 방법:
- 위의
app.py파일을 생성합니다. templates라는 폴더를 만들고 그 안에index.html과404.html파일을 생성합니다.- 터미널(명령 프롬프트)에서
app.py파일이 있는 디렉토리로 이동합니다. flask run또는python app.py명령을 실행합니다.- 웹 브라우저에서
http://127.0.0.1:5000/로 접속하여 테스트할 수 있습니다.
- 위의
이 간단한 파이썬 단축 URL 예제는 링크 단축 서버 개발의 기본적인 흐름을 보여줍니다. 실제 프로덕션 환경에서는 데이터베이스 연동, 에러 처리, 보안 강화, 로깅 등 더 많은 요소를 고려해야 합니다. 하지만 이 예시만으로도 단축 URL 서버가 어떻게 작동하는지 충분히 이해하고 직접 조작해볼 수 있을 것입니다.
4. 단축 URL 서버 배포 및 안정적인 운영 전략
단축 URL 서버 구축이 성공적으로 완료되었다면, 이제 이 서비스를 실제 사용자들이 이용할 수 있도록 세상에 내놓을 차례입니다. 이 과정은 단순히 코드를 실행하는 것을 넘어, 서버를 안정적으로 운영하고 관리하는 데 필요한 여러 단계를 포함합니다. 이 섹션에서는 구현된 링크 단축 서버 개발의 결과물을 배포하고, 서비스 운영 시 고려해야 할 중요한 사항들을 안내합니다.
4.1. 배포 준비: 프로덕션 환경을 위한 최적화
개발 환경에서 잘 작동하던 Flask 애플리케이션을 실제 서비스 환경(프로덕션 환경)에 배포하기 위해서는 몇 가지 준비가 필요합니다. Flask의 내장 웹 서버는 개발용이며, 실제 트래픽을 처리하기에는 부적합합니다.
- WSGI 서버 사용:
- WSGI (Web Server Gateway Interface)는 파이썬 웹 애플리케이션과 웹 서버 간의 표준 인터페이스입니다.
- Gunicorn, uWSGI와 같은 WSGI 서버는 Flask 앱을 실행하고, 다수의 동시 요청을 효율적으로 처리할 수 있게 해줍니다.
- 설치:
pip install gunicorn - 실행 예시:
gunicorn -w 4 app:app(4개의 워커 프로세스로app.py파일의app객체를 실행)
- 리버스 프록시 서버 설정:
- Nginx나 Apache와 같은 웹 서버를 리버스 프록시로 사용합니다.
- 이들은 외부 요청을 받아 Gunicorn(WSGI 서버)으로 전달하고, Gunicorn의 응답을 다시 클라이언트에게 전달하는 역할을 합니다.
- 장점:
- 정적 파일 서빙: HTML, CSS, JavaScript, 이미지 등 정적 파일을 직접 처리하여 WSGI 서버의 부하를 줄입니다.
- SSL/TLS (HTTPS) 종료: 암호화/복호화 작업을 담당하여 보안을 강화합니다.
- 로드 밸런싱: 여러 WSGI 서버 인스턴스 간에 트래픽을 분산시킬 수 있습니다.
- 보안 강화: 외부 공격으로부터 WSGI 서버를 보호합니다.
- 환경 변수 관리:
- 데이터베이스 연결 정보, API 키,
app.secret_key등 민감한 정보는 코드에 직접 하드코딩하지 않고, 환경 변수로 관리해야 합니다.python-dotenv라이브러리를 사용하거나, 배포 플랫폼의 환경 변수 기능을 활용합니다.
- 데이터베이스 연결 정보, API 키,
4.2. 클라우드 플랫폼 활용: 효율적인 서비스 런칭
구현된 단축 URL 서버를 실제 서비스로 배포하는 가장 일반적이고 효율적인 방법은 클라우드 컴퓨팅 플랫폼을 활용하는 것입니다. 대표적인 서비스들은 다음과 같습니다.
- AWS (Amazon Web Services):
- EC2 (Elastic Compute Cloud): 가상 서버를 직접 생성하여 OS부터 웹 서버, WSGI 서버, 애플리케이션까지 모두 설정하고 관리합니다. 가장 유연하지만 가장 많은 설정이 필요합니다.
- Elastic Beanstalk: Flask 애플리케이션 배포를 자동화하고 스케일링, 로드 밸런싱 등을 쉽게 관리할 수 있는 PaaS(Platform as a Service) 서비스입니다. 초보자에게 추천됩니다.
- Google Cloud Platform (GCP):
- Compute Engine: AWS EC2와 유사한 가상 머신 서비스입니다.
- App Engine: Elastic Beanstalk과 유사한 PaaS 서비스로, Python 애플리케이션 배포에 최적화되어 있습니다.
- Microsoft Azure:
- Virtual Machines: GCP Compute Engine, AWS EC2와 유사합니다.
- App Service: Flask를 포함한 다양한 언어의 웹 앱 배포를 지원하는 PaaS 서비스입니다.
- Heroku:
- 개발자 친화적인 PaaS로, Git 푸시만으로 쉽게 애플리케이션을 배포할 수 있습니다. 소규모 프로젝트나 빠른 프로토타이핑에 매우 유용합니다.
- Vercel / Netlify:
- 주로 정적 사이트 호스팅에 강점이지만, 서버리스 함수(Serverless Functions)를 통해 간단한 백엔드 로직도 처리할 수 있어 소규모 단축 URL 서비스에도 활용될 수 있습니다.
배포 과정의 일반적인 흐름 (EC2나 Compute Engine 기준):
- 가상 머신(VM) 생성: Ubuntu나 CentOS와 같은 리눅스 운영체제를 선택하여 VM 인스턴스를 생성합니다.
- 보안 그룹(방화벽) 설정: HTTP(80), HTTPS(443) 포트를 열어 외부에서 웹 서버에 접속할 수 있도록 합니다. SSH(22) 포트는 서버 관리를 위해 제한된 IP에서만 허용합니다.
- 애플리케이션 코드 업로드: Git을 통해 코드를 VM에 복제(clone)하거나 FTP/SFTP를 통해 업로드합니다.
- 필요 소프트웨어 설치: Python, pip, Gunicorn, Nginx, 데이터베이스 클라이언트 등을 설치합니다.
- 환경 설정: 환경 변수를 설정하고, Nginx 설정 파일을 작성하여 Gunicorn과 연동합니다.
- 서비스 시작: Gunicorn, Nginx, 데이터베이스 서버를 시작하고, 시스템 시작 시 자동으로 실행되도록 설정합니다 (systemd 서비스 등록).
- 도메인 연결: 구매한 도메인(예:
my-short.link)을 서버의 IP 주소와 연결합니다. DNS 레코드(A 레코드 또는 CNAME 레코드)를 설정합니다. - HTTPS 적용: Let's Encrypt와 같은 무료 SSL/TLS 인증서를 발급받아 Nginx에 적용하여 HTTPS를 활성화합니다. 이는 링크 단축 서버 개발 서비스의 신뢰도를 높이고 보안을 강화하는 필수 과정입니다.
4.3. 운영 시 고려사항: 서비스의 지속적인 관리와 성장
배포가 끝났다고 해서 모든 것이 완료된 것은 아닙니다. 서비스가 안정적으로 계속 작동하도록 운영하는 것이 중요합니다.
- 모니터링:
- 서버 리소스: CPU 사용량, 메모리 사용량, 디스크 공간 등을 지속적으로 모니터링하여 과부하 징후를 조기에 감지합니다. (Prometheus, Grafana, 클라우드 제공 모니터링 도구)
- 웹 트래픽: 요청 수, 응답 시간, 에러율 등을 모니터링하여 서비스 성능을 파악합니다.
- 데이터베이스: 쿼리 성능, 연결 수, 디스크 I/O 등을 모니터링하여 병목 현상을 파악합니다.
- 애플리케이션 로그: 에러 메시지, 사용자 요청 로그 등을 수집하여 문제 발생 시 원인을 파악합니다. (ELK Stack, CloudWatch Logs, Splunk)
- 보안:
- 정기적인 업데이트: 운영체제, 라이브러리, 프레임워크 등을 최신 상태로 유지하여 알려진 취약점을 제거합니다.
- 방화벽 설정: 필요한 포트만 개방하고, 불필요한 네트워크 접근을 차단합니다.
- DDoS 방어: 분산 서비스 거부 공격에 대비하여 클라우드 서비스의 방어 기능을 활용하거나 CDN(Content Delivery Network)을 사용합니다.
- 데이터 암호화: 데이터베이스에 저장되는 민감 정보는 암호화하여 저장하고, 전송 중인 데이터는 HTTPS를 통해 암호화합니다.
- 백업: 데이터베이스와 애플리케이션 코드를 정기적으로 백업하여 데이터 손실에 대비합니다.
- 확장성:
- 수평 확장 (Scale-out): 트래픽 증가에 따라 서버 인스턴스를 여러 대 추가하고 로드 밸런서를 통해 트래픽을 분산시킵니다.
- 데이터베이스 샤딩/레플리카: 데이터베이스 부하가 커지면 데이터를 여러 DB 서버에 분산시키거나, 읽기 전용 복제본을 두어 읽기 요청을 분산합니다.
- 캐싱: Redis와 같은 인메모리 캐시를 활용하여 데이터베이스 접근 횟수를 줄여 응답 속도를 향상시킵니다.
이러한 배포 및 운영 전략을 통해 여러분이 직접 단축 URL 서버 구축한 서비스는 안정적으로 사용자에게 제공될 수 있습니다. 링크 단축 서버 개발은 단순히 코드를 작성하는 것을 넘어, 서비스의 생명 주기를 관리하는 포괄적인 과정임을 기억해야 합니다.
5. 단축 URL 서비스 선택 가이드: 직접 구축 vs 기성 솔루션
나만의 단축 URL 서비스 만들기는 매력적인 목표이지만, 항상 직접 구축만이 정답은 아닙니다. 시장에는 이미 Bitly, TinyURL과 같은 강력한 상용 서비스들이 있고, YOURLS, Kutt와 같은 오픈소스 솔루션도 존재합니다. 이 섹션에서는 각 방식의 장단점을 비교하고, 여러분의 목표와 상황에 맞는 단축 URL 서버 구축 전략을 제시하여 현명한 선택을 돕겠습니다. 이 비교는 링크 단축 서버 개발 여부를 결정하는 데 중요한 기준이 될 것입니다.
5.1. 기존 단축 URL 서비스 활용 (Bitly, TinyURL 등): 편리함 뒤의 트레이드오프
가장 쉽고 빠르게 단축 URL 기능을 사용할 수 있는 방법입니다.
- 장점:
- 편리성 및 즉시 사용 가능: 회원 가입 후 바로 사용할 수 있으며, 별도의 설치나 설정이 필요 없습니다.
- 고가용성 및 확장성: 대규모 트래픽을 안정적으로 처리할 수 있도록 설계되어 있으며, 서비스 제공업체가 모든 인프라를 관리합니다.
- 다양한 부가 기능: 클릭 통계, QR 코드 생성, 사용자 정의 도메인 연결(유료), 캠페인 관리 등 고급 기능을 제공합니다.
- 보안 전문가 관리: 피싱 URL 방지, DDoS 공격 방어 등 보안에 대한 전문적인 관리가 이루어집니다.
- 단점:
- 커스터마이징 제한: 서비스에서 제공하는 기능 범위 내에서만 사용해야 하며, 특정 비즈니스 로직을 통합하기 어렵습니다.
- 브랜드 종속성: 대부분의 무료 서비스는 단축 URL에 자체 도메인(예:
bit.ly/xxxx)을 사용합니다. 기업 브랜딩에는 불리할 수 있습니다. - 데이터 프라이버시: 모든 URL 데이터와 클릭 통계가 외부 서비스 제공업체의 서버에 저장됩니다. 민감한 정보를 다루는 경우 프라이버시 문제가 발생할 수 있습니다.
- 비용: 고급 기능이나 대규모 사용은 유료 플랜으로 전환해야 하며, 비용이 상당할 수 있습니다.
5.2. 오픈소스 단축 URL 솔루션 활용 (YOURLS, Kutt 등): 자유와 책임의 균형
이미 개발된 오픈소스 솔루션을 자신의 서버에 설치하여 사용하는 방식입니다.
- 장점:
- 무료 및 커스터마이징 가능: 소프트웨어 자체는 무료이며, 필요에 따라 코드를 수정하여 기능을 추가하거나 변경할 수 있습니다.
- 데이터 프라이버시 강화: 모든 데이터가 자신의 서버에 저장되므로, 데이터 주권을 확보할 수 있습니다.
- 자체 도메인 사용 용이: 자신의 도메인(예:
go.mycompany.com)을 연결하여 브랜드 가치를 높일 수 있습니다. - 커뮤니티 지원: 활성화된 커뮤니티를 통해 문제 해결이나 정보 공유가 용이합니다.
- 단점:
- 직접 설치 및 관리 필요: 서버 환경 설정, 소프트웨어 설치, 데이터베이스 연결 등 기술적인 지식과 노력이 필요합니다.
- 유지보수 부담: 정기적인 업데이트, 보안 패치, 문제 해결 등 운영에 대한 책임이 전적으로 사용자에게 있습니다.
- 초기 설정 복잡성: 환경에 따라 설치 과정이 다소 복잡할 수 있습니다.
- 확장성 관리: 트래픽 증가 시 직접 서버 증설, 로드 밸런싱 등을 관리해야 합니다.
5.3. 단축 URL 서버 직접 구축: 완전한 제어권과 기술 성장
이 글에서 다루는 것처럼, 모든 것을 처음부터 직접 코딩하여 단축 URL 서버 구축하는 방식입니다.
- 장점:
- 완전한 제어권 및 무한한 커스터마이징: 필요한 모든 기능을 원하는 방식으로 구현할 수 있으며, 기존 비즈니스 로직과 완벽하게 통합할 수 있습니다.
- 기술적 학습 및 성장: 웹 개발, 백엔드 아키텍처, 데이터베이스, 배포, 운영 등 전반적인 기술 스택을 깊이 있게 이해하고 경험할 수 있습니다. 이는 링크 단축 서버 개발의 궁극적인 목표 중 하나입니다.
- 특정 비즈니스 요구사항 충족: 시장에 없는 매우 특수하거나 니치한 기능을 구현해야 할 때 최적의 선택입니다.
- 브랜드 강화: 완전한 자체 도메인과 디자인을 사용하여 서비스의 정체성을 확립할 수 있습니다.
- 단점:
- 높은 기술적 요구사항: 웹 개발, 데이터베이스 관리, 서버 운영 등 광범위한 기술 지식이 필요합니다.
- 개발 시간과 비용: 초기 개발부터 배포, 운영까지 상당한 시간과 인적/물적 자원이 소모됩니다.
- 유지보수 및 책임 부담: 모든 버그 수정, 보안 취약점 관리, 성능 최적화, 스케일링 등 모든 운영의 책임이 개발자에게 있습니다.
- 보안 및 확장성 문제: 초기에는 보안이나 확장성에 대한 고려가 부족할 수 있어 잠재적인 위험이 있습니다.
5.4. 나에게 맞는 단축 URL 서비스 현명하게 선택하기
어떤 방법을 선택할지는 여러분의 목표, 가용 리소스(시간, 인력, 예산), 기술 수준에 따라 달라집니다.
- 빠른 시작 및 마케팅 목적:
- 당장 단축 URL이 필요하고, 복잡한 통계나 고급 기능이 중요하며, 개발 리소스가 부족하다면 Bitly와 같은 기존 상용 서비스가 가장 효율적입니다. 브랜드 커스터마이징이 필요하다면 유료 플랜을 고려할 수 있습니다.
- 프라이버시 중요 & 기술력 보유 (중급 개발자 이상):
- 데이터 주권을 확보하고 싶고, 어느 정도 서버 관리 경험이 있으며, 커스터마이징의 필요성이 있다면 YOURLS 같은 오픈소스 솔루션을 자신의 서버에 설치하는 것이 좋습니다.
- 깊은 학습 & 완전한 제어 & 특정 기능 구현 (초/중급 개발자 및 실무자 레벨):
- 단축 URL 서버 구축의 전 과정을 학습하여 기술 스택을 확장하고 싶거나, 기존 시스템과의 완벽한 통합이 필수적이며, 서비스에 특화된 기능을 구현해야 한다면 직접 서버를 구축하는 것이 가장 적합합니다. 이는 가장 도전적이지만, 그만큼 얻는 것이 많은 방식입니다. 특히 나만의 단축 URL 서비스 만들기는 단순한 기능 구현을 넘어, 백엔드 개발 전반에 대한 깊이 있는 이해를 선사합니다.
결론적으로, 직접 단축 URL 서버를 구축하는 것은 상당한 노력과 기술적 깊이를 요구하지만, 그 과정에서 얻는 학습 경험과 서비스에 대한 완전한 통제권은 다른 어떤 방식도 제공할 수 없는 가치입니다. 여러분의 상황과 목표를 명확히 정의하고, 가장 적합한 길을 선택하시길 바랍니다.
결론: 나만의 지름길을 만드는 여정
지금까지 우리는 단축 URL의 기본 원리부터 단축 URL 서버 구축을 위한 데이터베이스 설계, 고유한 단축 코드 생성 방법, Python Flask를 활용한 간단한 구현 예시, 그리고 배포 및 운영 전략까지, 링크 단축 서버 개발의 전 과정을 상세히 살펴보았습니다. 마지막으로는 기존 서비스 및 오픈소스 솔루션과의 비교를 통해, 여러분의 상황에 맞는 최적의 선택을 위한 통찰을 제공했습니다.
단순히 URL을 짧게 만드는 기능을 넘어, 이 과정은 웹 애플리케이션의 핵심 구성 요소들(데이터베이스, 서버 로직, 라우팅, 배포, 보안 등)을 통합적으로 이해하고 구현하는 값진 경험이 됩니다. 특히 나만의 단축 URL 서비스 만들기는 여러분이 단순한 사용자에서 벗어나, 기술적인 문제를 스스로 해결하고 창조하는 진정한 개발자로 성장할 수 있는 디딤돌이 될 것입니다.
물론, 직접 서비스를 구축하고 운영하는 것은 쉬운 일이 아닙니다. 예상치 못한 문제에 부딪히고, 수많은 시행착오를 겪을 수도 있습니다. 하지만 그 모든 과정을 통해 얻게 되는 지식과 자신감은 그 어떤 것과도 바꿀 수 없는 소중한 자산이 될 것입니다.
이 가이드가 여러분이 직접 단축 URL 서버 구축에 도전하고, 자신만의 멋진 서비스를 세상에 내놓는 데 훌륭한 나침반이 되기를 바랍니다. 지금 바로 코드를 열고, 여러분만의 지름길을 만들어보세요!
- Total
- Today
- Yesterday
- LLM
- 개발가이드
- 백엔드개발
- 프론트엔드개발
- AI반도체
- 배민
- Java
- 인공지능
- 마이크로서비스
- 웹보안
- AI
- 클라우드컴퓨팅
- springai
- 개발자가이드
- AI기술
- SEO최적화
- n8n
- 업무자동화
- 클린코드
- 개발자성장
- 웹개발
- 프롬프트엔지니어링
- 미래ai
- 성능최적화
- 자바개발
- 로드밸런싱
- 데이터베이스
- 생성형AI
- 개발생산성
- restapi
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | ||||
| 4 | 5 | 6 | 7 | 8 | 9 | 10 |
| 11 | 12 | 13 | 14 | 15 | 16 | 17 |
| 18 | 19 | 20 | 21 | 22 | 23 | 24 |
| 25 | 26 | 27 | 28 | 29 | 30 | 31 |
