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.py의startup이벤트가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"
🟢 예시 답안 (코드잇 제공)
.
