Skip to main content
Summary

로컬에서 끝난 프로젝트는 아직 서비스가 아니다

지금까지 만든 Django 게시판은 로컬 컴퓨터에서 잘 동작한다. python manage.py runserver를 실행하고 브라우저에서 http://127.0.0.1:8000/에 접속하면 목록, 작성, 수정, 삭제, 로그인 기능을 확인할 수 있다.

하지만 이것만으로는 실제 서비스라고 보기 어렵다. runserver는 개발자가 기능을 확인하기 위한 도구다. 코드 변경을 감지해 다시 실행되고, 오류 화면도 자세히 보여 주기 때문에 개발 중에는 편리하다. 그러나 외부 사용자의 요청을 오래 안정적으로 처리하거나, 정적 파일을 효율적으로 제공하거나, 장애 상황을 기록하고 복구하는 운영용 서버로 쓰기에는 맞지 않다.1

배포는 단순히 코드를 서버에 복사하는 일이 아니다. 다음 질문에 답할 수 있어야 한다.

  • 사용자가 접속할 도메인은 무엇인가?

  • Django 코드는 어떤 서버 프로세스가 실행하는가?

  • CSS, JavaScript, 이미지 파일은 누가 제공하는가?

  • 데이터베이스는 어디에 있고, 백업은 어떻게 하는가?

  • 에러가 발생했을 때 어디에서 확인할 수 있는가?

  • 새 버전을 올릴 때 같은 절차를 다시 안전하게 반복할 수 있는가?

이 장에서는 Django 프로젝트를 실제 서버에 올리기 전에 반드시 이해해야 할 기본 요소를 정리한다.

배포와 운영 준비AI 생성 이미지

runserver와 운영 서버의 차이

runserver는 개발 서버다. 개발 중에는 편리하지만 운영 환경에서는 위험하다. 특히 DEBUG=True 상태에서는 오류가 발생했을 때 내부 설정, 파일 경로, 변수 값, 일부 소스 코드 정보가 화면에 노출될 수 있다. 운영에서는 절대 DEBUG=True로 두면 안 된다.

운영 환경에서는 보통 다음과 같은 구조를 사용한다.

사용자 브라우저
  ↓
웹 서버 또는 리버스 프록시(Nginx 등)
  ↓
애플리케이션 서버(Gunicorn 또는 Uvicorn)
  ↓
Django 애플리케이션
  ↓
데이터베이스(PostgreSQL 등)

여기서 웹 서버는 바깥 요청을 먼저 받는다. 도메인으로 들어온 요청을 받아 HTTPS 처리를 맡고, 정적 파일을 직접 제공하며, Django가 처리해야 할 요청만 애플리케이션 서버로 넘긴다. Nginx가 이 역할에 자주 쓰이지만, Caddy, Apache, 클라우드 로드 밸런서처럼 다른 선택지도 있다.

Gunicorn은 WSGI 방식으로 Django 애플리케이션을 실행하는 대표적인 서버다. 일반적인 Django 웹사이트, 관리자 페이지, 게시판, 쇼핑몰처럼 요청을 받고 HTML이나 JSON을 응답하는 서비스라면 Gunicorn으로 충분한 경우가 많다. 예를 들어 다음 명령은 config/wsgi.py에 있는 application 객체를 Gunicorn으로 실행한다.

gunicorn config.wsgi:application --bind 0.0.0.0:8000

Uvicorn은 ASGI 방식으로 애플리케이션을 실행하는 서버다. ASGI는 비동기 요청 처리, WebSocket, 실시간 알림 같은 기능을 다룰 때 사용된다. Django는 asgi.py도 함께 제공하므로 필요하다면 다음처럼 실행할 수 있다.1

uvicorn config.asgi:application --host 0.0.0.0 --port 8000

처음 배포하는 일반적인 Django 프로젝트라면 WSGI와 Gunicorn으로 시작해도 괜찮다. 실시간 채팅이나 WebSocket처럼 ASGI가 필요한 기능이 생겼을 때 Uvicorn 같은 ASGI 서버를 검토하면 된다. 중요한 것은 runserver를 그대로 외부에 열어 두지 않는 것이다.

운영 설정 분리하기

개발 환경과 운영 환경은 설정이 달라야 한다. 로컬에서는 디버그 화면이 필요하고 SQLite를 쓸 수도 있다. 반면 운영에서는 비밀 값을 숨겨야 하고, 실제 도메인만 허용해야 하며, HTTPS와 로그 설정도 필요하다.

대표적으로 다음 값들은 운영에서 반드시 점검해야 한다.

# settings.py
import os

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "false").lower() == "true"

ALLOWED_HOSTS = [
    host.strip()
    for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",")
    if host.strip()
]

SECRET_KEY는 코드에 직접 적지 않는 것이 좋다. Git 저장소에 올라가면 안 되는 비밀 값이기 때문이다. 데이터베이스 비밀번호, 외부 API 키, 이메일 비밀번호도 마찬가지다. 이런 값은 환경 변수나 배포 플랫폼의 시크릿 설정에 둔다.2

운영 환경에서는 보통 다음처럼 설정한다.

DJANGO_DEBUG=false
DJANGO_ALLOWED_HOSTS=example.com,www.example.com
DJANGO_SECRET_KEY=운영용_긴_랜덤_문자열

ALLOWED_HOSTS는 이 Django 애플리케이션이 응답할 도메인 목록이다. 운영에서 ['*']처럼 모든 호스트를 허용하면 원치 않는 도메인으로 들어온 요청에도 응답할 수 있다. 실제 도메인과 필요한 서버 IP를 명확히 적는 편이 안전하다. 가능하다면 Nginx 같은 앞단 웹 서버에서도 허용하지 않는 도메인의 요청은 Django까지 넘기지 않도록 막아 둔다.

HTTPS를 적용했다면 보안 관련 설정도 함께 확인한다.

SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True

이 설정들은 HTTP 요청을 HTTPS로 돌리거나, 세션 쿠키와 CSRF 쿠키가 HTTPS 연결에서만 전송되도록 돕는다. 단, 로컬 개발 환경에서 그대로 켜면 접속이 불편할 수 있으므로 개발 설정과 운영 설정을 분리하는 방식이 좋다.

또 하나 주의할 점이 있다. Nginx나 로드 밸런서가 HTTPS를 처리하고 Django에는 내부 HTTP로 요청을 넘기는 구조라면, Django가 원래 요청이 HTTPS였다는 사실을 알 수 있도록 프록시 헤더 설정을 맞춰야 한다. 이런 환경에서는 다음 설정이 함께 필요할 수 있다.

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

다만 이 값은 앞단 프록시가 X-Forwarded-Proto 헤더를 정확히 설정하고, 외부 사용자가 이 헤더를 임의로 조작할 수 없게 막혀 있을 때 사용해야 한다. 보안 설정은 한 줄을 켜는 것으로 끝나는 것이 아니라, 웹 서버와 Django 설정이 서로 맞아야 한다.

Django에는 운영 배포 전에 설정을 점검해 주는 명령도 있다.

python manage.py check --deploy

이 명령이 모든 문제를 대신 해결해 주지는 않지만, DEBUG, 보안 쿠키, HTTPS, 허용 호스트처럼 놓치기 쉬운 항목을 확인하는 출발점으로 유용하다.2

collectstatic으로 정적 파일 모으기

개발 중에는 각 앱의 static/ 폴더나 프로젝트 공통 정적 파일 폴더에 CSS와 JavaScript를 나누어 둔다. Django 개발 서버는 DEBUG=True일 때 이런 파일을 찾아서 제공해 준다.

운영에서는 방식이 다르다. 정적 파일을 한곳에 모은 뒤, 웹 서버나 CDN, WhiteNoise 같은 도구가 제공하도록 구성한다. 이때 사용하는 명령이 collectstatic이다.3

먼저 설정을 확인한다.

# settings.py
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

STATIC_ROOT는 운영 배포용 정적 파일이 모이는 목적지다. 개발자가 직접 CSS를 작성하는 원본 폴더가 아니라, 배포 전에 수집된 결과물이 저장되는 위치라고 이해하면 된다. 따라서 앱 안의 static/ 폴더와 STATIC_ROOT를 같은 위치로 잡지 않는 편이 좋다.

명령은 다음과 같다.

python manage.py collectstatic

자동 배포나 서버 배포 스크립트에서는 중간 확인 질문을 피하기 위해 다음처럼 실행하는 경우가 많다.

python manage.py collectstatic --noinput

이 명령을 실행하면 Django는 여러 위치에 흩어진 정적 파일을 찾아 STATIC_ROOT로 복사한다. 이후 Nginx 같은 웹 서버가 /static/ 요청을 이 폴더에서 직접 응답하게 구성한다. 이렇게 하면 이미지나 CSS 요청까지 Django 애플리케이션 서버가 모두 처리하지 않아도 된다.

주의할 점은 미디어 파일과 정적 파일을 섞지 않는 것이다. 사용자가 업로드한 프로필 이미지나 첨부 파일은 MEDIA_ROOT에 저장하고, CSS나 개발자가 준비한 아이콘은 static으로 관리한다. 배포 구조에서도 둘은 분리해서 생각해야 한다.

특히 미디어 파일은 사용자가 올린 파일이므로 신뢰하면 안 된다. 웹 서버가 업로드된 파일을 코드처럼 실행하지 않도록 설정해야 하고, 데이터베이스와 별도로 업로드 파일 백업도 준비해야 한다. 게시판 첨부 파일이나 프로필 이미지를 잃어버리면 데이터베이스만 복구해도 서비스가 완전히 돌아오지 않는다.

SQLite에서 PostgreSQL로 전환하기

Django를 처음 배울 때는 SQLite가 편하다. 별도 서버 설치 없이 파일 하나로 동작하기 때문이다. 하지만 실제 운영 서비스에서는 PostgreSQL 같은 서버형 데이터베이스를 많이 사용한다. 동시 접속, 백업, 권한 관리, 운영 도구 측면에서 더 적합하기 때문이다.

PostgreSQL을 사용하려면 먼저 드라이버를 설치한다.

pip install "psycopg[binary]"

그리고 DATABASES 설정을 운영 환경에 맞게 바꾼다.

# settings.py
DATABASES = {
    "default": {
        "ENGINE": "django.db.backends.postgresql",
        "NAME": os.environ["POSTGRES_DB"],
        "USER": os.environ["POSTGRES_USER"],
        "PASSWORD": os.environ["POSTGRES_PASSWORD"],
        "HOST": os.environ.get("POSTGRES_HOST", "localhost"),
        "PORT": os.environ.get("POSTGRES_PORT", "5432"),
    }
}

새 데이터베이스를 준비한 뒤에는 마이그레이션을 적용한다.

python manage.py migrate
python manage.py createsuperuser

이미 SQLite에 중요한 데이터가 있다면 단순히 설정만 바꾸면 안 된다. 기존 데이터를 옮겨야 한다. 작은 프로젝트에서는 다음 흐름을 사용할 수 있다.

# SQLite 설정 상태에서 데이터 내보내기
python manage.py dumpdata --exclude auth.permission --exclude contenttypes > data.json

# PostgreSQL 설정으로 변경 후 마이그레이션
python manage.py migrate

# 데이터 가져오기
python manage.py loaddata data.json

다만 실제 서비스 데이터 이전은 더 조심해야 한다. 문자 인코딩, 기본키 충돌, 파일 업로드 경로, 사용자 비밀번호, 외래키 관계를 확인해야 한다. 데이터 양이 많거나 이미 사용자가 있는 서비스라면 점검용 서버에서 먼저 이전을 연습하고, 서비스 중단 시간과 되돌리는 방법까지 정해 두어야 한다. 운영 데이터 이전 전에는 반드시 백업을 먼저 만든다.

배포 순서의 기본 흐름

작은 Django 프로젝트를 배포할 때의 기본 순서는 다음과 같다.

1. 서버 준비
2. 코드 가져오기
3. 가상환경 생성 및 패키지 설치
4. 환경 변수 설정
5. 데이터베이스 연결 확인
6. 마이그레이션 실행
7. collectstatic 실행
8. Gunicorn 또는 Uvicorn 실행 설정
9. Nginx 등 웹 서버 연결
10. 도메인과 HTTPS 설정
11. 관리자 계정과 주요 화면 확인
12. 로그와 백업 상태 확인

예를 들어 서버에서 다음 명령들이 실행될 수 있다.

git pull
pip install -r requirements.txt
python manage.py check --deploy
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart gunicorn

여기서 중요한 점은 배포가 반복 가능한 절차여야 한다는 것이다. 기억에 의존해 수동으로 처리하면 언젠가 빠뜨리는 단계가 생긴다. 최소한 체크리스트를 문서로 남기고, 가능하면 배포 스크립트나 CI/CD 도구로 자동화한다.

또한 배포 순서도 중요하다. 패키지를 설치하지 않은 상태에서 서버 프로세스만 재시작하면 새 코드가 필요한 라이브러리를 찾지 못할 수 있다. 마이그레이션이 필요한 코드인데 데이터베이스를 먼저 바꾸지 않으면 화면에서 오류가 날 수 있다. 반대로 큰 마이그레이션을 아무 준비 없이 실행하면 서비스가 오래 멈출 수도 있다. 처음에는 작은 프로젝트라도 "어떤 순서로 실행하고, 실패하면 어디서 멈추고, 어떻게 되돌릴지"를 적어 두는 습관이 좋다.

운영은 배포 다음 날부터 시작된다

서비스는 배포한 순간 끝나는 것이 아니라 그때부터 운영이 시작된다. 운영에서 가장 먼저 필요한 것은 로그다. 로그가 없으면 사용자가 "안 돼요"라고 말했을 때 서버에서 무슨 일이 있었는지 알 수 없다.

간단한 Django 로그 설정은 다음처럼 시작할 수 있다.

LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
        },
    },
    "loggers": {
        "django": {
            "handlers": ["console"],
            "level": "INFO",
        },
    },
}

서버 환경에서는 콘솔 로그를 systemd, Docker, 클라우드 로그 수집기가 가져가도록 구성하는 경우가 많다. 파일 로그를 쓴다면 파일 크기 증가와 순환 정책도 함께 고려해야 한다.

에러 추적 도구도 유용하다. 예외가 발생했을 때 어떤 URL에서, 어떤 사용자 요청 중에, 어떤 스택 트레이스로 실패했는지 모아 주는 도구를 붙이면 장애 대응이 빨라진다. 처음에는 Django 로그와 서버 로그만으로 시작해도 되지만, 실제 사용자가 생기면 Sentry 같은 에러 추적 도구를 검토하는 것이 좋다.

백업도 운영의 핵심이다. 코드는 Git에 남지만 데이터베이스와 업로드 파일은 따로 백업해야 한다. 최소한 다음 항목을 정해 둔다.

  • 데이터베이스 백업 주기

  • 백업 파일 보관 기간

  • 백업 저장 위치

  • 복구 테스트 방법

  • 업로드 파일 백업 방법

백업은 "만들었다"보다 "복구해 봤다"가 중요하다. 복구 절차를 검증하지 않은 백업은 실제 장애 상황에서 믿기 어렵다. 새 서버나 임시 데이터베이스에 백업을 복원해 보고, 관리자 로그인과 주요 화면이 정상적으로 열리는지 확인해야 한다.

배포 전 최종 체크리스트

마지막으로 운영 배포 전에 다음 항목을 확인하자.

  • DEBUG=False인가?

  • python manage.py check --deploy를 운영 설정 기준으로 실행해 보았는가?

  • SECRET_KEY, 데이터베이스 비밀번호, API 키가 코드에 직접 들어 있지 않은가?

  • ALLOWED_HOSTS에 실제 도메인이 설정되어 있는가?

  • 운영 데이터베이스에 migrate를 실행했는가?

  • collectstatic을 실행했고, 웹 서버가 정적 파일 경로를 제공하는가?

  • 사용자 업로드 파일 저장 위치와 백업 정책이 있는가?

  • Gunicorn 또는 Uvicorn 프로세스가 자동 재시작되도록 설정되어 있는가?

  • Nginx 같은 웹 서버가 애플리케이션 서버로 요청을 올바르게 넘기는가?

  • HTTPS가 적용되어 있고, 보안 쿠키 설정이 운영 환경에서 켜져 있는가?

  • 관리자 페이지 주소, 관리자 계정, 권한을 점검했는가?

  • 로그를 확인할 수 있는가?

  • 에러 발생 시 알림을 받을 방법이 있는가?

  • 데이터베이스 백업과 복구 절차를 확인했는가?

Django를 배우는 동안 우리는 URL, View, Template, Model, Form, 인증, API, 테스트를 차례로 다루었다. 배포는 이 모든 조각을 실제 사용자 앞에 놓는 단계다. 로컬에서 동작하는 코드는 가능성을 보여 주지만, 운영 환경에서 안정적으로 돌아가는 코드는 서비스가 된다.

처음부터 완벽한 운영 구조를 만들 필요는 없다. 다만 DEBUG=False, 환경 변수, 운영 데이터베이스, 정적 파일 수집, 로그, 백업은 작은 프로젝트에서도 반드시 경험해 보자. 이 과정을 거치면 Django 프로젝트는 연습용 폴더를 넘어 실제로 관리할 수 있는 웹 애플리케이션이 된다.

참고

1Django documentation: How to deploy Django

2Django documentation: Deployment checklist

3Django documentation: How to manage static files

책 목록

Django 활용법: 웹 개발 기본기부터 배포와 운영까지

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기