Dynamically import django settings for multiple environment such as local, dev, beta, production
장고 개발을 하다보면 각 환경을 구분하고, 이에 따라 변경되야 하는 부분들이 존재한다. 예를들면 임포트 되어야하는 미들웨어 혹은 장고 어플리케이션들, 데이터베이스 연결정보 등이 달라진다. 이럴때 각 환경에 따라 코드를 수정하는 작업은 해서는 안되는 작업이며, 언젠가 한번쯤 실수를 할것이고, 비명을 지르게될것이다. “꺄야양야야야야야 ㅅㅂ”
“어떻게 환경을 인식을 할 것인가?”
어플리케이션이 작동하는 환경을 나누어 보자. 아래처럼 환경을 구분하는 것이 정답은 아니며, 개발프로세스나 회사 업무에 따라 다를것이다.
local
: 개발자 개인PCdevelopment
: 사내 네트워크 망에서 공유되는 서버를 통해 작동하는 어플리케이션 환경staing
: 외부 네트워크 망에서 작동하는 서버에서 제공되는 어플리케이션 환경. 릴리즈 전 1차 외부 테스트 환경.beta
:staging
과 동일하지만production
과 최대한 일치되는 환경으로production
릴리즈 이전에 최종테스트가 일어나는 환경.production
: 어플리케이션이 실제 서비스 되는 환경.
위와 같이 다양한 환경에서 동일한 코드베이스의 장고 어플리케이션이 동작하게 되며, 각 환경에서 로드되어야하는 모듈이나 설정정보들이 다를것이다. 이렇게 다른점을 적용하기 위해서는 현재 어플리케이션이 작동하는 환경이 production
인지 local
인지 인식을 해야한다.
현재 작동하는 환경을 인식하기 위한 방법은 다양하지만, 필자가 가장 선호하는 방법은 환경변수 셋팅이다.
export SELO_ENV=local
TIP) 개인 개발PC .bash_profile
와 같은 쉘 부트스트래핑 파일에 구문을 추가해 놓으면 편하다.
import os
CURRENT_APP_ENV = os.getenv('SELO_ENV')
“각 환경별 설정 모듈화 및 디렉토리 구조잡기”
유연하며, 명시적인 구조를 만들어보자. “Here we go. Let’s get it. Put your hands up!!!”
시작이 반: 장고 프로젝트 초기 구조
##### 장고의 초기 디렉토리 구조 #####
$ tree
.
|____bar
| |______init__.py
| |____settings.py
| |____urls.py
| |____wsgi.py
|____manage.py
버전 1.11
기준 장고의 프로젝트를 처음 생성하면 위와 같은 디렉토리 구조로 프로젝트가 생성된다. 아주 심플하다. 구조화 없이 프로젝트가 진행되다보면 알아보기 무서운 “괴물“이 될수있다.
결과물: 아주 이쁘게 모듈화된 최종 구조
먼저 우리가 갖게 될 최종 구조를 훓어보고 큰 그림을 그려보자. 최종구조는 다음과 같다.
$ tree
.
|____meta
| |______init__.py
| |____unittest.py
| |____ckeditor.py
| |____ ...
|______init__.py
|____default.py
|____development.py
|____staging.py
|____beta.py
|____prod.py
# 한눈에 보기 편하도록 출력결과물의 순서를 조정했다.
“설명이 필요없는 한눈에 이해되는 명확한 구조가 아닌가!! 아름답다!! 나만 그런가…?!!”
위 최종구조에서 각 모듈(파일)의 역활을 정리해보자.
settings/default.py
: 환경에 상관없는 공통적인 설정을 작성한다.- 기본 앱. e.g)
'django.contrib.admin'
,'django.contrib.auth'
- 공통 미들웨어
TIMEZONE
등 서비스에서 통용되는 상수
- 기본 앱. e.g)
settings/<env_name>.py
:prod.py
와 같이 각 환경에 의존적인 설정을 작성한다.<env_name>.py
.- 개발환경에서의 디버깅 툴. e.g) Django Debug Toolbar
- 데이터베이스 커넥션 정보
- 환경별 미들웨어. e.g) 베타유저 인증 미들웨어
TOKEN
과 같은 환경별 상수- 모니터링 툴. e.g)
NewRelic
settings/meta/<*.py>
:meta
폴더 하위에는 특정 라이브러리의 설정이나 특별한 실행 환경에서 실행되는 설정에 대한 부분을 작성한다.$ python manage.py test
처럼 테스트 실행과 같은 특별한 환경에서 필요한 설정 혹은 라이브러리monkey patch
와 같은 작업을 진행한다.- 많은 라인(15줄 이상)의 설정이 필요한 라이브러리(
django-ckeditor
등).settings/meta/ckeditor.py
default.py
가 읽기 힘들정도로 길어질 때 모듈로 분리해야하는 경우
Import: 즐거운 코딩하는 시간
manage.py
...
# setdefault 함수의 2번째 인자 부분을 알맞게 변경해주자
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "foo.settings.default")
...
foo/settings/default.py
환경변수를 읽어, 셋팅을 동적으로 로드하자.
import os
try:
SELO_ENV = os.environ['SELO_ENV']
except KeyError:
print "SELO_ENV must be set to OS Variable."
exit(1)
if SELO_ENV == 'development':
from foo.settings.development import *
elif SELO_ENV == 'staging':
from foo.settings.staging import *
elif SELO_ENV == 'beta':
from foo.settings.beta import *
elif SELO_ENV == 'prod':
from foo.settings.beta import *
else:
print "Given Value is %s. It's invalid environment name." % SELO_ENV
exit(1)
코드가 드럽다. 안이쁘다. 구려보인다. 이쁘게 바꿔보자. 코드는 역시 외모지상주의!!
try:
SELO_ENV = os.environ['SELO_ENV']
except KeyError:
print "SELO_ENV must be set to OS Variable."
exit(1)
def import_module_asterisk(module_name):
module = __import__(module_name, globals=globals(), fromlist=['*'])
for v in dir(module):
if v in {'__name__', '__builtins__', '__doc__', '__file__', '__package__'}:
continue
globals()[v] = getattr(module, v)
try:
import_module_asterisk(SELO_ENV)
except ImportError:
print "Given Value is %s. It's invalid environment name." % SELO_ENV
exit(1)
else:
print("Successfully Loaded %s settings." % SELO_ENV)
위 코드를 간단히 설명하겠다. __import__
는 빌트인 함수이며, 동적으로 모듈을 임포트할 수 있게해준다. 즉, 오직 실행환경에서만 임포트 대상을 알 수 있는 경우 사용한다. 코드로 풀어서 설명하자면, __import__('development', fromlist=['*'])
는 from development import *
와 동등한 기능을 수행한다. 결론적으로 development.py
모듈의 네임스페이스를 통째로 임포트한다. 설명은 파이썬2 기준이며, 파이썬3에서는 사용해보지 않았다.
정리
필자가 참여한 장고 프로젝트에는 신규 프로젝트도 있었고, 몇 년동안 개발 운영 되고 있는 서비스도 있었다. 프로젝트에서 이러한 환경에 따른 설정은 매우 치명적인 부분임에도 불구하고 기능개발에 집중하다보면 간과되는 경우가 많다. 문제가 발생하지 않으면 다행이지만… 최근에 오픈한 모바일게임 듀랑고 클라이언트에서 서버 로그가 출력되는 안타까운 문제가 발생했고, 논란의 중심에 있다. 듀랑고 사태의 정확한 맥락은 알수 없지만 기본을 간과하는 순간 문제가 발생하게 되는것 같다.
같은 맥락으로 이어서 말하자면 코드를 공유하는 개발자들 사이에서 설정을 변경 후 푸쉬하는 경우로 문제가 발생했던 적이 몇 번 있었다. 특히, 디비커넥션 정보, 디버거 임포트 등과 같이 개발중인 기능에 의존적인 설정들이 예가 될 것이다. 위 방식과 구조를 취한다면 이러한 문제들을 해결할 수 있을 것이다.
PS. 언젠가 정리해야지 막연히 생각하던걸 새로운 회사에서 또 한번(ㅠ?) 장고프로젝트를 진행하게 됬고, 이 참에 정리했다… 개운하구만!! 그럼 이만 뿅!!
오타나 잘못된 부분에 대한 언급은 언제나 감사드립니다.