16-(2) FastAPI를 활용하여 AI 모델을 통합한 웹 API를 구현할 때, 기능별로 나눠서 구성하는 것이 바람직합니다. 전체적인 API 서버 코드 구조는 어떻게 구성되면 좋을지 자유롭게 작성해보세요. |        Serendipity                                                                                                                                                                                                                                                                                                                                                                                   
Post

16-(2) FastAPI를 활용하여 AI 모델을 통합한 웹 API를 구현할 때, 기능별로 나눠서 구성하는 것이 바람직합니다. 전체적인 API 서버 코드 구조는 어떻게 구성되면 좋을지 자유롭게 작성해보세요.

16-(2) FastAPI를 활용하여 AI 모델을 통합한 웹 API를 구현할 때, 기능별로 나눠서 구성하는 것이 바람직합니다. 전체적인 API 서버 코드 구조는 어떻게 구성되면 좋을지 자유롭게 작성해보세요.

🟢 Intro

Streamlit이 프로토타이핑과 데모에 강점이 있다면, FastAPI는 안정적이고 확장 가능한 AI 모델 서빙에 압도적으로 유리하다. ‘기능별 분리’ (관심사 분리, Separation of Concerns)는 유지보수와 협업을 위해 필수적이다.

대규모 애플리케이션에서 사용하는 견고한 디렉터리 구조를 AI 모델 API에 맞게 변형해서 제안해보았다.


⚪ AI 모델 서빙을 위한 FastAPI 프로젝트 구조

핵심은 라우터(API 경로), 스키마(데이터 검증), **서비스(핵심 로직)**를 분리하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
my_ai_api/
├── app/
│   ├── __init__.py
│   ├── main.py             # 1. FastAPI 앱 생성 및 전체 설정
│   │
│   ├── api/
│   │   ├── __init__.py
│   │   ├── router.py         # 2. API 라우터 (엔드포인트)
│   │   └── schemas.py        # 3. Pydantic 데이터 스키마 (검증)
│   │
│   ├── core/
│   │   ├── __init__.py
│   │   └── config.py         # 4. 설정 (환경 변수, 모델 경로 등)
│   │
│   └── services/
│       ├── __init__.py
│       └── model_service.py  # 5. AI 모델 로드 및 추론 로직 (핵심)
│
├── models/                   # 6. 훈련된 모델 파일 (예: .pth, .h5, .onnx)
│   └── best_model.pth
│
├── .env                      # 7. 환경 변수 파일 (config.py가 읽어 감)
├── requirements.txt
└── run.py                    # (선택) uvicorn 서버 실행 스크립트

📜 각 파일 및 디렉터리의 역할

각 구성 요소가 어떤 ‘기능’을 담당하는지 설명해보면.

1. app/main.py : 앱의 진입점 (Conductor)

가장 상위 레벨의 파일이야. FastAPI 앱 인스턴스를 생성하고, 미들웨어(CORS 등)를 설정하고, api/router.py에 정의된 라우터들을 ‘포함(include_router)’시킨다.

가장 중요한 역할: 앱이 시작(startup)될 때 AI 모델을 단 한 번만 메모리에 로드하도록 이벤트 핸들러를 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# app/main.py
from fastapi import FastAPI
from app.api.router import api_router
from app.services.model_service import load_model_on_startup, close_model_on_shutdown

app = FastAPI(title="My AI Model API")

# 앱 시작 시 모델 로드
@app.on_event("startup")
async def startup_event():
    load_model_on_startup()

# 앱 종료 시 (선택 사항)
@app.on_event("shutdown")
async def shutdown_event():
    close_model_on_shutdown()

# /api/v1 과 같은 프리픽스로 라우터 포함
app.include_router(api_router, prefix="/api/v1")

@app.get("/")
def read_root():
    return {"message": "AI Model API is running!"}

2. app/api/router.py : API 엔드포인트 (Controller)

사용자가 실제로 접속하는 URL 경로 (/predict, /health_check 등)를 정의한다.

이 파일은 **‘무엇을 받고 무엇을 반환할지’**에만 집중한다. 실제 복잡한 로직은 model_service.py에 위임해. FastAPI의 **의존성 주입(Dependency Injection)**을 사용하면 깔끔해진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# app/api/router.py
from fastapi import APIRouter, Depends, UploadFile, File
from app.api.schemas import PredictionRequest, PredictionResponse
from app.services.model_service import get_model_service, ModelService

api_router = APIRouter()

@api_router.post("/predict", response_model=PredictionResponse)
async def predict(
    request_data: PredictionRequest,  # JSON 입력을 받을 때
    # file: UploadFile = File(...),   # 이미지 파일 등을 받을 때
    model_service: ModelService = Depends(get_model_service) # 서비스 주입
):
    """
    모델 추론을 수행합니다.
    """
    # 실제 로직은 서비스 레이어로 넘김
    prediction = model_service.predict(request_data.input_data)
    
    return PredictionResponse(prediction=prediction)

@api_router.get("/health")
def health_check():
    return {"status": "ok"}

3. app/api/schemas.py : 데이터 계약서 (Pydantic Models)

API가 받을 **입력(Request)**과 반환할 **출력(Response)**의 데이터 형식(타입, 필수 필드 등)을 Pydantic으로 엄격하게 정의한다.

이게 FastAPI의 핵심이다. 데이터 유효성 검사를 자동으로 해주고, API 문서를 기가 막히게 만들어 준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# app/api/schemas.py
from pydantic import BaseModel, Field
from typing import List, Any

# 예시: 텍스트 입력을 받는 경우
class PredictionRequest(BaseModel):
    input_data: str = Field(..., example="이 모델은 무엇을 예측하나요?")

# 예시: 딥러닝 모델이 이미지 경로를 받는 경우
# class PredictionRequest(BaseModel):
#     image_path: str = Field(..., example="/path/to/image.jpg")

class PredictionResponse(BaseModel):
    prediction: Any # 모델의 출력에 맞게 (e.g., str, float, List[float])

4. app/services/model_service.py : 핵심 비즈니스 로직 (The Brain)

모든 AI 관련 로직은 여기에 있어. 모델을 로드하고, 입력 데이터를 전처리(preprocessing)하고, model.predict()를 호출하고, 결과를 후처리(postprocessing)하는 실제 작업 공간이다.

  • main.pystartup 이벤트가 load_model_on_startup을 호출한다.
  • router.py의 엔드포인트는 predict 함수를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# app/services/model_service.py
import torch
from your_model_arch import YourModel # 실제 모델 아키텍처
from app.core.config import settings # 설정 파일에서 모델 경로 가져오기

# 모델을 담을 전역 변수 (또는 클래스 속성)
# 앱이 실행되는 동안 이 변수에 모델이 할당됨
model_instance = None

class ModelService:
    def __init__(self, model):
        if model is None:
            raise ValueError("Model is not loaded!")
        self.model = model

    def _preprocess(self, data):
        # (예시) 텍스트 토큰화 또는 이미지 텐서 변환
        # tensor = ...
        # return tensor
        return data # 임시

    def _postprocess(self, output):
        # (예시) 텐서를 리스트나 클래스 이름으로 변환
        # result = ...
        # return result
        return output # 임시

    def predict(self, input_data: str):
        # 1. 전처리
        processed_data = self._preprocess(input_data)
        
        # 2. 추론
        # with torch.no_grad():
        #     output = self.model(processed_data)
        
        # (임시 예시 로직)
        output = f"Processed: {processed_data}"

        # 3. 후처리
        prediction_result = self._postprocess(output)
        
        return prediction_result

def load_model_on_startup():
    """앱 시작 시 모델을 로드하여 전역 변수에 할당"""
    global model_instance
    try:
        # settings.MODEL_PATH는 .env 파일에서 읽어 옴
        # model = YourModel()
        # model.load_state_dict(torch.load(settings.MODEL_PATH))
        # model.eval()
        
        # (임시 예시) 실제 모델 대신 더미 객체 할당
        model_instance = "MyLoadedAIModel" # 실제로는 model 객체
        print("Model loaded successfully!")
        
    except Exception as e:
        print(f"Error loading model: {e}")
        model_instance = None

def close_model_on_shutdown():
    """(선택) 앱 종료 시 모델 관련 리소스 해제"""
    global model_instance
    model_instance = None
    print("Model resources released.")


def get_model_service() -> ModelService:
    """
    라우터에서 호출할 의존성 주입 함수.
    로드된 모델 인스턴스를 서비스 클래스에 주입하여 반환.
    """
    if model_instance is None:
        raise HTTPException(status_code=503, detail="Model is not loaded or failed to load.")
    return ModelService(model=model_instance)

5. app/core/config.py : 설정 관리

모델 파일의 경로, 데이터베이스 주소, API 키 등 하드코딩하면 안 되는 값들을 관리해. Pydantic의 BaseSettings를 사용하면 .env 파일에서 자동으로 환경 변수를 읽어와서 편하다.

1
2
3
4
5
6
7
8
9
10
11
# app/core/config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    MODEL_PATH: str = "models/best_model.pth"
    API_V1_STR: str = "/api/v1"

    class Config:
        env_file = ".env" # .env 파일을 읽도록 설정

settings = Settings()

6. models/ 디렉터리 및 .env 파일

  • models/: git lfs로 관리되거나 CI/CD 파이프라인을 통해 S3 같은 곳에서 다운로드 받아야 하는 실제 모델 아티팩트(.pth 등)를 저장한다.
  • .env: config.py가 읽어갈 민감 정보나 경로를 저장. (이 파일은 .gitignore에 꼭 추가해야 한다!)
1
2
# .env
MODEL_PATH="models/my_final_model_v1.2.pth"

🟢 예시 답안 (코드잇 제공)

.

This post is licensed under CC BY 4.0 by the author.

© 2025 Soohyun Jeon ⭐

🌱 Mostly to remember, sometimes to understand.