# Part 9: 프로덕션 레벨 API와 Docker

**⬅️ 이전 시간: [Part 8: FastAPI를 이용한 모델 서빙](../08_model_serving_with_fastapi/part_8_model_serving_with_fastapi.md)**
**➡️ 다음 시간: [Part 10: 전문가로 가는 길](../10_expert_path/part_10_expert_path.md)**

---

## 1. 학습 목표 (Learning Objectives)

이번 파트가 끝나면, 여러분은 다음을 할 수 있게 됩니다.

- 유지보수와 협업에 용이하도록 FastAPI 프로젝트를 기능별로 구조화할 수 있습니다.
- `APIRouter`를 사용하여 API 엔드포인트를 모듈화하고 관리할 수 있습니다.
- "제 컴퓨터에선 되는데요..." 문제를 해결하는 Docker의 원리를 이해하고, 이미지/컨테이너 개념을 설명할 수 있습니다.
- `Dockerfile`을 작성하여 파이썬 애플리케이션을 컨테이너 이미지로 만들 수 있습니다.
- `docker-compose.yml`을 사용하여 여러 서비스(API 서버, DB 등)를 한 번에 실행하고 관리할 수 있습니다.

## 2. 핵심 키워드 (Keywords)

`프로젝트 구조화`, `관심사 분리(SoC)`, `APIRouter`, `의존성 주입(DI)`, `Docker`, `컨테이너(Container)`, `이미지(Image)`, `Dockerfile`, `Docker Compose`

## 3. 도입: '1인 식당'에서 '프랜차이즈 레스토랑'으로

지난 시간에는 FastAPI로 '1인 식당'처럼 빠르게 AI 모델 API를 만들었습니다. 프로토타입으로는 훌륭하지만, 메뉴가 다양해지고(기능 추가) 여러 셰프가 협업해야 하는(팀 개발) 실제 '프랜차이즈 레스토랑' 환경에서는 한계가 명확합니다.

이번 주차에는 `main.py` 하나에 모든 것을 담던 방식에서 벗어나, **유지보수와 확장이 용이한 프로젝트 구조**로 우리 레스토랑을 리팩터링합니다. 또한, 어떤 백화점에 입점하든 동일한 맛과 서비스를 보장하는 **'밀키트(Docker 컨테이너)'** 기술을 배워, 우리 AI 서비스를 세상 어디에든 쉽고 안정적으로 배포하는 능력을 갖추게 될 것입니다.

> [!TIP]
> 본 파트의 모든 예제 코드는 `../../source_code/part_9_fastapi_production` 폴더에서 직접 실행하고 수정해볼 수 있습니다.

---

## 4. 1단계: 프로덕션 레벨 프로젝트 구조 설계

> **🎯 1-2일차 목표:** '관심사 분리' 원칙에 따라 FastAPI 프로젝트를 기능별 파일과 폴더로 재구성합니다.

`main.py` 하나에 모든 코드가 있는 것은, 주방장이 혼자 주문받고, 요리하고, 서빙까지 하는 것과 같습니다. 우리는 이제 역할을 분담하여 '프랜차이즈 본사'처럼 체계적인 시스템을 만들 것입니다.

- **`settings.py`**: 본사의 운영 방침 (환경 설정)
- **`schemas.py`**: 모든 지점에서 통일된 '주문서 양식' (데이터 형식 정의)
- **`routers/`**: '한식 코너', '중식 코너' 등 메뉴별로 분리된 주방 (기능별 API)
- **`main.py`**: 모든 코너를 총괄하고 손님을 맞는 '총지배인'

### 4-1. 프로젝트 폴더 및 라우터 분리
먼저 아래와 같이 프로젝트 구조를 만듭니다. `main.py`에 있던 예측 관련 로직을 `routers/predictions.py`로 옮깁니다.

```
/fastapi_project
├── app/
│   ├── __init__.py
│   ├── main.py
│   └── routers/
│       ├── __init__.py
│       └── predictions.py
├── requirements.txt
└── iris_model.pkl
```

**`app/routers/predictions.py` (예측 코너 주방)**:
`APIRouter`를 사용하여 예측 기능만을 위한 독립적인 API 그룹을 만듭니다.

```python
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List
import joblib
import numpy as np

# 1. 이 라우터만의 '주문서 양식' 정의
class PredictionRequest(BaseModel):
    features: List[float]

# 2. APIRouter 인스턴스 생성
router = APIRouter(prefix="/predict", tags=["predictions"])

# 3. 모델 로드 (임시)
model = joblib.load("iris_model.pkl")
class_names = ['setosa', 'versicolor', 'virginica']

# 4. 예측 엔드포인트 정의
@router.post("/", summary="붓꽃 품종 예측")
def predict_iris(request: PredictionRequest):
    model_input = np.array(request.features).reshape(1, -1)
    prediction_idx = model.predict(model_input)[0]
    confidence = model.predict_proba(model_input)[0][prediction_idx]
    
    return {
        "predicted_class": class_names[prediction_idx],
        "confidence": float(confidence)
    }
```

### 4-2. 최종 통합 (`main.py`)
'총지배인' `main.py`에서 분리된 '예측 코너'를 전체 레스토랑에 포함시킵니다.

```python
from fastapi import FastAPI
from .routers import predictions

app = FastAPI(title="AI 레스토랑 (프랜차이즈 ver)")

# "예측 코너" 라우터 포함
app.include_router(predictions.router)

@app.get("/")
def read_root():
    return {"message": "AI 레스토랑에 오신 것을 환영합니다."}
```
이제 `uvicorn app.main:app --reload`로 서버를 실행하고 `http://127.0.0.1:8000/docs`에 접속하면, `predictions`라는 태그로 그룹화된 API를 확인할 수 있습니다.

---

## 5. 2단계: Docker로 우리 앱 포장하기

> **🎯 3-4일차 목표:** Dockerfile을 작성하여 우리 앱을 이미지로 만들고, docker-compose로 실행합니다.

### 5-1. 왜 Docker를 써야 할까?
> **💡 비유: '어디서든 똑같은 맛을 내는 밀키트'**
>
> 우리 레스토랑의 레시피와 재료를 완벽하게 준비해도, 지점마다 주방 환경(OS, 라이브러리 버전 등)이 다르면 음식 맛이 달라질 수 있습니다. "제 주방(컴퓨터)에서는 되는데요?" 라는 문제는 개발자들의 흔한 악몽입니다.
>
> **Docker**는 우리 레스토랑의 모든 것(코드, 라이브러리, 설정)을 하나의 **'밀키트(이미지)'**로 완벽하게 포장하는 기술입니다. 이 밀키트는 어떤 주방(서버)에서 데우든 항상 똑같은 환경에서, 똑같은 맛의 요리를 만들어냅니다.

### 5-2. Dockerfile: 밀키트 레시피 작성
`Dockerfile`은 이 밀키트를 만드는 '레시피'입니다. 프로젝트 최상위 폴더에 `Dockerfile`이라는 이름으로 파일을 만듭니다.

```dockerfile
# 1. 베이스 이미지 선택 (기본 주방 키트 가져오기)
FROM python:3.9-slim

# 2. 작업 디렉토리 설정 (조리대 위치 지정)
WORKDIR /code

# 3. 의존성 파일 먼저 복사 (소스가 바뀌어도 의존성이 같으면 캐시 재사용)
COPY ./requirements.txt /code/requirements.txt

# 4. 의존성 설치 (필요한 조미료 채우기)
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# 5. 소스 코드와 모델 파일 복사 (레시피와 식재료 넣기)
COPY ./app /code/app
COPY ./iris_model.pkl /code/iris_model.pkl

# 6. 서버 실행 (레스토랑 개점!)
# 0.0.0.0 주소로 실행해야 Docker 외부에서 접속 가능
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
```

### 5-3. Docker Compose: 프랜차이즈 매장 동시 오픈
`docker-compose.yml`은 여러 '밀키트'(서비스)들을 정의하고 한 번에 관리하는 '프랜차이즈 오픈 계획서'입니다. 지금은 API 서버 하나지만, 나중에 DB 등이 추가되면 그 위력을 실감하게 됩니다.

```yaml
# docker-compose.yml
version: '3.8'

services:
  # 서비스 이름 정의 (우리 레스토랑 지점)
  api:
    # 어떤 Dockerfile을 사용할지 지정
    build: .
    # 포트 매핑 (외부 8000번 포트 -> 컨테이너 내부 8000번 포트)
    ports:
      - "8000:8000"
    # 컨테이너에 이름 부여
    container_name: fastapi_iris_app
```

---

## 6. 캡스톤 프로젝트 연계 미니 프로젝트: 나만의 AI API '밀키트' 만들기

지난 8장 미니 프로젝트에서 여러분은 '1인 식당'처럼 빠르게 동작하는 API를 만들었습니다. 이제 이 식당을 '프랜차이즈 본사'처럼 체계적으로 리팩터링하고, 어디서든 동일한 품질을 보장하는 '밀키트(Docker 이미지)'로 포장하는 작업을 진행합니다. 이 과정은 캡스톤 프로젝트의 코드를 체계적으로 관리하고, 안정적으로 배포하는 핵심 역량을 길러줍니다.

### 프로젝트 목표
- 8장 미니 프로젝트에서 만든 '나만의 AI 모델 API'를 '관심사 분리' 원칙에 따라 체계적인 폴더 구조로 리팩터링할 수 있습니다.
- `APIRouter`를 사용하여 API 엔드포인트 로직을 `main.py`에서 완전히 분리할 수 있습니다.
- `Dockerfile`을 작성하여, 내 모델과 API 코드가 포함된 '나만의 Docker 이미지'를 성공적으로 빌드할 수 있습니다.
- `docker-compose up` 명령어로, 직접 만든 AI API 컨테이너를 실행하고 API가 정상적으로 동작하는 것을 확인할 수 있습니다.

### 개발 과정
1.  **프로젝트 구조 리팩터링**:
    - 8장에서 만든 `my_api` 프로젝트를 아래와 같은 구조로 변경합니다.
      ```
      /my_prod_api
      ├── app/
      │   ├── __init__.py
      │   ├── main.py
      │   ├── schemas.py       # Pydantic 모델 분리
      │   └── routers/
      │       ├── __init__.py
      │       └── predict.py   # 예측 로직 분리
      ├── models/              # 모델/토크나이저 등은 별도 폴더로
      │   └── sentiment_model.pth
      ├── requirements.txt
      ├── Dockerfile
      └── docker-compose.yml
      ```
    - `schemas.py`: `ReviewRequest`, `SentimentResponse` 등 Pydantic 모델들을 이 파일로 옮깁니다.
    - `routers/predict.py`: `APIRouter`를 생성하고, 모델 로딩과 예측 로직을 이 파일로 옮깁니다. (모델 파일 경로는 `../models/sentiment_model.pth`와 같이 수정되어야 합니다.)

2.  **`main.py` 재작성**:
    - `main.py`는 이제 '총지배인' 역할만 합니다. FastAPI 앱을 생성하고, `predict.py`에 정의된 라우터를 `app.include_router()`로 포함시키는 코드만 남깁니다.

3.  **`requirements.txt` 생성**:
    - 8장 프로젝트에 필요했던 모든 라이브러리(`fastapi`, `uvicorn`, `torch`, `pydantic` 등)를 `requirements.txt`에 명시합니다.

4.  **`Dockerfile` 작성**:
    - 본문 예제를 참고하여 `Dockerfile`을 작성합니다.
    - `COPY` 명령어를 사용하여 `app/` 폴더 전체와 `models/` 폴더 전체를 컨테이너의 작업 디렉토리로 복사해야 합니다.
      ```dockerfile
      # ...
      COPY ./app /code/app
      COPY ./models /code/models
      # ...
      ```

5.  **`docker-compose.yml` 작성 및 실행**:
    - 본문 예제를 참고하여 `docker-compose.yml` 파일을 작성합니다.
    - 터미널에서 `docker-compose up --build` 명령을 실행하여 이미지를 빌드하고 컨테이너를 실행합니다.
    - `http://localhost:8000/docs`에 접속하여, 리팩터링되고 Docker 컨테이너 안에서 실행 중인 여러분의 API가 이전과 똑같이 잘 동작하는지 확인합니다.

### 캡스톤 프로젝트 연계 방안
이번 미니 프로젝트에서 완성한 **프로젝트 구조**와 **Dockerfile**, **docker-compose.yml**은 여러분의 캡스톤 프로젝트의 '뼈대'가 됩니다. 새로운 기능(예: 사용자 인증, 결과 저장)이 필요할 때마다, 이 구조에 따라 `routers`에 새로운 파일을 추가하고, DB 컨테이너를 `docker-compose.yml`에 추가하는 방식으로 체계적인 확장이 가능합니다. 이는 최종 프로젝트의 기술적 완성도와 협업 효율성을 크게 높여줄 것입니다.

---

## ⚠️ What Could Go Wrong? (토론 주제)

이제 우리는 Docker를 사용하여 어디서든 동일하게 실행되는 애플리케이션을 만들 수 있게 되었습니다. 하지만 실제 프로덕션 환경은 '실행되는 것' 이상의 것을 요구합니다. 다음 시나리오들에 대해 토론해보세요.

1.  **점점 비대해지는 Docker 이미지**
    -   **상황**: 처음에는 200MB였던 우리 팀의 Docker 이미지가, 여러 기능이 추가되면서 이제는 2GB를 넘어섰습니다. 이미지를 빌드하고 레지스트리에 푸시하는 시간이 너무 오래 걸리고, 서버에 배포하는 시간도 길어졌습니다.
    -   **토론**:
        -   Docker 이미지 크기가 커지는 일반적인 원인들은 무엇일까요? (예: `python:3.9` 같은 큰 베이스 이미지 사용, `.dockerignore` 파일의 부재, 빌드 캐시나 불필요한 도구가 이미지에 남는 경우 등)
        -   **다단계 빌드(Multi-stage build)**란 무엇이며, 이 기법이 어떻게 더 작고 안전한 프로덕션 이미지를 만드는 데 도움이 될 수 있을까요?

2.  **환경 설정의 대혼란**
    -   **상황**: 한 개발자가 로컬 DB 접속 주소(`"localhost:5432"`)를 소스 코드에 하드코딩했습니다. 그의 컴퓨터에서는 앱이 완벽하게 동작했지만, 다른 DB를 사용하는 테스트 환경에 배포하자마자 앱이 실패했습니다.
    -   **토론**:
        -   소스 코드에 설정값(DB 주소, API 키 등)을 하드코딩하는 것이 왜 나쁜 습관일까요?
        -   개발, 테스트, 프로덕션 환경별로 달라지는 설정들을 관리하는 더 나은 방법들은 무엇이 있을까요? (예: 환경 변수, `.env` 파일, `docker-compose.yml`의 `env_file` 옵션 활용)

3.  **"죽었는데 살아있다"고 말하는 헬스 체크(Health Check)**
    -   **상황**: 우리 서비스의 헬스 체크는 단순히 컨테이너가 '실행 중'인지만 확인합니다. 어느 날 코드의 버그로 인해 `/predict` 엔드포인트가 계속 `500 Internal Server Error`를 반환하기 시작했지만, 컨테이너 자체는 정상적으로 실행 중이었습니다. 그 결과, 오케스트레이션 시스템(예: Kubernetes, Docker Swarm)은 서비스가 건강하다고 판단하여 계속해서 고장난 컨테이너로 트래픽을 보냈고, 사용자들은 장애를 겪었습니다.
    -   **토론**:
        -   단순히 '프로세스가 실행 중인지'만 확인하는 헬스 체크의 문제점은 무엇일까요?
        -   의미 있는 헬스 체크란 무엇을 확인해야 할까요? 단순히 `/health` 엔드포인트에서 `200 OK`를 반환하는 것으로 충분할까요, 아니면 DB 연결이나 모델 가용성 같은 핵심 의존성을 함께 확인해야 할까요?

4.  **무심코 `root`로 실행한 컨테이너**
    -   **상황**: 빠른 개발을 위해, `Dockerfile`에서 별다른 사용자 설정 없이 컨테이너를 `root` 사용자로 실행해왔습니다. 최근 실시한 보안 점검에서 이 부분이 주요 취약점으로 지적되었습니다.
    -   **토론**:
        -   컨테이너를 `root` 권한으로 실행하는 것이 왜 보안상 위험할까요? 만약 공격자가 우리 애플리케이션의 취약점을 통해 쉘(shell)을 획득한다면 어떤 일이 벌어질 수 있을까요?
        -   `Dockerfile`에서 `root`가 아닌 일반 사용자(non-root user)를 생성하고, 그 사용자로 애플리케이션을 실행하도록 만드는 구체적인 단계를 설명해보세요.

## 7. 되짚어보기 (Summary)

이번 주차에는 '1인 식당'을 '프랜차이즈 레스토랑'으로 확장하는 여정을 통해, 프로덕션 레벨의 AI 서비스를 구축하는 핵심 기술을 배웠습니다.

- **프로젝트 구조화**: `APIRouter`를 사용해 기능별로 코드를 분리하여, 유지보수와 협업이 용이한 프로젝트 구조를 설계했습니다.
- **Docker 컨테이너화**: '밀키트' 비유를 통해 Docker의 필요성을 이해하고, `Dockerfile`을 작성하여 우리 앱을 재현 가능한 이미지로 만들었습니다.
- **Docker Compose**: `docker-compose.yml`을 통해 여러 서비스를 명령어 하나로 관리하는 방법을 익혀, 복잡한 애플리케이션 배포의 기초를 다졌습니다.

---

## 8. 트러블슈팅 (Troubleshooting)

- **`docker-compose up` 실행 시 `Cannot connect to the Docker daemon...` 오류가 발생하나요?**
  - Docker 데몬(백그라운드 서비스)이 실행 중이 아니라는 의미입니다.
    - **Mac/Windows**: Docker Desktop 앱이 실행 중인지 확인하고, 아니라면 실행해주세요.
    - **Linux**: `sudo systemctl start docker` 명령으로 Docker 데몬을 시작하거나, `docker-compose` 앞에 `sudo`를 붙여 실행해보세요 (`sudo docker-compose up`).
- **`docker-compose up` 실행 시 `build path ... does not exist` 오류가 발생하나요?**
  - `docker-compose.yml`이 있는 위치에서 `Dockerfile`을 찾을 수 없다는 의미입니다. `docker-compose.yml`과 `Dockerfile`이 프로젝트 최상위 폴더에 함께 있는지 확인하세요.
- **`CMD ["uvicorn", "app.main:app", ...]` 실행 중 `ModuleNotFoundError: No module named 'app'` 오류가 발생하나요?**
  - Docker 컨테이너 안에서 `app`이라는 폴더/모듈을 찾지 못하는 경우입니다. `Dockerfile`의 `COPY ./app /code/app` 부분이 올바르게 작성되었는지, 그리고 실제 프로젝트 구조와 일치하는지 확인하세요. 작업 디렉토리(`WORKDIR`)가 `/code`로 설정되어 있으므로, 파이썬은 `/code/app`을 찾게 됩니다.
- **`/docs` 접속은 되는데, `Try it out` 실행 시 모델 파일을 찾지 못한다는 오류(`FileNotFoundError`)가 발생하나요?**
  - `Dockerfile`에 `COPY ./iris_model.pkl /code/iris_model.pkl` 라인을 추가하여 모델 파일을 이미지 안으로 복사했는지 확인하세요. 컨테이너는 호스트 머신(여러분의 컴퓨터)의 파일 시스템을 직접 볼 수 없으므로, 필요한 모든 파일을 이미지에 포함시켜야 합니다.
- **`docker-compose up` 후 `http://localhost:8000` 접속이 안 되나요?**
  - `docker-compose.yml`의 `ports` 설정이 ` "8000:8000" `으로 올바르게 되어 있는지 확인하세요. 이는 여러분 컴퓨터의 `8000`번 포트를 컨테이너의 `8000`번 포트로 연결하는 설정입니다.
  - `Dockerfile`의 `CMD` 명령어에 `"--host", "0.0.0.0"` 옵션이 포함되어 있는지 확인하세요. 컨테이너 내부에서 실행되는 서버는 `0.0.0.0` 주소로 바인딩되어야만 외부(호스트 머신)에서 접속할 수 있습니다. `127.0.0.1`은 컨테이너 자기 자신만을 의미합니다.

더 자세한 문제 해결 가이드나 다른 동료들이 겪은 문제에 대한 해결책이 궁금하다면, 아래 문서를 참고해주세요.

- **➡️ [Geumdo-Docs: TROUBLESHOOTING.md](../../../TROUBLESHOOTING.md)**

---

## 9. 더 깊이 알아보기 (Further Reading)
- [TestDriven.io: FastAPI and Docker](https://testdriven.io/blog/dockerizing-fastapi/): Docker를 사용한 FastAPI 배포에 대한 상세한 튜토리얼
- [Docker 공식 문서: Get Started](https://docs.docker.com/get-started/): Docker의 기본 개념부터 차근차근 배울 수 있는 최고의 자료
- [『코어 자바스크립트』 저자의 Docker/k8s 강의](https://www.inflearn.com/course/%EB%8F%84%EC%BB%A4-k8s-%ED%95%B5%EC%8B%AC%EA%B0%9C%EB%념-%EC%9D%B5%ED%9E%88%EA%B8%B0): Docker와 쿠버네티스의 핵심 원리를 비유를 통해 쉽게 설명하는 강의

---

**➡️ 다음 시간: [Part 10: 전문가로 가는 길](../10_expert_path/part_10_expert_path.md)** 