본문 바로가기
Django

django 배포 시 runserver를 사용하지 않는 이유

by Haeine 2024. 5. 14.

보통 django로 웹 애플리케이션 서버(WAS)를 구현할 때 아래와 같은 명령어를 사용하여 서버를 실행 시킨다.

// 개발 서버 실행
python manage.py runserver

 

하지만, 사용자들에게 실제로 서비스를 제공 할 서버를 배포할 경우 위 명령어를 통해 django 서버를 실행시키지 않는다. 왜 그런걸까? 왜 아래와 같은 복잡한 짓을 하는걸까?

// 1. nginx 세팅
// 2. gunicorn을 통해 실행되는 django 서버

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

//--bind 옵션으로 서버가 호스팅될 포트 설정, 마지막 인자로 gunicorn에 django의 wsgi 객체를 넘김
// 3. nginx 실행

 

이유는 간단하다. runserver가 능력이 없다. 해당 명령어는 개발 시 django 서버를 간단히 실행시켜 보고 필요한 부분을 체크하고 테스트하기 위해서 제공 되는 대충 만들어진 명령이다. 실제 서비스에서 runserver를 통해 서버가 요청을 처리하게 되면 발생하는 문제점이 많다. 

 

runserver 명령어의 한계

1. 스레드 및 프로세스 관리의 한계
2. 접근제어 불가
3. HTTPS 지원 부족
4. 취약한 디버그 도구
5. 업데이트 및 패치 관리 부재
6. 로깅 및 모니터링 부재

 

위에서 제시한 각 한계 및 문제점들을 하나씩 자세하게 살펴보자

 

1. 스레드 및 프로세스 관리의 한계

싱글 스레드의 병목

  기본적으로 HTTP 요청을 처리하는 실제 웹 서버는 쓰레드 풀(multi thread)을 가지고 있다. 요청이 오면 쓰레드 하나를 요청에 할당한다. 때문에 스레드 풀에 있는 가용한 스레드가 하나 감소한다. 만약 스레드풀에 남아있는 스레드가 없다면 가용한 스레드가 생길때 까지 요청은 서버에 의해 처리되지 못하고 대기하게 된다. 이때 요청을 보낸 클라이언트도 같이 대기하게되는 것이다. 서버가 요청에 대한 응답을 클라이언트에게 전송하면 할당된 스레드가 해제되면서 스레드풀에 가용한 스레드가 하나 증가하게 된다.

  실제로 구동되는 서버와는 다르게 runserver 명령어로 실행된 서버는 스레드 풀의 개념이 없다. 단일 스레드(Single thread)로 구동되는 서버로서 한번에 한 개의 HTTP 요청만을 처리할 수 있는 것이다.

예를 들어, runserver로 구동된 서버에 100개의 요청이 온다고 가정하면 요청이 도착한 순서대로 차례 차례 처리될것이다. 100번째 온 요청은 앞의 99개 요청이 처리되기를 기다려야 된다. HTTP 프로토콜의 특성상 서버가 요청을 최초 확인 및 승인하는데 많은 시간과 공간의 자원이 소요된다. 99번의 요청 확인 및 승인을 대기해야 하는 100번째 요청의 입장에서는 처리되는데 상당한 시간이 걸리게 된다. 이같은 이유로 컴퓨팅 파워를 최대한 활용하여 고객에게 빠르게 응답을 제공하는것이 불가능 하다.

 

위에서 설명한 내용들이 하나의 프로세스 위에서 벌어지는 일들이다. 하나의 cpu위에서 동시에 여러 개의 프로세스가 실행될 수 있다.(cpu는 하나이지만 내부에 core는 여러개다. 더군다나 요즘 시대의 가상화 기술은 상상을 초월한다.) 

 

  정리하면 멀티 스레드와 멀티 프로세스를 지원하지 못하는 runserver 명령어는 컴퓨팅 파워를 최대로 활용할 수 없다. 따라서 다수의 클라이언트에게 신속한 응답을 제공하는데 적합하지 못하다. 여기까지만 읽어도 실제 서버를 배포 및 호스팅하는 경우 왜 runserver 명령어를 사용하지 않는지에 대한 의문은 사라질 것이다.

 

2. 접근 제어 불가

무릇 서버란 네트워크로 부터 전달받은 요청을 원하는대로 제어 할 수 있어야 한다. 특정 대역의 IP의 요청만을 수락 하거나 그 반대의 시나리오도 처리할 수 있어야한다. 그리고 의도치 않은 네트워크 인터페이스에 대한 접근을 막을 수 있어야한다. 하지만 runserver를 통해 호스팅된 서버는 모든 네트워크 요청을 받아들인다.

django settings에서 ALLOWED_HOSTS를 설정할 수 있다. 이는 서버가 받은 HTTP 요청 헤드에 있는 Host 키의 값을 확인하여 해당 값이 ALLOWED_HOSTS 리스트 내부에 있으면 요청을 받아들이는 기능을 한다. 이는 IP 및 네트워크 인터페이스 기반의 요청을 제한하는것과는 무관하다.

 

3. HTTPS 지원 부족

runserver는 HTTPS 요청을 처리할 수 없다. HTTP 프로토콜을 통한 네트워크 통신은 결국 클라이언트와 서버 사이에 데이터 패킷을 무선 통신으로 주고 받는것이다. 만약 누군가가 전송중인 패킷을 훔쳐서 해당 패킷에 담긴 데이터를 확인할 수 있다면 이는 심각한 문제가 된다. 이를 스니핑 공격이라고 한다. 카카오톡으로 누군가에게 한 말을 스니핑 공격자가 훔쳐서  확인할 수 있다고 생각해보라. 과연 누가 카카오톡을 쓰겠는가? 이 같은 스니핑 공격을 막기위해서 HTTPS 프로토콜을 사용한다. 이를 통해 요청 패킷을 암호화하고  응답을 받은 주체는 해당 패킷을 복호화 하게됩니다. 결국 전송 중인 패킷은 암호화 되어있는 상태가 되며 스니핑 공격에서 보호받게 됩니다.

 

4. 취약한 디버그 도구

개발 서버(runserver)는 개발의 편의를 위해 다양한 디버그 도구와 기능을 제공한다. 이러한 기능들이 개발과 테스팅을 할 경우에는 도움을 주지만 실제 서비스 단계에서는 공격자가 시스템을 분석하고 공격 경로를 찾는데 악용될 수 있다. 

예를 들어 django의 디버그 페이지, 디버그 툴바 등은 현재 서버의 상태 및 여러가지 설정 정보를 응답에 포함시켜서 보여준다. 개발과정에서는 유용한 정보가 되지만 실제 서비스에서 공격자에게 노출된다면 심각한 문제가 된다. 그리고 개발 서버에서는 정적파일을 직접적으로 서빙하게된다. 때문에 공격자는 정적 파일의 디렉터리 구조를 파악할 수 있으며, 디렉터리 트래버설 공격등을 시도할 수 있다. 마지막으로 일부 보안 관련 미들웨어가 비활성화될 수 있다. 응답의 특정 헤더 설정을 생략할 수 있다.

 

5. 업데이트 및 패치 관리 부재

서비스가 배포되어 운영중에 있습니다. 이때 특정 문제가 발견되었다고 가정해봅시다. 만약 문제가 되는 특정 소프트웨어가 빠르게 수정되어 업데이트 될것이며 해당 수정 사항이 패치를 통해 시스템에 적용될 것이다. 하지만 이런경우 runserver 로직에 해당 내용이 적용될리가 없다. 왜냐하면 해당 명령으로 실행된 서버에서는 이 문제를 고려할 필요가 없기 때문이다. 

 

6. 로깅 및 모니터링 부족

runserver는 개발용 서버로, 기본적인 콘솔 출력 외에 정교한 로깅 기능이 부족하다. 이는 운영 환경에서 발생하는 다양항 상황을 기록하고 분석하는 데 한계가 있다. 그리고 성능, 에러유, 트래픽 등을 실시간으로 모니터링하는 기능을 제공하지 않는다.

실제 서버 환경에서 gunicorn을 사용하면 특정 로그를 지정된 경로에 기록하도록 아래와 같이설정할 수 있다.
gunicorn --access-logfile /path/to/access.log --error-logfile /path/to/error.log myproject.wsgi:application
Prometheus와 Grafana를 사용하여 Django 애플리케이션을 모니터링 할 수 있다.

 

# django settings.py
MIDDLEWARE = [
    ...
    'django_prometheus.middleware.PrometheusBeforeMiddleware',
    'django_prometheus.middleware.PrometheusAfterMiddleware',
]

INSTALLED_APPS = [
    ...
    'django_prometheus',
]

 

'Django' 카테고리의 다른 글

Django test 수 많아졌을때 테스트 도중에 멈추는 경우  (1) 2023.11.01