다섯 줄이
실행 환경을 재현한다
모든 버전이 == 으로 고정돼 있다. 오늘 만든 이미지와 다음 달에 만든 이미지가 같은 코드를 담게 하는 것이 목적이다.
이 파일을 만든 이유
Python은 import fastapi가 어느 버전을 가리키는지 코드에 적지 않는다. 설치된 것을 쓸 뿐이다. 그래서 무엇을 설치할지 적어 두는 파일이 따로 필요하다.
이 프로젝트는 한 걸음 더 나아가 >=가 아니라 ==로 정확한 버전을 고정했다. 8GB 환경에서 어렵게 맞춰 놓은 동작이 어느 날 자동 업그레이드로 달라지면 원인을 찾기 어렵기 때문이다. Dockerfile이 이 파일만 먼저 복사하는 것도 layer cache를 얻기 위해서다 — 코드를 고쳐도 의존성 설치를 다시 하지 않는다.
전체 코드
fastapi==0.141.1
pydantic==2.13.5
pydantic-settings==2.15.0
uvicorn[standard]==0.52.4
httpx2==2.12.0
-r requirements.txt
pytest==9.1.1
다섯 package가 각각 하는 일
| package | 역할 | 이 프로젝트에서 쓰이는 곳 |
|---|---|---|
fastapi | HTTP API framework | main.py의 FastAPI(), router.py의 APIRouter |
pydantic | 데이터 검증과 직렬화 | 응답 schema, Adapter의 wire model |
pydantic-settings | 환경변수를 설정 객체로 | config.py의 BaseSettings |
uvicorn[standard] | ASGI 서버 | uvicorn app.main:app으로 실제 실행 |
httpx2 | 비동기 HTTP client | lifespan의 생성·probe client 두 개 |
uvicorn[standard]의 대괄호
extras라고 부르는 선택 의존성이다. standard를 지정하면 더 빠른 HTTP 파서와 이벤트 루프 구현, --reload에 필요한 파일 감시 도구가 함께 설치된다. 대괄호 없이 설치하면 순수 Python 구현으로 동작해 더 느리고 --reload도 되지 않는다.
fastapi와 pydantic을 따로 적은 이유
FastAPI가 Pydantic을 의존하므로 적지 않아도 설치된다. 그럼에도 명시한 것은 우리 코드가 Pydantic을 직접 import하기 때문이다. 직접 쓰는 것은 직접 고정한다 — 그래야 FastAPI가 의존 범위를 바꿔도 우리 버전이 흔들리지 않는다.
httpx2
추론 서버에 요청을 보내는 비동기 client다. 여기에는 모델을 실행하는 기능이 전혀 없다. FastAPI가 Gemma를 다시 적재하지 않는다는 프로젝트 규칙이 의존성 목록에서도 확인된다.
개발용 파일이 따로 있는 이유
-r requirements.txt ← 위 다섯 줄을 그대로 포함한다
pytest==9.1.1 ← 그 위에 개발용만 추가
-r은 다른 requirements 파일을 그대로 포함하라는 지시다. 목록을 두 번 적지 않아도 되고, 운영용을 고치면 개발용도 자동으로 따라온다.
| 파일 | 설치 대상 | 쓰는 곳 |
|---|---|---|
requirements.txt | 실행에 필요한 다섯 개 | Dockerfile의 production image |
requirements-dev.txt | 위 다섯 개 + pytest | 개발자의 로컬 .venv |
Dockerfile이 운영용만 설치하므로 pytest는 이미지에 들어가지 않는다. .dockerignore가 tests 폴더를 제외하는 것과 같은 방향의 결정이다 — 테스트는 이미지를 만들기 전에 끝낸다.
실제로 확인하는 방법
cd be
python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements-dev.txt
.venv/bin/python -m pip list | grep -iE "fastapi|pydantic|uvicorn|httpx2|pytest"
.venv/bin/python -m pytest -q
fastapi 0.141.1
httpx2 2.12.0
pydantic 2.13.5
pydantic-settings 2.15.0
pytest 9.1.1
uvicorn 0.52.4
47 passed
pip list에는 이보다 훨씬 많은 package가 보인다. 나머지는 이 다섯 개가 끌어온 간접 의존성이다. 그것들의 버전까지 고정하려면 pip freeze나 pip-compile로 만든 lock 파일이 필요한데, 현재는 직접 쓰는 package만 고정한 상태다.
자주 발생하는 오류
| 증상 | 원인 | 진단 |
|---|---|---|
ModuleNotFoundError: fastapi | 가상환경을 켜지 않음 | .venv/bin/python을 직접 쓰거나 activate한다 |
--reload가 동작하지 않음 | uvicorn을 extras 없이 설치 | uvicorn[standard]가 필요하다 |
| Docker에서만 다르게 동작 | host의 .venv가 섞임 | .dockerignore가 제외하는지 확인한다 |
pytest가 이미지에 없음 | 의도된 동작 | production image는 운영용만 설치한다 |
| 간접 의존성이 갑자기 바뀜 | lock 파일 없음 | 재현이 중요해지면 pip freeze 기반 파일을 추가한다 |
설계 선택과 대안
왜 Poetry나 uv를 쓰지 않는가?
둘 다 lock 파일과 의존성 해석을 자동으로 해 준다. 대신 도구를 하나 더 배워야 하고 Dockerfile도 바뀐다. 다섯 줄짜리 목록에서는 얻는 것보다 배우는 비용이 크다. 5단계에서 PostgreSQL·SQLAlchemy·Alembic이 들어오면 다시 판단할 문제다.
간접 의존성까지 고정해야 하나?
완전한 재현성을 원하면 그렇다. 다만 pip freeze 결과는 플랫폼에 따라 달라져 macOS에서 만든 목록이 Linux container에서 그대로 설치되지 않을 수 있다. 지금은 ==로 직접 의존성만 고정하는 절충이며, 그 한계를 알고 쓰는 것이 중요하다.
>=로 열어 두면?
보안 수정을 자동으로 받는 장점이 있다. 대신 어제 되던 것이 오늘 안 되는 상황이 생긴다. 학습이 목적인 이 프로젝트에서는 “변수를 하나라도 줄이는 것”이 우선이라 고정을 택했다.
이전 단계와 다음 파일
여기 설치한 package를 실제로 쓰는 첫 코드는 main.py다. 실행에 필요한 설정값은 .env.example, 이 목록으로 이미지를 만드는 과정은 Dockerfile에서 이어진다.