왜 설정 관리가 반복적으로 사고를 만드는가
운영 사고의 상당수는 코드가 아니라 설정에서 시작된다. 스테이징 DB 주소가 프로덕션 코드에 섞여 있거나, 배포 직전에 누군가 로컬 .env를 커밋해 시크릿이 유출되는 식이다. 12-factor의 세 번째 원칙 "설정은 환경변수에 저장하라"는 이 문제의 방향을 제시하지만, 원문을 문자 그대로 지키면 오히려 관리가 어려워진다. 환경변수가 수십 개로 불어나면 어디서 주입되는지 추적이 불가능해지기 때문이다. 핵심은 "환경변수만 써라"가 아니라 설정을 코드에서 분리하고, 환경 간 차이를 명시적으로 만들라는 의도다.
설정의 세 가지 유형을 구분한다
모든 설정을 같은 방식으로 다루면 안 된다. 성격에 따라 저장 위치와 접근 방식이 달라야 한다.
| 유형 | 예시 | 저장 위치 | 환경별 차이 |
|---|---|---|---|
| 비밀값 | DB 비밀번호, API 키 | 시크릿 매니저 | 있음 |
| 환경 종속값 | DB 호스트, 로그 레벨 | 환경변수 / 설정 파일 | 있음 |
| 불변 상수 | 재시도 횟수, 타임아웃 | 코드 또는 기본값 | 없음 |
불변 상수까지 환경변수로 빼는 것은 과잉이다. 환경마다 달라질 이유가 없는 값은 코드에 두고, 정말 환경에 종속된 값만 밖으로 꺼낸다.
계층형 설정 로딩: 기본값 위에 환경을 덮어쓴다
실무에서 검증된 패턴은 "기본값 → 환경별 파일 → 환경변수" 순으로 덮어쓰는 것이다. 코드에 안전한 기본값을 두고, 환경변수는 오직 차이나는 부분만 담당한다. 아래는 파이썬에서 타입 검증까지 포함한 예시다.
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=(".env", f".env.{__import__('os').getenv('APP_ENV', 'local')}"),
env_prefix="APP_",
)
app_env: str = "local"
db_host: str = "localhost" # 안전한 기본값
db_pool_size: int = 5 # 불변 상수는 코드에
log_level: str = "INFO"
db_password: str # 기본값 없음 → 미주입 시 즉시 실패
settings = Settings() # 부팅 시점에 검증, 잘못된 타입이면 예외
db_password처럼 기본값이 없는 필드는 주입되지 않으면 애플리케이션이 부팅 시점에 죽는다. 런타임 한복판에서 KeyError를 만나는 것보다 훨씬 낫다.
비밀값은 파일이 아니라 주입으로
시크릿을 .env에 두면 언젠가 반드시 커밋된다. Git 히스토리에 한 번 들어간 시크릿은 회수가 사실상 불가능하다. CI/CD 파이프라인에서는 시크릿 매니저나 CI 시크릿 저장소에서 배포 직전에 주입한다.
# .github/workflows/deploy.yml
jobs:
deploy:
environment: production # 환경별 시크릿 스코프 분리
steps:
- name: Render config
env:
APP_DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
APP_ENV: production
run: |
# 시크릿을 로그에 남기지 않도록 마스킹 확인
echo "::add-mask::$APP_DB_PASSWORD"
./scripts/deploy.sh
GitHub Actions의 environment를 쓰면 production 시크릿을 스테이징 워크플로가 읽지 못하도록 격리할 수 있다. 시크릿을 echo로 출력하는 실수는 add-mask로 방어한다.
환경변수만으로는 부족한 지점
12-factor가 나온 2011년과 지금은 다르다. 환경변수는 프로세스 단위 평면 문자열이라 중첩 구조나 목록을 표현하기 어렵고, 컨테이너 오케스트레이션에서는 오히려 ConfigMap과 Secret으로 나누는 편이 낫다. 원칙의 정신인 "코드-설정 분리"는 지키되, 저장 매체는 환경에 맞춰 선택한다.
# k8s: 일반 설정과 비밀값을 분리
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
APP_ENV: "production"
APP_LOG_LEVEL: "WARNING"
---
apiVersion: v1
kind: Secret
metadata:
name: app-secret
type: Opaque
stringData:
APP_DB_PASSWORD: ""
주의점: 설정 드리프트와 검증
가장 흔한 사고는 환경 간 설정 항목이 어긋나는 "드리프트"다. 스테이징에만 있는 키가 프로덕션에는 없어서 배포 후에야 발견된다. 방어책은 두 가지다. 첫째, 필수 설정 목록을 코드로 선언하고 부팅 시 검증한다(위 pydantic 예시가 이 역할을 한다). 둘째, 환경 간 설정 키 집합을 CI에서 비교한다.
# 환경 간 키 누락 검사 (배포 전 CI에서 실행)
diff <(grep -oE '^APP_[A-Z_]+' .env.staging | sort) \
<(grep -oE '^APP_[A-Z_]+' .env.production | sort) \
&& echo "OK: 키 집합 일치" \
|| { echo "환경 간 설정 키 불일치"; exit 1; }
값이 아니라 키의 존재 여부를 비교하는 것이 핵심이다. 값은 환경마다 달라야 정상이지만, 키가 빠지는 것은 거의 항상 실수다.
정리
12-factor의 설정 원칙은 "환경변수를 쓰라"가 아니라 "설정을 코드에서 분리하고 차이를 명시하라"로 읽어야 한다. 설정을 유형별로 나누고, 계층형으로 로딩하며, 비밀값은 주입하고, 부팅 시점에 검증하는 것 — 이 네 가지가 원칙을 현실에 맞게 옮기는 방법이다.