(미완료)[Pro Django] Tutorial_1: django-split-setting

Saemi An·2025년 5월 6일

해당 포스팅은 유투버 Bucky의 『Pro Django Tutorial』 1강의 내용을 바탕으로 작성 했습니다.
동영상 바로가기
동영상 속 깃헙 바로가기


(여러번 봐도 이해가 되지 않아 넘어갔다가
	디버깅 할때 어딜 고쳐야 하는지 도저히 모르겠어서
				다시 심기일전🔥)

⚠️ VSCode EXPLORER 패널 커스텀
해당 튜토리얼에서는 여러 설정파일을 복잡한 구조로 분리하는 작업을 진행한다.
하도 폴더 구조가 눈에 안보여서 커스텀을 진행했다.

  • Settings > search: workbench.tree.indent를 찾아 기본값 8에서 16으로변경
  • Material Icon Theme 확장팩 설치

그런데 Docker 파일과 docker-compose 파일의 고래 아이콘 색깔이 둘 다 파란색으로 나온다.
docker-compose 파일의 고래 아이콘을 핑크색으로 커스텀하기 위해 user의 settings.json에서 다음과 같이 추가해 줬다.
관련 공식 문서를 참고하면 더 다양한 커스텀이 가능하다.

    // Material Icon Theme 커스텀
    "material-icon-theme.files.customClones": [
        {
            "name": "docker-compose",
            "base": "docker",
            "color": "pink-300",
            "fileNames": ["docker-compose.yml", "docker-compose.dev.yml"]
        }
    ]
}

훨씬 편하다.. 너무 좋다..
괜히 확장팩 많이 설치하면 IDE만 무거워 진다고 생각했었던 지난날의 나 반성해..




(지난 Tutorial_1에서 django-split-settings와 PyYAML을 설치 했었다.
그에 이어지는 내용이다.)

./core/project/settings 디렉토리를 생성한다.
이후 ./core/project/settings/templates 디렉토리를 생성한다.
이후 ./core/project/settings/templates/settings.dev.py 파일을 생성한다.

기존의 settings.py 파일의 이름을 base.py로 변경한 후 ./core/project/로 이동한다.

🎨 settings.dev.py

🫟 필요성

settings.dev.py는 팀원 각자가 로컬 머신에서 개발서버를 돌릴 때 사용할 Django setting 파일이다.
예를들어 각자가 원하는 로깅 방식을 적용할 때나 특정 테스트를 위해 DB를 변경하는 경우 사용한다.

template/settings.dev.py는 이를 위한 템플렛이다.

추후 README에 서술되겠지만 그 구체적인 사용 방법으로는
팀원이 프로젝트를 git clone 한 뒤,
(.gitignore 목록에 있어서 git이 트래킹 하지 않는) ./local/ 위치에 직접 settings.dev.py 파일을 만들고,
각자가 개발서버에서 사용할 설정을 명시한다.

이때 모든 개발/운영 환경에 적용되어야할 글로벌 세팅은 base.py에 있으며, 해당 파일은 팀원들이 수정하지 않는다.

base.py에서 DEBUG=FlaseSECRET_KEY=NotImplemented로 변경해준 뒤
settings.dev.py에서 DEBUG=TRUESECRET_KEY='(내 장고 시크릿키)'를 적어준다.

위의 설정으로
모든 개발 팀원들은 프로젝트를 돌릴 때 팀 내부적으로 공유된 시크릿키를 각자의 설정 파일에서 적용하고
운영서버에서는 환경변수로 시크릿키를 넘겨줌으로써
깃헙과 같은 외부 노출을 피할 수 있다.

🫟 실습

mkdir -p local
cp core/project/settings/templates/settings.dev.py ./local/settings.dev.py
위 명령어를 사용하여 프로젝트 최상위 디렉토리에 local 파일을 생성한 뒤 template/settings.dev.py의 내용을 카피해 준다.

이후 local/settings.dev.py에 나한테 필요한 설정을 추가한 뒤 개발서버를 실행하면 된다.

👾 리눅스 명령어 옵션 | -p

-p 옵션은 안전하게 디렉토리를 생성할 때 거의 필수적으로 쓰이는 옵션이다. 그 기능은 다음과 같다;

첫째로, 상위 디렉토리가 없으면 자동으로 만들어 준다.

mkdir parent/child 실행시 parent가 없는 상태라면 에러가 난다. 하지만 -p 옵션을 달면 parent 디렉토리 생성 후 child 디렉토리를 생성한다.

두번째로, 이미 존재하는 디렉토리를 생성하도록 명령해도 에러를 내지 않는다.

mkdir -p local 실행시 local 폴더가 이미 존재해도 그냥 넘어간다.

🎨 __init__.py

./core/project/settings/__init__.py의 내용을 다음과 같이 적어준다;

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent

(해당 파일은 맨 아래에서 다시 작성하게 됨으로 마지막에 다시 설명한다. )

🎨 core/general/utils

./core/general/utils 디렉토리를 생성해 준다.

🫟 collections_utils.py

./core/general/utils/collections_utils.py 파일을 생성한 뒤 다음과 같이 작성해 준다.

def deep_update(base_dict, updated_with):

    # 새로 들어온 값 순회
    for key, value in updated_with.items():

        # 만일 value가 dict 타입 이라면
        if isinstance(value, dict):
            base_dict_value = base_dict.get(key)  # base_dict에서 같은 key를 갖는 애를 찾아 그 value를 변수에 담음

            # 만일 원본 value 또한 dict 타입 이라면 재귀함수 호출
            if isinstance(base_dict_value, dict):
                deep_update(base_dict_value, value)
            # 그렇지 않으면 값 업데이트
            else:
                base_dict[key] = value

        # 만일 value가 dict 타입이 아니라면 새로운 value로 업데이트
        else:
            base_dict[key] = value

    return base_dict

위 함수는 base_dict와 updated_with 이라는 두개의 인자를 받아 업데이트된 base_dict를 반환해주는 기능을 한다.

실제로 Tutorial_4에서 이 함수가 호출 됨으로 해당 예시를 통해 설명을 진행한다.

base.py에서 DATABASE 세팅이 base_dict이고

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'pro_django',
        'USER': 'pro_django',
        'PASSWORD': 'pro_django',
        'HOST': '127.0.0.1',
        'PORT': '5432',
    }
}

docker-compose.yml에서 장고앱의 환경변수가 updated_with이다.

{"default":{"HOST":"db"}}

deep_update() 함수에서는 두 번째 인자인 updated_with을 순회하며
updated_with의 key가 base_dict와 일치하면 base_dict[key]를 updated_with의 value로 바꿔준다.

만일 nested_dict 형식이라면 재귀호출을 통해 끝까지 타고 들어가서 값을 바꿔준다.

결국 base.py의 DATABASE 세팅을 유지하면서 docker-compose에서 주어진 환경변수값만 업데이트 해주는 것이다.

그 결과는 다음과 같다;

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'pro_django',
        'USER': 'pro_django',
        'PASSWORD': 'pro_django',
        'HOST': 'db',
        'PORT': '5432',
    }
}

🫟 misc.py

./core/general/utils/misc.py 파일을 생성한다.
해당 파일에서는 yaml 파일에 적용될 함수를 다음과 같이 작성한다;

👾 PyYMAL

이전 Tutorial_1에서 django-split-setting과 함께 PyYMAL 패키지를 설치 했었다.

PyYMAL은 파이썬에서 YAML 파일을 다루는 경우 유용한 패키지다. 해당 모듈은 YAML 파일을 읽고 쓸 수 있는 기능을 제공할 뿐만 아니라 YAML 데이터를 파이썬 객체로 변환하는 기능을 제공한다.

도커 설정파일이나 .env와 같은 외부 설정파일에서는 모든 값이 문자열이다.

이를 장고가 알아듣게 하기 위해서는 파이썬이 취급하는 데이터 타입으로 변환해줄 필요가 있다.
(또 다른 예시로는 .env 파일에 DEBUG = TRUE로 작성된 설정이 'TRUE'로 읽히는 경우, boolean 값을 기대한 장고가 오류를 일으키는 경우 등이 있다.)

따라서 다음과 같이 misc.py에 작성해 준다;

import yaml

def yaml_coerce(value):
    if isinstance(value, str):
        return yaml.load(f'dummy: {value}', Loader=yaml.SafeLoader)['dummy']

    return value

yaml_coerce() 함수는 yaml.load를 이용해 YAML 파서로 문자열 타입의 value를 해석한다.
이때 YMAL은 딕셔너리나 리스트 구조만 파싱할 수 있기 때문에 'dummy'라는 키를 붙여 value를 딕셔너리처럼 만든 뒤, 그 값만 추출하여 반환하도록 한다.

🫟 settings.py

./core/general/utils/settings.py 파일을 생성한다.
해당 파일에서는 추후 여러 설정파일에서 사용할 prefix와 관련된 함수를 다음과 같이 작성한다;

import os

from .misc import yaml_coerce

# 환경변수를 순회하며 특정 접두사로 시작하는 항목들만 추려서 딕셔너리 형태로 가공하여 반환하는 함수

# ============================================================
# **************************** 예시 ***************************
# ============================================================

# 환경변수 목록
    # test = {
    #     "TS_IN_DOCKER": "1",
    #     "TS_DEBUG": "true",
    #     "OTHER_VAR": "ignored"
    # }

# prefix = 'TS_'

# 모든 환경변수 목록을 순회하며
# 특정 prefix로 시작하는 환경변수들만 모아 
# prefix를 뗀 부분을 키: 문자열 환경변수를 파이썬 문법에 맞게 변경한 값
# 을 갖는 딕셔너리 반환

# {'IN_DOCKER': 1, 'DEBUG': True}

# ============================================================
# ************************************************************
# ============================================================


def get_settings_from_environment(prefix):

    prefix_len = len(prefix)

    # tmp = {}
    # for key, value in test.items():   # 모든 환경변수 목록 탐색
    #     if key.startswith(prefix):   # prefix 필터링
    #         tmp[key[prefix_len:]] = yaml_coerce(value)   # prefix 떼고 뒷부분을 키로, 값은 yaml_coerce(value)로 하여 딕셔너리에 추가

    # return tmp

    return {key[prefix_len]: yaml_coerce(value) for key, value in os.environ.items() if key.startswith(prefix)}

⚠️ 테스트 실행중 ImportError 발생

from .misc import yaml_coerce

# 환경변수 중 특정 접두사로 시작하는 항목들만 추려서 딕셔너리 형태로 가공하여 반환하는 함수

test_env = {
    "TS_IN_DOCKER": "1",
    "TS_DEBUG": "true",
    "OTHER_VAR": "ignored"
}

prefix = 'TS_'

def get_settings_from_environment(prefix):

    prefix_len = len(prefix)

    tmp = {}

    for key, value in test.items():   # 모든 환경변수 목록 탐색

        if key.startswith(prefix):   # 접두사 필터링
            tmp[key[prefix_len:]] = yaml_coerce(value)

    return tmp

result = get_settings_from_environment(prefix)

print(result)

get_settings_from_environment(arg) 함수가 어떻게 동작하는지 알아보기 위해 해당 파일이 있는 위치에서 python settings.py 명령어를 실행하니 다음과 같은 에러가 났다;
Traceback (most recent call last):
File "/Users/saemi/src/Core/core/general/utils/settings.py", line 3, in \<\module>\
from .misc import yaml_coerce
ImportError: attempted relative import with no known parent package

⚠️ 에러 원인
python settings.py 명령어는 파이썬으로 하여금 settings.py를 독립적인 스크립트로 간주하도록 한다.

하지만 settings.py 소스코드 첫 줄에서 상대경로 import (from misc import yaml_coerce )를 해주고 있다.

이때 파이썬은 .(현재 패키지 위치)를 알 수가 없기 때문에 오류가 발생한다.


(똑같은 내용 다른 말로 설명해보기)

상대경로로 import한 소스코드를 포함하는 모듈은
패키지(부모디렉토리) 안에서 그 일부로 실행된다는 전제 하에
정상적으로 작동될 수 있다.

하지만 python settings.py는 settings.py의 소스코드를 직접 스크립트로 실행하도록 명령한다.

따라서 .과 같은 상대경로를 이해하지 못한다.

즉, Python에서 상대 경로(import .misc)를 사용할 때 해당 파일(settings.py)이 패키지(core.general.utils) 내부에서 모듈로 실행되지 않았기 때문에 오류가 발생한다.


결론
코드의 실행 방식에 따라 상대 경로로 import한 모듈을 파이썬이 못알아 들을수도 있다.
상대경로로 import하는 코드를 포함하는 파일을 실행시킬 때에는
파이썬으로 하여금 실행 파일이 패키지 내부 모듈임을 알게하자.

⚠️ 해결 방법
1) 실행 파일의 패키지 구조를 적어주기
python -m core.general.utils.settings

-m 옵션을 사용하여 터미널 내 현재 위치를 기준으로 실행파일의 위치를 함께 적어준다.
이 방식은 파이썬으로 하여금 전체 프로젝트 구조를 파악해서
settings.py가 패키지의 일부임을 이해시키기 때문에
from .misc 같은 상대 import를 잘 수행하도록 한다.

2) 절대경로로 import 해주기
settings.py 상단의 상대경로 import를 다음과 같이 절대경로로 바꿔준다;
from core.general.utils.misc import yaml_coerce

🫟 custom.py

+) Django Filter, Django Rest Framework (?)

./core/project/settings 위치에 custom.py를 생성한 후 다음과 같이 작성한다;

IN_DOCKER = False

도커에서는 환경변수나 .env 파일을 통해 IN_DOCKER = True가 자동으로 설정되지만
로컬 환경에서 개발자가 별도로 환경변수를 쓰지 않을 수 있으니 기본값으로 False 지정한다.

+) 도커파일 혹은 컴포즈 파일에서 IN_DOCKER = True 안 주던데 Dockerfile이나 docker-compose.yml 파일 작성하면 자동으로 이게 True가 되는건가? ❌

🫟 envvars.py

./core/project/settings 위치에 envvars.py를 생성한 후 다음과 같이 작성한다;

from core.general.utils.collections_utils import deep_update
from core.general.utils.settings import get_settings_from_environment

# export CORE_SETTINGS_IN_DOCKER = True
# 위와 같은 환경변수가 있을 때 이를 파이썬 문법으로 변환한 뒤 전역 변수에 업데이트한다.
# globals() is a dictionary of global variables

deep_update(globals(), get_settings_from_environment(ENVVAR_SETTINGS_PREFIX))  # type: ignore # noqa: F821

# 서버 실행시 core/project/settings/__init__.py에서 include()의 실행 순서 덕분에(split-setting 덕분에) 갠춘

+) 이건 뭐를 위한 설정인가?

🫟 docker.py

./core/project/settings 위치에 docker.py를 생성한 후 다음과 같이 작성한다;

# Docker 환경에서 MIDDLEWARE 설정이 기대한 보안 구조를 따를 것을 assert로 강제(하는 안전장치)
# 조건이 충족되지 않을시 AssertionError 에러와 함께 즉시 실행 종료됨

# 서버 실행시 core/project/settings/__init__.py에서 include()의 실행 순서 덕분에(split-setting 덕분에) 갠춘
if IN_DOCKER:  # type: ignore # noqa: F821

    # MIDDLEWARE 리스트의 첫 번째 요소가 SecurityMiddleware임을 확실하게 함
    assert MIDDLEWARE[:1] == [  # type: ignore # noqa: F821
        'django.middleware.security.SecurityMiddleware'
    ]

🎨 모든 설정 종합: __init__.py

이제 ./core/general/utils/ 폴더에 작성했던 유틸 패키지를 종합 __init__.py를 다음과 같이 작성해 준다.

import os.path
from pathlib import Path
from split_settings.tools import include, optional

# BASE_DIR 설정
BASE_DIR = Path(__file__).resolve().parent.parent.parent.parent


# 환경변수 prefix 정의
    # Namespacing(프로그램에서 사용되는 이름의 논리적 그룹인 식별자의 컨텍스트) (?)
ENVVAR_SETTINGS_PREFIX = 'CORE_SETTINGS_'


# 팀원들이 (템플렛을 사용해)각자의 로컬머신에서 settings.dev.py가 아닌 커스텀 세팅파일을 만들 경우(?)
LOCAL_SETTINGS_PATH = os.getenv(f'{ENVVAR_SETTINGS_PREFIX}LOCAL_SETTINGS_PATH')

if not LOCAL_SETTINGS_PATH:   # 커스텀 세팅파일 없으면
    LOCAL_SETTINGS_PATH = 'local/settings.dev.py'   # 디폴트 세팅파일 사용

# 팀원의 커스텀 세팅파일 path가 상대경로일 경우 절대경로로 변경
if not os.path.isabs(LOCAL_SETTINGS_PATH):
    LOCAL_SETTINGS_PATH = str(BASE_DIR / LOCAL_SETTINGS_PATH)


# 다수의 설정파일을 종합 (순서 중요)
include('base.py', 'custom.py', optional(LOCAL_SETTINGS_PATH), 'envvars.py', 'docker.py')

__init__.py 파일의 핵심은 맨 마지막의 include() 부분이다.
코드에서 보이다시피 include() 함수는 다수의 설정파일을 종합할 수 있도록 하는 django-split-settings 패키지에서 제공하는 기능이다.

이를 통해 장고 앱 실행시 가장 먼저 base.py를 읽어오며 그 뒤에 순차적으로 설정파일들을 읽어 메모리에 올려 놓도록 한다.

envvars.py에서 deep_update(globals(), get_settings_from_environment(ENVVAR_SETTINGS_PREFIX)) 의 ENVVAR_SETTINGS_PREFIX 부분에 (IDE가 표시하는)에러가 뜨지만 실제 프로젝트 실행시 문제없이 작동하는 이유가 이것 덕분이다.
(docker.py에서도 마찬가지)

🫟 장고 서버 실행 흐름 정리

+) 추가 필요

profile
하나씩 차근차근 천천히

0개의 댓글