
Django 강의에서 URL 단축 서비스 실습을 진행했다.
URL 단축 서비스는 긴 웹 주소를 짧은 주소로 변환해 주는 서비스이다.
예를 들어 아래와 같은 긴 URL이 있다고 가정해보자.
http://www.climate-skeptic.com/2008/10/global-warming-accelerating.html
이 주소를 다음과 같이 짧게 줄일 수 있다.
https://bit.ly/x
사용자는 짧은 URL로 접속하지만, 실제로는 원래의 긴 URL로 이동하게 된다.
프로젝트 생성
먼저 URL 단축 서비스를 만들기 위한 Django 프로젝트를 생성한다.
- 가상환경 생성
- `python3.13 -m venv .short_url`
- 가상환경 활성화
- Mac: `source .short_url/bin/activate`
- Windows: `source .short_url\Scripts\activate`
- Django 설치
- `pip install django`
- 프로젝트 디렉토리 생성
- `mkdir short_url`
- Django 프로젝트 생성
- `django-admin startproject config short_url`
- 프로젝트로 이동
- `cd short_url`
- 서버 실행
- `python manage.py runserver`
앱 생성
사용자 기능과 URL 단축 기능을 분리하기 위해 두 개의 앱을 생성했다.
- `python manage.py startapp user`
- `python manage.py startapp shortener`
생성한 앱은 `settings.py`의 `INSTALLED_APPS`에 등록한다.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'user.apps.UserConfig',
'shortener.apps.ShortenerConfig',
]
CustomUser 추가
Django 프로젝트에서는 기본 User 모델을 그대로 사용할 수도 있지만, 이후 확장 가능성을 고려해서 CustomUser를 정의했다.
from django.contrib.auth.models import AbstractUser
class CustomUser(AbstractUser):
pass
그리고 `settings.py`에 사용할 User 모델을 지정한다.
- `AUTH_USER_MODEL = "user.CustomUser"`
User 모델을 변경했으면 마이그레이션을 적용해야 한다.
- `python manage.py makemigrations`
- `python manage.py migrate`
API 설계
URL 단축 서비스에서 사용할 기능은 다음과 같이 설계했다.
| Method | URL | 설명 |
| GET | / | 홈 화면, 모든 Short URL 목록 |
| POST | /short-urls/ | 새로운 Short URL 생성 |
| GET | /<str:short_url>/ | 원래 URL로 리디렉트 |
| DELETE | /<str:short_url>/ | Short URL 삭제 |
예를 들어 `http://127.0.0.1:8000/b7ea4iQ/` 주소로 접속하면 `b7ea4iQ`에 해당하는 원래 주소로 이동한다.
ShortURL 모델
URL 단축 정보를 저장하기 위해 `ShortURL` 모델을 만들었다.
import string
import random
from django.db import models
class ShortURL(models.Model):
code = models.CharField(max_length=8, unique=True)
original_url = models.URLField(max_length=200)
access_count = models.PositiveIntegerField(default=0)
created_at = models.DateTimeField(auto_now_add=True)
# {app_label}_{클래스 이름 소문자}
# db table: shortener_ shorturl -> short_url
class Meta:
app_label = "shortener"
db_table = "short_url"
@staticmethod
def generate_code():
characters = string.ascii_letters + string.digits
return "".join(random.choices(characters, k=8))
- `code` 필드에는 `unique=True` 적용
- 데이터베이스에 Unique 제약 조건을 생성한다는 의미
- 동일한 `code` 값을 가진 데이터가 중복 저장되지 않음
Form 사용의 한계
HTML Form 기반 요청에서는 기본적으로 `GET`과 `POST` 메서드만 사용할 수 있다.
즉, RESTful하게 설계한 아래 요청은 HTML Form만으로는 바로 보내기 어렵다.
- `DELETE /<str:code>/`
그래서 브라우저에서 Form을 사용할 때는 삭제 요청을 어떻게 처리할지 고민이 필요하다.
해결 방법
1. RESTful한 API 디자인 포기하고 URL 변경
삭제 요청을 DELETE가 아니라 POST로 처리한다.
- ` POST /<str:code>/delete/`
HTML Form에서 `POST` 요청을 보내고, Django View에서는 이를 삭제 요청으로 처리한다.
<form method="post" action="/abc123/delete/">
{% csrf_token %}
<button type="submit">삭제</button>
</form>
간단하게 사용할 수 있지만, RESTful하지 않아 아쉽다.
2. Form에서 POST로 요청하고, Django에서 DELETE로 처리
Django Middleware를 사용하는 방법이다.
Form은 POST로 보내되, hidden input을 추가해서 실제 의도를 표시한다.
<form method="post" action="/abc123/">
{% csrf_token %}
<input type="hidden" name="_method" value="DELETE">
<button type="submit">삭제</button>
</form>
Django에서는 _method 값을 확인해서 삭제 로직을 실행한다.
Middleware를 사용하면 요청이 View에 도달하기 전에 공통 로직을 처리할 수 있다.
예를 들어 Form에서 `_method=DELETE` 값을 보내면 Middleware가 이를 확인하고 요청 메서드를 DELETE로 바꿀 수 있다.
Django MiddleWare
Middleware는 요청과 응답 사이에서 실행되는 중간 처리 계층이다.
사용자의 요청은 View에 도달하기 전에 Middleware를 거친다.
Request → Middleware → View → Middleware → Response
따라서 로그인 확인, 보안 처리, 공통 예외 처리, 요청 데이터 가공 같은 작업을 Middleware에서 처리할 수 있다.
이번 프로젝트에서는 Form에서 보낸 _method 값을 확인해 요청 메서드를 바꾸는 용도로 활용할 수 있다.

ShortURL 삭제 처리
HTML Form은 DELETE 요청을 직접 보낼 수 없기 때문에 hidden input을 사용했다.
<form action="{% url 'short_url_detail' short_url.code %}" method="post">
{% csrf_token %}
<input type="hidden" name="_method" value="DELETE">
<button type="submit" class="btn btn-danger">삭제</button>
</form>
View에서는 다음과 같이 처리할 수 있다.
class ShortURLDetailView(View):
# localhost:8000/abc12345 -> original_url
def get(self, request, code):
short_url = get_object_or_404(ShortURL, code=code)
short_url.access_count = F("access_count") + 1
short_url.save()
return redirect(short_url.original_url)
def delete(self, request, code):
short_url = get_object_or_404(ShortURL, code=code)
short_url.delete()
return redirect("home")
이렇게 하면 실제 HTTP 요청은 POST이지만, 서버에서는 삭제 요청으로 해석할 수 있다.
[참고] 실전! Django 입문 강의