실행 위치가 달라도
코드 대신 설정만 바꾼다
macOS에서 직접 실행하면 127.0.0.1, Docker 안에서는 host.docker.internal을 사용한다. 주소·model ID·timeout을 source 밖으로 빼 같은 image를 환경별로 재사용한다.
왜 URL을 Adapter에 고정하지 않는가?
http://127.0.0.1:8080을 코드에 쓰면 host 실행에서는 맞지만 container 안에서는 자기 자신을 가리킨다. 반대로 Docker 전용 주소를 고정하면 local Python 실행이 불편해진다. Environment variable은 “무엇을 할지”라는 code와 “어디에 연결할지”라는 배포 값을 분리한다.
현재 전체 코드
from functools import lru_cache
from typing import Literal
from pydantic import Field, HttpUrl, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Configuration loaded from APP_-prefixed environment variables."""
service_name: str = "local-moe-backend"
service_version: str = "0.1.0"
environment: Literal["development", "test", "production"] = "development"
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"
inference_base_url: HttpUrl = HttpUrl("http://127.0.0.1:8080")
inference_model: str = Field(default="gemma-4-26b-a4b-it", min_length=1)
inference_connect_timeout: float = Field(default=3.0, gt=0, allow_inf_nan=False)
inference_probe_timeout: float = Field(default=5.0, gt=0, allow_inf_nan=False)
inference_generation_timeout: float = Field(default=300.0, gt=0, allow_inf_nan=False)
@field_validator("inference_base_url")
@classmethod
def require_server_root(cls, value: HttpUrl) -> HttpUrl:
"""Paths live in the adapter, not in deployment configuration."""
if value.path not in (None, "/") or any(
part is not None
for part in (value.username, value.password, value.query, value.fragment)
):
raise ValueError("Use a server root URL without /v1, credentials, query or fragment")
return value
model_config = SettingsConfigDict(
env_prefix="APP_",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
@lru_cache
def get_settings() -> Settings:
"""Create one immutable-by-convention settings object per process."""
return Settings()추론 설정 다섯 개
| 환경변수 | 기본값 | 검증과 의미 |
|---|---|---|
APP_INFERENCE_BASE_URL | http://127.0.0.1:8080 | 유효한 HTTP(S) URL이며 server root만 허용 |
APP_INFERENCE_MODEL | gemma-4-26b-a4b-it | 빈 문자열 불가, model 목록과 응답에서 재확인 |
APP_INFERENCE_CONNECT_TIMEOUT | 3초 | 연결·pool 대기 상한, 0·음수·무한·NaN 불가 |
APP_INFERENCE_PROBE_TIMEOUT | 5초 | health와 models 각각의 HTTP 전송·본문 수신 deadline |
APP_INFERENCE_GENERATION_TIMEOUT | 300초 | 생성 HTTP 요청의 풀 대기·전송·본문 수신 deadline |
연결 실패는 빠르게 확인해야 하지만 8GB Mac에서 큰 MoE 생성은 수십 초 이상 걸릴 수 있다. 하나의 짧은 timeout을 모든 작업에 쓰면 정상적인 생성도 실패하므로 목적별로 나눈다. 두 probe 요청에 각각 5초가 적용되므로 readiness 전체의 5초 보장은 아니며, HTTP 수신 뒤의 동기 JSON 검증도 이 deadline 밖이다.
왜 base URL에 /v1을 넣으면 안 되는가?
require_server_root()는 path, 사용자명·비밀번호, query, fragment가 붙은 URL을 거절한다. Endpoint 경로는 Adapter 하나가 소유해야 변경 지점이 명확하다.
HttpUrl을 쓰면 무엇이 좋아지는가?
단순 str은 not-a-url도 받아들인다. HttpUrl은 scheme과 host를 파싱한 object를 만들어 Settings 구성 시점에 잘못된 설정을 발견한다. 기본 실행에서는 app 모듈 import 중 create_app → get_settings에서 발생하므로 lifespan startup에 들어가기 전 실패할 수도 있다. 실제 연결 가능성까지 검사하는 것은 아니며 그것은 /readyz의 역할이다.
Host와 Docker에서 실제 값이 달라지는 과정
Python과 TurboFieldfare가 같은 macOS network namespace에 있으므로 loopback으로 연결한다.
Container의 127.0.0.1은 container 자신이다. Docker Desktop의 특별 DNS 이름으로 macOS host를 가리킨다.
실제 container에서 이 주소로 /health, /v1/models, completion이 모두 HTTP 200임을 확인했다. TurboFieldfare 자체의 bind는 계속 127.0.0.1이며 LAN에 직접 공개하지 않았다.
lru_cache와 “immutable-by-convention”
get_settings()를 처음 호출하면 Settings를 만들고, 이후 같은 process에서는 cache된 object를 반환한다. 요청마다 .env를 다시 파싱하지 않는다. 다만 Settings class 자체에 frozen=True를 설정한 것은 아니므로 언어 차원에서 변경 불가능한 object는 아니다. “수정하지 않기로 한 관례”이며 test는 명시적 Settings를 create_app()에 전달해 서로 격리한다.
자주 생기는 오류
| 증상 | 가능한 원인 | 확인 |
|---|---|---|
unavailable | server 미실행, Docker에서 127 사용 | 실행 위치와 base URL 확인 |
| 설정 구성 중 validation error | /v1 포함, timeout 0, URL 오타 | .env와 environment 검사 |
model_mismatch | --model-id와 설정이 다름 | /v1/models 응답 확인 |
| 환경변수를 바꿨는데 유지 | 실행 중 process의 cached settings | Backend를 정상 재시작 |