소프트웨어 개발/백엔드

📚[FastAPI] 4장. API 설계 및 구현: RESTful 엔드포인트와 Pydantic 스키마

브라더댄 2025. 1. 26. 14:02
728x90
반응형
SMALL

안녕하세요! 이번 포스팅에서는 FastAPI 애플리케이션에서 RESTful API를 설계하고 구현하는 방법을 자세히 다뤄보겠습니다. 이를 위해 Pydantic을 사용해 데이터 검증 및 스키마 정의를 하고, 실제로 CRUD 엔드포인트를 만드는 과정을 예시와 함께 살펴봅니다.

이전 3장에서 데이터베이스를 연동하고 ORM 모델을 설정했는데요, 이제 그 모델을 기반으로 실제 RESTful API를 작성하며, OpenAPI 스펙을 통해 자동 문서화까지 경험할 수 있습니다.


4.1. RESTful API 개념 이해

4.1.1. RESTful의 주요 개념

  • 리소스(Resource): 서버가 제공하는 정보를 추상화한 개념입니다. 예: users, posts, orders
  • HTTP 메서드:
    • GET: 리소스 조회
    • POST: 리소스 생성
    • PUT: 리소스 전체 수정
    • PATCH: 리소스 일부 수정
    • DELETE: 리소스 삭제
  • URI 설계: REST에서는 보통 리소스의 종류에 따라 /{resource}/ 형태의 경로를 설계합니다. 예: /api/v1/users, /api/v1/posts/{post_id}/comments
  • 상태 코드(HTTP Status Code): 요청 결과를 나타내며, 대표적으로 200 OK, 201 Created, 400 Bad Request, 404 Not Found, 500 Internal Server Error 등이 있습니다.

4.1.2. FastAPI가 제공하는 장점

  • 간결한 라우팅: @app.get(), @app.post() 등 데코레이터로 손쉽게 API를 정의할 수 있습니다.
  • 자동 문서화: FastAPI는 OpenAPI 스펙을 자동으로 생성해 /docs(Swagger UI), /redoc(Redoc UI)에서 조회할 수 있습니다.
  • Dependency Injection: API 요청 처리 시 필요한 DB 세션, 보안 토큰, 기타 설정 등을 깔끔하게 주입받을 수 있습니다.

4.2. Pydantic 스키마 기초

4.2.1. Pydantic이란?

  • Pydantic은 Python에서 데이터 유효성 검증 및 설정 관리를 위한 라이브러리입니다.
  • FastAPI와 밀접하게 통합되어 있어, 요청 바디응답 데이터를 자동으로 검사하고 변환할 수 있습니다.
  • BaseModel을 상속하여 입력/출력 스키마를 정의하면, FastAPI가 이를 기반으로 데이터 검증타입 힌트를 제공해줍니다.

4.2.2. 예시: User 스키마 (schemas/user.py)

from pydantic import BaseModel, EmailStr, Field
from typing import Optional

class UserBase(BaseModel):
    email: EmailStr
    role: Optional[str] = "user"

class UserCreate(UserBase):
    password: str = Field(..., min_length=6, max_length=30)

class UserUpdate(BaseModel):
    email: Optional[EmailStr] = None
    password: Optional[str] = Field(None, min_length=6, max_length=30)
    role: Optional[str] = None

class UserRead(BaseModel):
    id: int
    email: EmailStr
    role: str

    class Config:
        orm_mode = True
  • UserBase: email과 role을 기본 필드로 설정. role이 없으면 기본값 user로 지정.
  • UserCreate: 회원가입을 위해 password가 반드시 필요하므로 ...(Required)로 설정, 비밀번호 길이를 6~30으로 제한.
  • UserUpdate: 업데이트 시에는 부분만 변경 가능하므로 모든 필드를 Optional로 둡니다.
  • UserRead: 클라이언트에게 돌려주는 응답 스키마로, orm_mode = True 설정을 통해 SQLAlchemy 모델을 자동 변환 가능.

TIP: Field를 통해 min_length, max_length, regex 등 유효성 검증을 추가할 수 있습니다.


4.3. RESTful API 엔드포인트 설계

4.3.1. API 엔드포인트 구조 예시

이전 장의 User 모델을 기준으로 예시를 들어보겠습니다:

  1. GET /api/v1/users : 모든 사용자 조회
  2. GET /api/v1/users/{user_id} : 특정 사용자 조회
  3. POST /api/v1/users : 사용자 생성
  4. PUT /api/v1/users/{user_id} : 사용자 전체 수정
  5. PATCH /api/v1/users/{user_id} : 사용자 일부 수정 (옵션)
  6. DELETE /api/v1/users/{user_id} : 사용자 삭제

FastAPI에서는 @router.get(), @router.post(), @router.put(), @router.delete() 데코레이터를 사용해 위 엔드포인트들을 쉽게 작성할 수 있습니다.

4.3.2. 라우터 코드 예시 (api/v1/endpoints/user.py)

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from typing import List

from app.api.deps import get_db
from app import crud
from app.models.user import User
from app.schemas.user import UserCreate, UserRead, UserUpdate

router = APIRouter()

@router.get("/", response_model=List[UserRead])
def read_users(db: Session = Depends(get_db)):
    users = crud.user.get_all_users(db)
    return users

@router.get("/{user_id}", response_model=UserRead)
def read_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.user.get_user_by_id(db, user_id)
    if not db_user:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user

@router.post("/", response_model=UserRead, status_code=201)
def create_user(user_in: UserCreate, db: Session = Depends(get_db)):
    existing_user = crud.user.get_user_by_email(db, user_in.email)
    if existing_user:
        raise HTTPException(status_code=400, detail="Email already registered")
    new_user = crud.user.create_user(db, user_in)
    return new_user

@router.put("/{user_id}", response_model=UserRead)
def update_user(user_id: int, user_in: UserUpdate, db: Session = Depends(get_db)):
    db_user = crud.user.get_user_by_id(db, user_id)
    if not db_user:
        raise HTTPException(status_code=404, detail="User not found")
    updated_user = crud.user.update_user(db, db_user, user_in)
    return updated_user

@router.delete("/{user_id}")
def delete_user(user_id: int, db: Session = Depends(get_db)):
    db_user = crud.user.get_user_by_id(db, user_id)
    if not db_user:
        raise HTTPException(status_code=404, detail="User not found")
    crud.user.delete_user(db, db_user)
    return {"detail": "User deleted"}
  • response_model: 함수의 반환 값이 자동으로 Pydantic 모델 형태로 직렬화되어 클라이언트에 전달됩니다.
  • status_code=201: 생성 성공 시 201을 반환하도록 지정(RESTful 관례).
  • HTTPException: 404, 400 등 오류 상황에 대해 적절한 상태 코드와 메시지를 반환합니다.

4.3.3. include_router로 라우터 등록 (main.py)

from fastapi import FastAPI
from app.api.v1.endpoints import user

app = FastAPI()

app.include_router(
    user.router,
    prefix="/api/v1/users",
    tags=["users"]
)
  • prefix="/api/v1/users": 해당 라우터의 엔드포인트들은 /api/v1/users/... 경로를 사용합니다.
  • tags=["users"]: Swagger 문서에서 “users”라는 섹션으로 구분됩니다.

4.4. 예외 처리와 응답 모델

4.4.1. 예외 처리 (HTTPException)

FastAPI에서는 원하는 시점에 HTTPException을 일으켜 에러를 처리할 수 있습니다. 예:

if not db_user:
    raise HTTPException(
        status_code=404,
        detail="User not found"
    )

또한, 전역 수준에서 커스텀 예외 핸들러를 등록해 500번 에러 등을 일괄 처리할 수도 있습니다.

4.4.2. 응답 모델(레벨) 지정

  • 함수 단위(response_model): 가장 흔히 사용하는 방식으로, 라우터 데코레이터에 바로 설정.
  • 수준 높은 응답 제어(Response, status_code): 파일 다운로드나 스트리밍 같은 경우에는 좀 더 낮은 레벨의 Response 객체를 사용할 수 있습니다.

4.5. OpenAPI 문서 자동화

4.5.1. Swagger UI & Redoc

  • Swagger UI: /docs 경로에서 제공
  • Redoc: /redoc 경로에서 제공
  • FastAPI는 Pydantic 모델과 엔드포인트 정보를 기반으로 자동으로 OpenAPI 스펙을 생성해 줍니다.

4.5.2. 커스텀 설정

  • app = FastAPI(title="My API", description="API Docs", version="1.0.0") 처럼 매개변수를 지정하여 문서 정보를 커스터마이징할 수 있습니다.
  • openapi_url 파라미터로 스펙 문서 경로를 변경 가능.

4.6. 실제 활용 사례

4.6.1. 블로그 API

  • 게시글(Post), 댓글(Comment), 태그(Tag) 등 엔티티를 정의하고, 각 엔티티에 대해 CRUD API를 만듭니다.
  • 예: POST /api/v1/posts → 글 작성, GET /api/v1/posts/{post_id}/comments → 특정 게시글의 댓글 조회
  • Pydantic 스키마를 통해 글 제목, 내용 길이를 검증하고, 자동 문서화를 통해 프런트엔드 개발자와 원활히 협업할 수 있습니다.

4.6.2. 사내 HR 시스템

  • 직원(Employee), 부서(Department), 휴가(Leave) 등 다양한 리소스에 대해 RESTful 엔드포인트를 제공합니다.
  • 허가되지 않은 부서에 대한 접근을 차단하기 위해 인증/권한 로직(예: JWT)을 추가하고, 유효하지 않은 입력은 Pydantic이 선제적으로 검증합니다.
  • /docs 페이지를 통해 사내 다른 팀과 공유하고, 문서 자동화를 활용해 API 버전을 관리할 수도 있습니다.

4.6.3. IoT 기기 관리 서비스

  • 온도 센서나 스마트 플러그에서 보내는 데이터를 수집하여 장치(Device), 측정값(Metrics), 알람(Alerts) 등을 CRUD로 관리.
  • Pydantic 스키마에서 센서 데이터 범위를 검증하거나, 잘못된 형식의 요청을 사전에 걸러줄 수 있습니다.

모범 사례

  1. URI 설계의 일관성: /api/v1/{resource}/... 등 일정한 규칙을 유지합니다.
  2. 명확한 상태 코드 사용: 생성 시 201 Created, 삭제 성공 시 204 No Content도 고려할 수 있습니다.
  3. Pydantic에서 엄격한 데이터 검증: API 진입 전 올바른 데이터만 들어오도록 관리해 안전성과 품질을 높입니다.
  4. OpenAPI 문서 활용: QA, 프런트엔드, 다른 백엔드 팀과 협업 시 문서 공유가 편리합니다.

4.7. 테스트 예시: FastAPI TestClient

API가 제대로 동작하는지 검증하기 위해 PytestFastAPI TestClient를 사용할 수 있습니다. (이는 6장에서도 좀 더 자세히 다룰 예정이지만 간단히 소개하겠습니다.)

# tests/test_user_api.py
import pytest
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_create_user():
    response = client.post(
        "/api/v1/users",
        json={"email": "test@example.com", "password": "123456"}
    )
    assert response.status_code == 201
    data = response.json()
    assert data["email"] == "test@example.com"

def test_get_user_not_found():
    response = client.get("/api/v1/users/9999")
    assert response.status_code == 404
  • TestClient(app) 객체를 사용하면, 실제 서버를 구동하지 않고도 빠른 단위 테스트가 가능합니다.
  • 응답 내용(response.json())과 상태 코드(response.status_code) 등을 검증하여 API 동작을 보장할 수 있습니다.

4.8. 마무리

이번 4장에서는 RESTful API를 구체적으로 설계하고 구현하는 과정을 살펴봤습니다. 특히:

  1. RESTful의 기본 개념HTTP 메서드, 상태 코드에 대해 복습했습니다.
  2. Pydantic 스키마를 정의하여 요청/응답 데이터를 명확히 하였습니다.
  3. FastAPI 라우터를 이용해 CRUD 엔드포인트를 작성하고, 예외 처리를 통해 안전한 API를 구성했습니다.
  4. OpenAPI 자동 문서 기능으로 Swagger UI와 Redoc을 쉽게 제공할 수 있음을 확인했습니다.
  5. 간단한 TestClient 예제로 테스트 기법을 맛보기로 살펴보았습니다.

다음 5장에서는 인증(Authorization)과 권한(Access Control) 기능을 더해, 실제로 안전한 서비스를 만들기 위한 JWT 기반 인증 방법과 OAuth2 흐름 등을 자세히 다룰 예정입니다. 데이터베이스 연동 + RESTful API 설계 + 인증 체계는 현대 웹 애플리케이션에서 필수적인 3대 요소이므로, 꼭 이어서 학습해 보시기 바랍니다!

 

728x90
반응형
LIST