# Part 7: 실전형 AI API 서버 구축 --- ### 💡 지난 시간 복습 지난 [Part 6: AI 모델 서비스화 기초 (FastAPI)](part_6_model_serving_with_fastapi.md)에서는 FastAPI를 사용하여 `iris_model.pkl` 모델을 간단한 API 서버로 만드는 과정을 실습했습니다. 이를 통해 모델을 외부에 서비스하는 첫걸음을 내디뎠습니다. --- Part 6에서 만든 단일 파일 API는 개념을 이해하고 빠르게 프로토타입을 만드는 데는 유용했지만, 실제 프로덕션 환경에서 운영하기에는 부족한 점이 많습니다. 기능이 추가될수록 `main.py` 파일은 점점 비대해지고, 데이터베이스 로직, API 경로, 데이터 형식 정의 등이 뒤섞여 유지보수가 어려워집니다. 이 섹션에서는 실제 프로덕션 환경에서 안정적으로 운영될 수 있는 견고하고 확장 가능한 AI 서버를 구축하는 방법을 다룹니다. 이는 우리 회사의 표준 AI 서비스 아키텍처 가이드라인이기도 합니다. --- ## 1. 왜 체계적인 프로젝트 구조가 필요한가? - **유지보수성**: 기능별로 코드가 분리되어 있으면, 특정 기능을 수정하거나 디버깅할 때 관련 파일만 집중해서 볼 수 있어 작업 효율이 높아집니다. - **협업 효율성**: 여러 개발자가 프로젝트를 동시에 진행할 때, 각자 맡은 부분(라우터, DB 로직 등)을 독립적으로 개발할 수 있어 충돌을 최소화하고 생산성을 높일 수 있습니다. - **재사용성**: 데이터베이스 처리 로직(`crud.py`)이나 데이터 유효성 검사 모델(`schemas.py`) 등을 분리해두면, 다른 기능이나 프로젝트에서도 해당 코드를 쉽게 가져다 쓸 수 있습니다. - **확장성**: 새로운 기능(예: 사용자 관리)을 추가할 때, 기존 코드를 건드리지 않고 새로운 라우터와 관련 파일들을 추가하는 것만으로 쉽게 시스템을 확장할 수 있습니다. --- ## 2. 표준 프로젝트 구조 (Standard Project Structure) FastAPI 프로젝트는 일반적으로 다음과 같은 기능 중심의 구조를 권장합니다. 아래 구조에 따라 예제 코드를 하나씩 만들어 보겠습니다. ``` /project ├── app/ │ ├── __init__.py │ ├── main.py # ✅ FastAPI 앱 생성 및 전역 설정, 라우터 통합 │ ├── database.py # ✅ 데이터베이스 연결 설정 (SQLAlchemy) │ ├── models.py # ✅ DB 테이블 모델 정의 (SQLAlchemy) │ ├── schemas.py # ✅ 데이터 유효성 검사 모델 정의 (Pydantic) │ ├── crud.py # ✅ 데이터베이스 처리 로직 (CRUD: Create, Read, Update, Delete) │ ├── settings.py # ✅ 환경변수 및 설정 관리 (Pydantic-Settings) │ └── routers/ │ ├── __init__.py │ └── predictions.py # ✅ '예측' 기능 관련 API 라우터 ├── requirements.txt # ✅ 프로젝트 의존성 목록 └── iris_model.pkl # Part 5에서 생성한 모델 파일 ``` 아래 다이어그램은 위 구조에서 `POST /predictions/` 와 같은 API 요청이 들어왔을 때, 각 파일과 모듈이 어떻게 상호작용하는지 보여주는 흐름도입니다. ```mermaid sequenceDiagram participant Client as 👨‍💻 클라이언트 participant Main as FastAPI (main.py) participant Router as ➡️ 라우터 (predictions.py) participant Model as 🤖 AI 모델 (*.pkl) participant CRUD as 📝 CRUD (crud.py) participant DB as 🗄️ 데이터베이스 (models.py) Client->>+Main: 1. POST /predictions/ 요청 Main->>+Router: 2. 요청 라우팅 Router->>+Model: 3. model.predict(입력 데이터) Model-->>-Router: 4. 예측 결과 반환 Router->>+CRUD: 5. crud.create_prediction_log(결과) CRUD->>+DB: 6. 데이터베이스에 로그 저장 (INSERT) DB-->>-CRUD: 7. 저장된 객체 반환 CRUD-->>-Router: 8. 생성된 로그 객체 반환 Router-->>-Main: 9. 최종 응답 생성 (Pydantic 스키마) Main-->>-Client: 10. 200 OK 응답 (JSON) ``` 이처럼 각 컴포넌트가 명확한 책임을 가지고 분리되어 있어, 코드를 이해하고 유지보수하기가 훨씬 용이해집니다. ### 2.0. Git 버전 관리 설정 (`.gitignore`) 프로젝트를 Git으로 관리할 때는, 모든 파일을 버전 관리 시스템에 포함시킬 필요가 없습니다. 가상환경 폴더(`.venv`), 파이썬 캐시 파일(`__pycache__/`), 그리고 특히 용량이 큰 데이터 파일이나 모델 파일 (`*.pkl`, `*.csv`) 등은 제외하는 것이 좋습니다. 이를 위해 프로젝트 최상위 디렉토리에 `.gitignore` 파일을 생성하고 아래와 같이 작성합니다. ```gitignore # .gitignore # Python __pycache__/ *.pyc .venv/ /venv/ env/ # Data and Models *.csv *.pkl *.joblib data/ models/ # IDE/Editor specific .vscode/ .idea/ ``` > 💡 **대용량 파일은 어떻게 관리하나요?** > > Git은 소스 코드의 '변경 이력'을 관리하는 데 최적화되어 있어, 용량이 큰 데이터셋이나 AI 모델 파일을 직접 저장하면 리포지토리 크기가 급격히 커지고 성능이 저하됩니다. 이런 파일들은 **DVC (Data Version Control)** 와 같은 도구를 사용하여 Git과 별도로 버전을 관리하고, 실제 파일은 클라우드 스토리지(S3, Google Cloud Storage 등)에 저장하는 것이 표준적인 MLOps 방식입니다. (Part 8에서 자세히 다룹니다) ### 2.1. 의존성 설치 (`requirements.txt`) 프로젝트에 필요한 라이브러리 목록입니다. `SQLAlchemy`는 데이터베이스와 상호작용하기 위한 ORM(Object-Relational Mapping) 라이브러리입니다. ```text # requirements.txt fastapi uvicorn[standard] scikit-learn joblib numpy SQLAlchemy pydantic-settings ``` > 터미널에서 `pip install -r requirements.txt` 명령으로 한 번에 설치할 수 있습니다. > 💡 **Tip: 재현 가능한 환경을 위한 의존성 버전 명시** > `requirements.txt` 파일을 작성할 때는 라이브러리 이름만 적는 것보다, `fastapi==0.104.1` 처럼 `==`을 사용하여 정확한 버전을 명시하는 것이 매우 중요합니다. > > - **왜 중요한가요?**: 라이브러리는 계속 업데이트되며, 새로운 버전에서 기능이 변경되거나 삭제될 수 있습니다. 버전을 명시하지 않으면, 나중에 다른 환경에서 `pip install`을 실행했을 때 최신 버전이 설치되어 코드가 예기치 않게 오작동할 수 있습니다. > - **어떻게 버전을 확인하나요?**: 현재 가상환경에 설치된 라이브러리와 그 버전 목록은 `pip freeze` 명령으로 확인할 수 있습니다. 이 결과를 복사하여 `requirements.txt` 파일에 붙여넣으면 됩니다. > ```bash > # 현재 환경의 패키지 목록과 버전을 확인 > pip freeze > requirements.txt > ``` > 이렇게 버전을 고정하면, 시간이 지나거나 다른 동료가 프로젝트를 설치하더라도 항상 동일한 환경에서 코드를 실행할 수 있어 **재현 가능성**이 보장됩니다. ### 2.2. 환경변수 기반 설정 관리 (`app/settings.py`) 소스 코드에 DB 접속 정보 같은 민감한 데이터를 하드코딩하는 대신, 환경 변수로부터 설정을 읽어오도록 관리합니다. `pydantic-settings` 라이브러리를 사용하면 간단하게 구현할 수 있습니다. ```python # app/settings.py from pydantic_settings import BaseSettings class Settings(BaseSettings): # .env 파일 또는 실제 환경변수에서 아래 변수들을 읽어옵니다. # 만약 환경변수가 설정되어 있지 않으면, 오른쪽에 명시된 기본값이 사용됩니다. DATABASE_URL: str = "sqlite:///./test.db" class Config: # .env 파일을 읽도록 설정 (프로젝트 루트에 .env 파일 생성 가능) env_file = ".env" # 설정 객체 인스턴스화 settings = Settings() ``` ### 2.3. 데이터베이스 설정 (`app/database.py`) 데이터베이스 연결을 설정하고, DB 세션을 관리하는 코드를 정의합니다. 여기서는 간단하게 파일 기반 데이터베이스인 `SQLite`를 사용합니다. ```python # app/database.py from sqlalchemy import create_engine from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from .settings import settings # 설정 파일 임포트 # 설정 파일(settings.py)에서 데이터베이스 URL을 가져옴 SQLALCHEMY_DATABASE_URL = settings.DATABASE_URL # 데이터베이스 엔진 생성 # connect_args는 SQLite를 사용할 때만 필요한 옵션입니다. engine = create_engine( SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} ) # 데이터베이스 세션 생성을 위한 클래스 SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) # DB 모델(테이블)을 정의할 때 사용할 베이스 클래스 Base = declarative_base() ``` ### 2.4. DB 테이블 모델 정의 (`app/models.py`) `SQLAlchemy`를 사용하여 데이터베이스 테이블을 파이썬 클래스로 정의합니다. 이 클래스는 실제 DB 테이블의 구조(컬럼, 타입 등)를 나타냅니다. ```python # app/models.py from sqlalchemy import Column, Integer, String, DateTime from .database import Base import datetime class PredictionLog(Base): __tablename__ = "prediction_logs" # DB에 생성될 테이블 이름 id = Column(Integer, primary_key=True, index=True) # 자동 증가하는 기본 키 input_features = Column(String) # 입력된 특징 값 (JSON 문자열로 저장) predicted_class_name = Column(String, index=True) # 예측된 품종 이름 created_at = Column(DateTime, default=datetime.datetime.utcnow) # 생성 시각 ``` ### 2.5. API 데이터 형식 정의 (`app/schemas.py`) `Pydantic` 모델을 사용하여 API의 요청(Request)과 응답(Response) 데이터의 형식을 명확하게 정의하고, 자동으로 유효성을 검사합니다. `models.py`와 분리하여 DB 모델과 API 모델의 역할을 명확히 나눕니다. ```python # app/schemas.py from pydantic import BaseModel from typing import List import datetime # --- 요청(Request) 스키마 --- # POST /predictions/ 요청의 Body에 포함될 데이터 형식 class PredictionCreate(BaseModel): features: List[float] # --- 응답(Response) 스키마 --- # GET /predictions/{id} 또는 POST /predictions/ 응답으로 반환될 데이터 형식 class PredictionResponse(BaseModel): id: int input_features: str predicted_class_name: str created_at: datetime.datetime class Config: orm_mode = True # SQLAlchemy 모델 객체를 Pydantic 모델로 자동 변환 허용 ``` ### 2.6. 데이터베이스 처리 함수 (`app/crud.py`) 실제 데이터베이스와 상호작용하는 로직(생성, 조회, 수정, 삭제)을 함수 단위로 분리하여 재사용성을 높입니다. ```python # app/crud.py from sqlalchemy.orm import Session from . import models, schemas import json def get_prediction_log(db: Session, log_id: int): """ID로 특정 예측 로그를 조회합니다.""" return db.query(models.PredictionLog).filter(models.PredictionLog.id == log_id).first() def get_all_prediction_logs(db: Session, skip: int = 0, limit: int = 100): """모든 예측 로그를 조회합니다 (페이지네이션 적용).""" return db.query(models.PredictionLog).offset(skip).limit(limit).all() def create_prediction_log(db: Session, request: schemas.PredictionCreate, predicted_class_name: str): """새로운 예측 로그를 DB에 저장합니다.""" # 리스트를 JSON 형태의 문자열로 변환하여 저장 features_str = json.dumps(request.features) db_log = models.PredictionLog( input_features=features_str, predicted_class_name=predicted_class_name ) db.add(db_log) db.commit() db.refresh(db_log) # DB에 저장된 내용을 다시 읽어와서 파이썬 객체에 반영 return db_log ``` ### 2.7. API 라우터 분리 (`app/routers/predictions.py`) `APIRouter`를 사용하면 기능별로 API 코드를 독립된 파일로 관리할 수 있어 `main.py`가 비대해지는 것을 방지합니다. ```python # app/routers/predictions.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List from .. import crud, schemas, database, models import joblib import numpy as np # APIRouter 인스턴스 생성 router = APIRouter( prefix="/predictions", # 이 라우터의 모든 경로는 /predictions 로 시작 tags=["Predictions"] # API 문서에서 "Predictions" 그룹으로 묶음 ) # --- 의존성 주입 (Dependency Injection) --- # DB 세션을 가져오는 함수 (요청이 들어올 때마다 세션을 생성하고, 끝나면 닫음) def get_db(): db = database.SessionLocal() try: yield db finally: db.close() # 모델을 로드하는 함수 (간단한 예제, 실제로는 캐싱 전략 필요) def get_model(): model = joblib.load("iris_model.pkl") return model # --- 왜 전역 변수 대신 Depends를 사용할까? --- # 전역 변수로 DB 세션이나 모델을 관리할 수도 있지만, Depends를 사용하면 다음과 같은 강력한 이점을 얻습니다. # 1. 테스트 용이성: 테스트 코드에서 `app.dependency_overrides`를 사용해 `get_db`나 `get_model`을 # 가짜(mock) 함수로 쉽게 교체할 수 있습니다. 예를 들어, 실제 DB에 접속하는 대신 테스트용 인메모리 DB를 사용하도록 # 만들어 단위 테스트를 격리된 환경에서 수행할 수 있습니다. # 2. 명시적인 의존성: 각 API 엔드포인트가 어떤 의존성을 필요로 하는지 함수 시그니처에 명확하게 드러나므로 # 코드의 가독성과 유지보수성이 향상됩니다. # 3. 재사용성: `get_db`와 같은 의존성 주입 함수는 다른 라우터에서도 그대로 재사용할 수 있습니다. # --- --- > 🚫 **Common Pitfall: 뚱뚱한 라우터 (Fat Router)** > `predictions.py`의 `create_prediction_and_log` 함수를 보면, 모델 예측 로직(`np.array`, `model.predict` 등)이 라우터 함수 내부에 직접 포함되어 있습니다. 간단한 경우에는 괜찮지만, 비즈니스 로직이 복잡해질수록 라우터 파일이 점점 비대해지고(뚱뚱해지고), 테스트하기 어려워지며, 재사용성이 떨어지는 문제가 발생합니다. > > - **문제점**: > - **단일 책임 원칙(SRP) 위배**: 라우터는 '요청을 받고 응답을 반환하는' 역할에 집중해야 하는데, 비즈니스 로직까지 처리하게 됩니다. > - **테스트의 어려움**: 비즈니스 로직만 따로 테스트하고 싶은데, FastAPI의 `Depends`와 HTTP 요청/응답 구조에 묶여 있어 순수한 단위 테스트가 어렵습니다. > - **재사용성 저하**: 다른 API나 백그라운드 작업에서 동일한 예측 로직을 사용하고 싶을 때, 코드를 복사-붙여넣기 해야 합니다. > - **해결책: 서비스 계층(Service Layer) 분리** > - `app/services.py` 와 같은 파일을 만들고, 실제 비즈니스 로직(모델 예측, 데이터 가공 등)을 별도의 함수나 클래스로 분리합니다. > - 라우터는 이 서비스 함수를 호출하여 결과를 받아오기만 하는, 가볍고(Thin) 깔끔한 상태를 유지합니다. > - 이렇게 하면 서비스 로직을 API와 독립적으로 테스트하고 재사용할 수 있게 됩니다. (Part 8에서 더 자세히 다룹니다.) @router.post("/", response_model=schemas.PredictionResponse) def create_prediction_and_log( request: schemas.PredictionCreate, db: Session = Depends(get_db), model = Depends(get_model) ): """ 새로운 붓꽃 품종 예측을 수행하고, 그 결과를 데이터베이스에 기록합니다. """ # 1. 모델 예측 로직 수행 if len(request.features) != 4: raise HTTPException(status_code=400, detail="입력 피처는 4개여야 합니다.") model_input = np.array(request.features).reshape(1, -1) prediction_idx = model.predict(model_input)[0] iris_target_names = {0: 'setosa', 1: 'versicolor', 2: 'virginica'} class_name = iris_target_names.get(int(prediction_idx), "Unknown") # 2. CRUD 함수를 이용해 결과 DB에 저장 return crud.create_prediction_log(db=db, request=request, predicted_class_name=class_name) @router.get("/", response_model=List[schemas.PredictionResponse]) def read_all_prediction_logs(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)): """ 저장된 모든 예측 로그를 조회합니다. """ logs = crud.get_all_prediction_logs(db, skip=skip, limit=limit) return logs @router.post("/dummy-io-task", summary="Async I/O Task Example") async def dummy_io_task(): """ 실제 I/O 작업(예: 외부 API 호출, DB 쿼리)을 시뮬레이션하는 비동기 엔드포인트입니다. `await asyncio.sleep(1)`은 I/O 작업이 1초간 진행되는 동안 서버가 다른 요청을 처리할 수 있도록 CPU를 반환합니다. 이를 통해 API 서버의 동시 처리 성능이 향상됩니다. """ import asyncio await asyncio.sleep(1) # 1초간 비동기 대기 return {"message": "Dummy I/O task completed after 1 second"} ``` ### 2.8. 메인 앱에 라우터 통합 (`app/main.py`) 마지막으로, 생성한 라우터를 메인 FastAPI 앱에 포함시키고, 애플리케이션 시작 시 DB 테이블을 생성하도록 설정합니다. ```python # app/main.py from fastapi import FastAPI from .database import engine from . import models from .routers import predictions # 애플리케이션 시작 시 DB에 필요한 테이블들을 생성 models.Base.metadata.create_all(bind=engine) app = FastAPI( title="Production-Ready AI API", description="붓꽃 예측 모델을 서빙하고, 예측 결과를 데이터베이스에 기록하는 실전형 API입니다.", version="1.0.0" ) # predictions 라우터를 메인 앱에 포함 app.include_router(predictions.router) @app.get("/", tags=["Root"]) def root(): return {"message": "Production-Ready AI API에 오신 것을 환영합니다!"} ``` ### 2.9. 실행 이제 프로젝트의 최상위 디렉토리( `app` 폴더가 있는 곳)에서 아래 명령어로 서버를 실행합니다. ```bash uvicorn app.main:app --reload ``` 웹 브라우저에서 http://127.0.0.1:8000/docs 로 접속하면, 예측을 수행하고 DB에 로그를 남기는(`POST /predictions/`) API와 저장된 모든 로그를 조회하는 (`GET /predictions/`) API가 추가된 것을 확인할 수 있습니다. --- ## 3. 주요 프로젝트 확장: 주택 가격 예측 API 지금까지 만든 붓꽃 **분류(Classification)** API를 확장하여, 이번에는 주택 가격을 예측하는 **회귀(Regression)** API를 추가해 보겠습니다. 이를 통해 우리 API 서버가 여러 종류의 모델을 동시에 서비스할 수 있도록 구조를 확장하는 방법을 배웁니다. ### 3.1. 회귀(Regression) 모델 준비 먼저, 주택 가격 예측을 위한 회귀 모델이 필요합니다. 여기서는 Scikit-learn의 캘리포니아 주택 가격 데이터셋을 사용합니다. > 💡 **보스턴 주택 가격 데이터셋에 관하여** > 이전에는 보스턴 주택 가격 데이터셋이 많이 사용되었으나, 데이터에 인종 차별적인 편향이 포함되어 있어 Scikit-learn 1.2 버전부터는 제거되었습니다. 우리는 대체재로 캘리포니아 주택 가격 데이터셋을 사용합니다. 아래 파이썬 스크립트를 `create_california_housing_model.py`와 같은 파일로 저장하고 실행하면, 데이터셋을 내려받아 `RandomForestRegressor` 모델을 훈련시킨 후 `california_housing_model.pkl` 파일로 저장해 줍니다. ```python # create_california_housing_model.py import pandas as pd from sklearn.datasets import fetch_california_housing from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor import joblib def create_model(): """ 캘리포니아 주택 가격 데이터셋을 로드하고, RandomForest 회귀 모델을 훈련시킨 후 'california_housing_model.pkl' 파일로 저장합니다. """ # 1. 데이터 로드 housing = fetch_california_housing() X = pd.DataFrame(housing.data, columns=housing.feature_names) y = pd.Series(housing.target, name='MedHouseVal') # 2. 데이터 분할 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42) # 3. 모델 훈련 model = RandomForestRegressor(n_estimators=100, random_state=42) model.fit(X_train, y_train) print(f"모델 훈련 완료. 테스트 점수: {model.score(X_test, y_test):.4f}") # 4. 모델 저장 joblib.dump(model, 'california_housing_model.pkl') print("모델이 'california_housing_model.pkl' 파일로 저장되었습니다.") if __name__ == "__main__": create_model() ``` 이 스크립트를 실행하여 `california_housing_model.pkl` 파일을 프로젝트 루트에 준비합니다. ### 3.2. 확장된 프로젝트 구조 새로운 회귀 모델 API를 추가하면 프로젝트 구조는 다음과 같이 확장됩니다. ``` /project ├── app/ │ ├── __init__.py │ ├── main.py │ ├── database.py │ ├── models.py # ✅ RegressionLog 테이블 모델 추가 │ ├── schemas.py # ✅ 주택 가격 예측용 스키마 추가 │ ├── crud.py # ✅ 주택 가격 예측 로그 CRUD 함수 추가 │ ├── settings.py │ └── routers/ │ ├── __init__.py │ ├── predictions.py │ └── housing.py # ✅ 새로 추가된 주택 가격 예측 라우터 ├── requirements.txt ├── iris_model.pkl └── california_housing_model.pkl # ✅ 새로 추가된 회귀 모델 ``` ### 3.3. DB 모델 및 스키마 확장 회귀 예측 결과(숫자 값)를 저장하기 위해 새로운 DB 테이블 모델과 API 스키마를 추가합니다. #### `app/models.py` 수정 `RegressionLog` 테이블 모델을 추가합니다. 분류 모델의 `predicted_class_name` 대신 `predicted_value` (Float)를 저장합니다. ```python // ... existing code ... import datetime from sqlalchemy import Column, Integer, String, DateTime, Float # Float 추가 class PredictionLog(Base): // ... existing code ... predicted_class_name = Column(String, index=True) # 예측된 품종 이름 created_at = Column(DateTime, default=datetime.datetime.utcnow) # 생성 시각 # 새로 추가 class RegressionLog(Base): __tablename__ = "regression_logs" id = Column(Integer, primary_key=True, index=True) input_features = Column(String) predicted_value = Column(Float) # 회귀 예측 결과 (숫자) created_at = Column(DateTime, default=datetime.datetime.utcnow) ``` #### `app/schemas.py` 수정 주택 가격 예측 API를 위한 요청/응답 스키마를 추가합니다. ```python // ... existing code ... class PredictionResponse(BaseModel): // ... existing code ... class Config: orm_mode = True # SQLAlchemy 모델 객체를 Pydantic 모델로 자동 변환 허용 # --- 주택 가격 예측용 스키마 (새로 추가) --- class HousingFeatures(BaseModel): features: List[float] # 캘리포니아 주택 데이터셋은 8개의 피처를 가집니다. class RegressionResponse(BaseModel): id: int input_features: str predicted_value: float created_at: datetime.datetime class Config: orm_mode = True ``` ### 3.4. CRUD 로직 추가 (`app/crud.py`) 새로 정의한 `RegressionLog` 모델을 다루는 CRUD 함수를 `app/crud.py`에 추가합니다. ```python // ... existing code ... def create_prediction_log(db: Session, request: schemas.PredictionCreate, predicted_class_name: str): // ... existing code ... db.refresh(db_log) # DB에 저장된 내용을 다시 읽어와서 파이썬 객체에 반영 return db_log # 새로 추가 def create_regression_log(db: Session, request: schemas.HousingFeatures, predicted_value: float): """새로운 회귀 예측 로그를 DB에 저장합니다.""" features_str = json.dumps(request.features) db_log = models.RegressionLog( input_features=features_str, predicted_value=predicted_value ) db.add(db_log) db.commit() db.refresh(db_log) return db_log ``` ### 3.5. 신규 API 라우터 작성 (`app/routers/housing.py`) `app/routers/` 디렉토리 안에 `housing.py` 파일을 새로 생성하고, 회귀 모델을 로드하여 예측을 수행하는 API 엔드포인트를 작성합니다. ```python # app/routers/housing.py from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List from .. import crud, schemas, database, models import joblib import numpy as np router = APIRouter( prefix="/housing", tags=["Housing Predictions"] ) # 의존성 주입: DB 세션 def get_db(): db = database.SessionLocal() try: yield db finally: db.close() # 의존성 주입: 회귀 모델 로드 def get_housing_model(): model = joblib.load("california_housing_model.pkl") return model @router.post("/predict", response_model=schemas.RegressionResponse) def create_housing_prediction( request: schemas.HousingFeatures, db: Session = Depends(get_db), model = Depends(get_housing_model) ): """ 새로운 주택 가격 예측을 수행하고, 그 결과를 데이터베이스에 기록합니다. """ # 캘리포니아 주택 데이터셋의 피처 개수는 8개입니다. if len(request.features) != 8: raise HTTPException(status_code=400, detail="입력 피처는 8개여야 합니다.") model_input = np.array(request.features).reshape(1, -1) predicted_value = model.predict(model_input)[0] # CRUD 함수를 이용해 결과 DB에 저장 return crud.create_regression_log(db=db, request=request, predicted_value=float(predicted_value)) ``` ### 3.6. 메인 앱에 라우터 통합 (`app/main.py`) 마지막으로 `app/main.py`를 수정하여 새로 만든 `housing` 라우터를 앱에 포함시킵니다. ```python # app/main.py from fastapi import FastAPI from .database import engine from . import models from .routers import predictions, housing # housing 라우터 임포트 # 애플리케이션 시작 시 DB에 필요한 테이블들을 생성 models.Base.metadata.create_all(bind=engine) app = FastAPI( title="Production-Ready AI API", description="붓꽃 분류 및 주택 가격 예측 모델을 서빙하는 API입니다.", # 설명 업데이트 version="1.1.0" # 버전 업데이트 ) # 각 라우터를 메인 앱에 포함 app.include_router(predictions.router) app.include_router(housing.router) # housing 라우터 추가 @app.get("/", tags=["Root"]) def root(): return {"message": "Production-Ready AI API에 오신 것을 환영합니다!"} ``` 이제 서버를 다시 실행(`uvicorn app.main:app --reload`)하고 http://127.0.0.1:8000/docs 에 접속하면, 기존의 `/predictions` API와 더불어 새로운 `/housing` API가 추가된 것을 확인할 수 있습니다. --- ## 4. GitHub 스타터 키트 복잡한 프로젝트 구조를 처음부터 따라 만드는 것은 어려울 수 있습니다. 여러분의 학습을 돕기 위해, Part 7의 완성된 코드를 포함하는 GitHub 리포지토리를 스타터 키트로 제공합니다. > 🔗 **Part 7 스타터 키트 리포지토리**: [여기에 GitHub 리포지토리 링크가 제공될 예정입니다.] 이 리포지토리를 `git clone`하여 바로 코드를 실행해보고, 구조를 분석하며 직접 기능을 수정해보세요. --- ### ✅ 정리 및 다음 단계 이번 파트에서는 단일 파일로 구성되었던 간단한 API 서버를, 실제 프로덕션 환경에서도 통용될 수 있는 **체계적인 프로젝트 구조**로 리팩토링했습니다. - **관심사 분리**: 라우터, DB 모델, 스키마, CRUD 로직을 각각의 파일로 분리하여 유지보수성과 확장성을 높였습니다. - **설정 관리**: 민감한 정보를 환경 변수로 관리하여 보안을 강화했습니다. - **데이터베이스 연동**: 예측 결과를 `SQLite` 데이터베이스에 기록하고 조회하는 기능을 추가했습니다. 이제 우리는 견고하고 확장 가능한 AI API 서버를 구축할 수 있는 역량을 갖추게 되었습니다. **➡️ 다음 시간: [Part 8: AI 전문가로의 성장 (심화 과정)](part_8_expert_path.md)** 지금까지 AI 서비스 개발의 '기초'를 탄탄히 다졌습니다. 마지막 파트에서는 이 기반 위에서 한 단계 더 도약하여, **재현 가능하고 신뢰할 수 있는 AI 시스템**을 구축하는 전문가로 성장하기 위한 로드맵을 제시합니다. **Docker**를 이용한 컨테이너화, **CI/CD**를 통한 배포 자동화, 그리고 **MLOps**와 같은 고급 주제들을 통해 진정한 AI 시스템 아키텍트로 나아가는 길을 함께 살펴보겠습니다.