Python

Pydantic BaseSettings로 환경변수 관리하기 – @property 패턴 적용기

코줍 2025. 6. 13. 15:06

 

https://kojub.tistory.com/24

 

Pydantic.BaseSetting을 사용한 환경변수 관리

.env 파일은 중요한 설정과 변수들을 정의한다. DB 정보, URL, API Key 등과 같은 민감한 정보를 코드에 하드코딩하지 않고 관리할 수 있어 필수적인 파일이다.그러나 개발⸰테스트⸰배포 환경이나

kojub.tistory.com

 

한 5개월 전에 Pydantic BaseSetting으로 환경변수 관리하는 config파일을 구성한 적 있다.

import os

from dotenv import load_dotenv
from pydantic_settings import BaseSettings

load_dotenv()


def _getenv(name: str, default: str = None) -> str | None:
    value = os.getenv(name, default)
    return value


class Configs(BaseSettings):

    DB_ENGINE: str = _getenv("DB_ENGINE")
    DB_USER: str = _getenv("DB_USER")
    DB_PASSWORD: str = _getenv("DB_PASSWORD")
    DB_HOST: str = _getenv("DB_HOST")
    DB_PORT: str = _getenv("DB_PORT")
    DATA_BASE: str = _getenv("DATA_BASE")

    DATABASE_URI: str = (
        "{db_engine}://{user}:{password}@{host}:{port}/{database}".format(
            db_engine=DB_ENGINE,
            user=DB_USER,
            password=DB_PASSWORD,
            host=DB_HOST,
            port=DB_PORT,
            database=DATA_BASE,
        )
    )

configs = Configs()

 

 

더 나은 방식에 대한 고민의 계기

이번에 한국관광공사 공모전에 참가하게 되면서, 새로운 프로젝트를 생성하게 되었다. ( 또 새로운 프로젝트 생성했다 젠장...마무리 짓는 것이 목표 ^^;;; )

시간 안에 완성도 있는 서비스를 만들어야 하기 때문에 백엔드 스택은 제일 익숙한..원래 사용하던...FastAPI + Postgresql  + Sqlalchemy 조합을 그대로 가져가게 되었다.

 

하지만 매번 같은 방식으로 초기 세팅을 하다 보면 어느 순간 발전이 없다는 생각이 들어서, 이번에는 환경 세팅과 config 구성부터 조금씩 변화를 줘보기로 했다.  


 

더 나은 방식 포인트

1. class Config로 .env파일을 자동처리할 수 있다.

load_dotenv()

def _getenv(name: str, default: str = None) -> str | None:
    value = os.getenv(name, default)
    return value

 

이 코드는 로컬에 있는 .env파일을 로드하기 위해 필요한 코드다.

따로 커스텀함수 까지 만들어서 .env파일에 정의된 환경변수를 가져오게끔 처리를 한거다.

 

class Configs(BaseSettings):

    ...
    
    class Config:
        env_file = ".env"

 

 

위처럼 Pydantic에서 Class Config를 사용하면, 자동으로 .env파일을 로드할 수 있다.

 

이게 좋은 게 뭐냐면, 

 

  1. 테스트용 DB로 connection 걸어야 할 땐, env_file값만 바꿔주면 된다는 점이다.
  2. 또, 불필요하게 load_dotenv() 메소드를 명시적으로 작성할 필요도 없고 __getenv메소드 처럼 커스텀 메소드를 만들 필요도 없다.

 

2. 실행 지점에서만 실패하게 만드는 설계 패턴 ( @property )

DATABASE_URI: str = (
    "{db_engine}://{user}:{password}@{host}:{port}/{database}".format(
        db_engine=DB_ENGINE,
        user=DB_USER,
        password=DB_PASSWORD,
        host=DB_HOST,
        port=DB_PORT,
        database=DATA_BASE,
    )
)

 

이렇게 되어 있다보니까, class 정의 시점에서 DB_ENGINE같은 변수를 전부 가져오고 .format도 즉시실행되어 DATABASE_URL에 할당된다. 그래서 .env파일이나 시스템 환경변수 주우 하나라도 없으면 서버 실행하자마자 에러가 난다.

 

class Configs(BaseSettings):
    DB_ENGINE: str = _getenv("DB_ENGINE")
    ...
    
    @property
    def DATABASE_URL(self):
        return f"{self.DB_ENGINE}://..."

 

 

이렇게 바꿔주면,  DATABASE_URL을 호출하기 전까진 실행되지 않는다.

그래서 configs.DATABASE_URL을 실제로 사용할 시점에 에러가 발생한다. 즉, Config() 객체 생성엔 문제가 없다.

 

이게 왜 좋냐면, 

  1. 유닛테스트나 로컬 서버를 띄울 때 꼭 DB 연결이 필요한 게 아니면, 설정이 완전하지 않아도 앱이 뜰 수 있다.
  2. 꼭 필요한 시점에만 오류를 만날 수 있어서 디버깅도 더 쉬워진다.

 

그래서 완성된 Config 파일은요

from pydantic_settings import BaseSettings

class Configs(BaseSettings):
    DB_ENGINE: str
    DB_USER: str
    DB_PW: str
    DB_HOST: str
    DB_PORT: str
    DATA_BASE: str

    @property
    def DATABASE_URL(self):
        return f"{self.DB_ENGINE}://{self.DB_USER}:{self.DB_PW}@{self.DB_HOST}:{self.DB_PORT}/{self.DATA_BASE}"

    class Config:
        env_file = ".env"