설정과 환경 변수
설정과 환경 변수 (Settings and Environment Variables)
앱을 만들다 보면 코드 밖에서 관리하고 싶은 값들이 생겨요. 시크릿 키, 데이터베이스 접속 정보, 이메일 서비스 자격 증명 같은 것들이죠. 이런 값들은 환경마다 달라질 수 있고, 보안에 민감한 경우도 많아요. 그래서 코드에 하드코딩하기보다 **환경 변수(environment variable)**로 두고 앱이 그걸 읽게 하는 게 일반적이에요.
이번 장에서는 Pydantic이 제공하는 Settings라는 유틸리티로 이런 설정을 우아하게 다루는 법을 배워볼게요.
출처: 공식문서
환경 변수의 탓: 타입과 검증
환경 변수는 파이썬 코드 바깥, 운영체제 안에 살아요. 그래서 다른 프로그램이나 다른 운영체제(Linux, Windows, macOS)와 호환되려면 **문자열(text string)**로만 다룰 수 있어요.
즉, 파이썬에서 환경 변수를 읽으면 언제나 str이에요. 그리고 그 값을 다른 타입으로 바꾸거나 검증하는 일은 코드에서 직접 해야 해요. 환경 변수 자체에는 타입 개념이 없으니까요.
Pydantic Settings
이런 번거로움을 해결해 주는 좋은 도구가 있어요. Pydantic이 환경 변수에서 오는 설정을 다루는 Settings 관리 기능을 제공해요.
pydantic-settings 설치하기
프로젝트에 pydantic-settings 패키지를 추가해요:
$ uv add pydantic-settings
---> 100%
all 엑스트라로 설치할 때도 함께 들어와요:
$ uv add "fastapi[all]"
---> 100%
Settings 객체 만들기
Pydantic에서 BaseSettings를 임포트하고, Pydantic 모델을 만들 때처럼 하위 클래스로 만들어요. 클래스 속성을 타입 애너테이션과 함께(기본값도 가능하게) 선언하면 돼요.
Pydantic 모델에 쓰던 검증 기능과 도구를 그대로 쓸 수 있어요. Field()로 추가 검증이나 다양한 데이터 타입도 쓸 수 있고요.
from fastapi import FastAPI
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
settings = Settings()
app = FastAPI()
@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
이제 Settings 클래스의 인스턴스(여기서는 settings 객체)를 만들면, Pydantic이 환경 변수를 대소문자 구분 없이 읽어요. 그래서 대문자 변수 APP_NAME이 app_name 속성으로 읽히는 식이에요.
그다음 데이터를 변환하고 검증해요. 그래서 settings 객체를 쓸 때는 선언한 타입 그대로의 데이터를 얻게 돼요 (예: items_per_user는 int가 되어 있어요).
settings 사용하기
이제 새로 만든 settings 객체를 앱에서 쓰면 돼요. 위 코드의 /info 경로가 그 예시죠.
서버 실행하기
이제 설정을 환경 변수로 넘기면서 서버를 실행해 봐요. 예를 들어 ADMIN_EMAIL과 APP_NAME을 이렇게 설정할 수 있어요:
$ ADMIN_EMAIL="[email protected]" APP_NAME="ChimichangApp" uv run fastapi run main.py
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
PowerShell에서는 이렇게 해요:
$ $Env:ADMIN_EMAIL = "[email protected]"
$ $Env:APP_NAME = "ChimichangApp"
$ uv run fastapi run main.py
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
Bash에서 하나의 명령에 여러 환경 변수를 설정하려면, 변수들을 공백으로 구분해서 명령 앞에 모두 넣으면 돼요.
이제 admin_email 설정은 "[email protected]"이 되고, app_name은 "ChimichangApp"이 돼요. 그리고 items_per_user는 기본값인 50을 그대로 유지해요.
설정을 다른 모듈에 두기
이 설정들을 여러 파일로 앱 만들기에서 본 것처럼 다른 모듈 파일에 둘 수도 있어요. 예를 들어 config.py 파일을 이렇게 만들고:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
settings = Settings()
main.py에서 이렇게 사용해요:
from fastapi import FastAPI
from .config import settings
app = FastAPI()
@app.get("/info")
async def info():
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
팁 — 여러 파일로 앱 만들기에서 본 대로
__init__.py파일도 필요해요.
의존성 주입으로 설정 다루기
어디서나 쓰이는 전역 settings 객체 대신, 설정을 **의존성(dependency)**에서 제공하는 게 더 유용한 경우가 있어요. 특히 테스트할 때 오버라이드하기 매우 쉬워져서 좋아요.
설정 파일 (config file)
앞의 예제를 이어서, config.py 파일은 이렇게 생겼어요:
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
이번에는 settings = Settings() 같은 기본 인스턴스를 만들지 않는다는 점에 주목해 주세요.
메인 앱 파일 (main app file)
이제 새 config.Settings()를 반환하는 의존성을 만들어요:
from functools import lru_cache
from typing import Annotated
from fastapi import Depends, FastAPI
from .config import Settings
app = FastAPI()
@lru_cache
def get_settings():
return Settings()
@app.get("/info")
async def info(settings: Annotated[Settings, Depends(get_settings)]):
return {
"app_name": settings.app_name,
"admin_email": settings.admin_email,
"items_per_user": settings.items_per_user,
}
팁 —
@lru_cache는 잠시 후에 이야기할게요. 지금은get_settings()가 평범한 함수라고 생각해도 돼요.
그리고 경로 연산 함수에서 이걸 의존성으로 요구하면, 필요한 곳 어디에서든 그대로 쓸 수 있어요.
설정과 테스트
이제 테스트에서 get_settings에 대한 의존성 오버라이드를 만들어 주면, 다른 설정 객체를 아주 쉽게 제공할 수 있어요:
from fastapi.testclient import TestClient
from .config import Settings
from .main import app, get_settings
client = TestClient(app)
def get_settings_override():
return Settings(admin_email="[email protected]")
app.dependency_overrides[get_settings] = get_settings_override
def test_app():
response = client.get("/info")
data = response.json()
assert data == {
"app_name": "Awesome API",
"admin_email": "[email protected]",
"items_per_user": 50,
}
의존성 오버라이드에서 새 Settings 객체를 만들 때 admin_email에 새 값을 넣고, 그 객체를 반환해요. 그다음 실제로 그 값이 쓰이는지 테스트하는 거죠.
.env 파일 읽기
설정이 많고 환경마다 자주 바뀐다면, 파일에 적어 두고 환경 변수처럼 읽어 오는 게 편해요. 이런 관행은 흔해서 이름까지 있어요. 환경 변수들을 .env 파일에 두면, 그 파일을 dotenv라고 불러요.
참고 — 점(
.)으로 시작하는 파일은 Unix 계열 시스템(Linux, macOS)에서 숨김 파일이에요. 물론 dotenv 파일 이름이 반드시 그럴 필요는 없어요.
Pydantic은 외부 라이브러리를 써서 이런 파일을 읽는 걸 지원해요. 자세한 내용은 Pydantic Settings: Dotenv (.env) support를 참고하세요.
팁 — 이게 동작하려면
uv add python-dotenv로python-dotenv를 프로젝트에 추가해 주세요.
.env 파일
.env 파일을 이렇게 둘 수 있어요:
ADMIN_EMAIL="[email protected]"
APP_NAME="ChimichangApp"
.env에서 설정 읽기
그리고 config.py를 이렇게 업데이트해요:
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
app_name: str = "Awesome API"
admin_email: str
items_per_user: int = 50
model_config = SettingsConfigDict(env_file=".env")
팁 —
model_config속성은 Pydantic 설정을 위한 거예요. 자세한 내용은 Pydantic: Concepts: Configuration에서 볼 수 있어요.
여기서는 Pydantic Settings 클래스 안에서 env_file 설정을 정의하고, 사용할 dotenv 파일 이름으로 값을 넣었어요.
lru_cache로 Settings 만들기
디스크에서 파일을 읽는 건 비용이 드는(느린) 작업이에요. 그래서 요청마다 다시 읽기보다, 설정 객체를 한 번만 만들고 재사용하고 싶어요.
매번 이렇게 하면:
Settings()
새 Settings 객체가 만들어지고, 만들 때마다 .env 파일을 다시 읽어요.
만약 의존성 함수가 그냥 이렇게 되어 있었다면:
def get_settings():
return Settings()
요청마다 객체를 만들고, 요청마다 .env 파일을 읽게 되는 거예요. ⚠️
하지만 위에 @lru_cache 데코레이터를 붙였으니, Settings 객체는 처음 호출될 때 한 번만 만들어져요. ✔️
그러면 그다음 요청들에서 의존성의 get_settings()가 다시 호출될 때, 내부 코드를 실행하고 새 Settings 객체를 만드는 대신, 처음 호출에서 반환된 같은 객체를 계속 반환해요.
lru_cache 기술적 세부사항
@lru_cache는 데코레이팅된 함수가 매번 코드를 다시 실행해 값을 계산하는 대신, 처음 반환했던 값과 같은 값을 반환하도록 바꿔요.
그래서 아래의 함수는 인자 조합마다 한 번씩 실행되고, 그 후로는 정확히 같은 인자 조합으로 호출될 때마다 그 조합이 반환했던 값이 계속 쓰여요.
예를 들어 이런 함수가 있다면:
@lru_cache
def say_hi(name: str, salutation: str = "Ms."):
return f"Hello {salutation} {name}"
같은 인자로 다시 호출되면 함수 본문은 실행되지 않고, 저장된 결과가 반환돼요.
우리의 의존성 get_settings()는 인자를 아예 받지 않으니, 항상 같은 값을 반환해요. 그래서 거의 전역 변수처럼 동작해요. 그런데 의존성 함수로 만들어 두었으니 테스트할 때 쉽게 오버라이드할 수도 있고요.
@lru_cache는 파이썬 표준 라이브러리인 functools의 일부예요. 더 자세한 내용은 파이썬 공식 문서의 @lru_cache를 참고하세요.
정리 (Recap)
Pydantic Settings를 쓰면 Pydantic 모델의 모든 힘을 그대로 누리면서 앱의 설정이나 구성을 다룰 수 있어요.
- 의존성을 쓰면 테스트가 쉬워져요.
.env파일도 쓸 수 있어요.@lru_cache를 쓰면 요청마다 dotenv 파일을 다시 읽지 않으면서도, 테스트 중에는 오버라이드할 수 있어요.