[FastAPI 시리즈 6편] response_model과 검증, 문서 개선하기

English version

CRUD 흐름을 만들었다면 이제 응답 구조를 통일하는 것이 중요합니다. response_model은 FastAPI가 "이 엔드포인트는 어떤 JSON을 돌려준다"라고 선언하도록 돕는 옵션입니다. 미리 선언해 두면 문서와 실제 응답이 일치하고, 민감한 필드를 숨길 수 있습니다. 5편에서 만든 리스트 기반 API를 계속 확장해 FastAPI의 response_model 옵션을 적용해 보겠습니다.

이번 글에서 새로 나오는 용어

  1. response_model: FastAPI 엔드포인트가 반환해야 할 JSON 모양을 미리 정해 두는 옵션으로, 응답을 자동으로 필터링하고 문서를 일치시키는 데 쓰입니다.
  2. 읽기/쓰기 모델 분리: 입력 전용(UserCreate)과 출력 전용(UserRead) 모델을 따로 만드는 패턴으로, 비밀번호 같은 민감 정보를 응답에서 숨길 때 필수입니다.
  3. Field: Pydantic 필드에 설명, 기본값, 길이 제한 등을 붙여 Swagger 문서와 검증을 동시에 강화하는 헬퍼입니다.
  4. OpenAPI 스키마: FastAPI가 자동 생성하는 API 명세로, response_model과 모델 정의가 바뀌면 즉시 업데이트되어 협업 기준이 됩니다.

실습 카드

  • 예상 소요 시간: 40분
  • 사전 준비: 5편 CRUD 예제 코드, Swagger UI 사용
  • 실습 목표: 쓰기/읽기 모델을 분리해 response_model로 응답을 고정한다

이 글에서 할 것

  • response_model 사용법 익히기
  • 민감한 필드를 숨기고, 읽기/쓰기를 다른 모델로 분리하기
  • 문서 페이지에서 응답 스키마 자동 업데이트 확인하기

읽기/쓰기 모델 분리

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field

app = FastAPI()

class UserCreate(BaseModel):
    email: str
    password: str = Field(min_length=8)

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

users: list[dict] = []
current_id = 0

UserCreate에는 비밀번호를 포함하지만, UserRead에는 비밀번호가 없습니다.

response_model 지정하기

@app.post("/users", response_model=UserRead, status_code=201)
def create_user(payload: UserCreate):
    global current_id
    current_id += 1
    record = {"id": current_id, **payload.model_dump()}
    users.append(record)
    return record

실제 반환은 비밀번호를 포함하지만 response_modelUserRead이기 때문에 JSON 응답에서는 idemail만 노출됩니다. 자동 문서에서도 이 스키마만 보여 주어, 클라이언트 개발자가 헷갈리지 않습니다.

요청/응답 모델 시각화

Frontend / Swagger UIUserCreate (email, password)POST /usersusers listUserRead (id, email)OpenAPI Schema 입력 작성validationsave full recordcast resultJSON 응답자동 문서에 노출

하나의 엔드포인트에서 두 모델이 어떻게 쓰이는지 시각화하면 초반에 헷갈리기 쉬운 "비밀번호는 받지만 응답에는 없다"는 흐름이 훨씬 직관적으로 이해됩니다.

리스트 응답에도 적용

@app.get("/users", response_model=list[UserRead])
def list_users():
    return users

타입 힌트 덕분에 Swagger UI에서도 응답이 배열이라는 사실을 명확히 보여줍니다.

필드 설명과 예시 붙이기

Field를 사용하면 문서 페이지에 설명과 예시가 자동으로 삽입됩니다.

class UserCreate(BaseModel):
    email: str = Field(description="로그인에 사용할 이메일", examples=["[email protected]"])
    password: str = Field(min_length=8, description="8자 이상 비밀번호")

examples는 Swagger UI의 Example Value 영역에 그대로 노출됩니다.

실전 예제: 버전 정보와 메타 데이터 포함하기

교내 앱 배포 시점에 따라 응답 형식을 구분해야 할 때가 있습니다. 아래처럼 version 필드를 추가한 응답 모델을 만들어 두면, 모바일 앱이 오래된 응답을 받았을 때 경고를 띄우게 할 수 있습니다.

class ApiEnvelope(BaseModel):
    version: str = "v1"
    data: list[UserRead]

@app.get("/users/enveloped", response_model=ApiEnvelope)
def list_users_with_meta():
    return {"version": "v1", "data": users}

이 패턴은 이후 GraphQL, gRPC 같은 다른 프로토콜을 실험할 때에도 응답 구조를 명확히 정의하는 연습이 됩니다.

응답 캐스팅

FastAPI는 반환 값이 dict, ORM 모델 등 다양한 타입일 수 있지만 response_model에 맞춰 자동으로 변환해 줍니다. 만약 모델에 정의되지 않은 필드가 있으면 기본적으로 제거합니다. 이 덕분에 API 응답을 신뢰할 수 있습니다.

필요하다면 Config에서 model_config = ConfigDict(from_attributes=True)와 같은 설정을 추가해 ORM 객체에서도 필드를 읽을 수 있게 만들 수 있습니다.

문서에서 바로 확인하기

서버를 실행한 뒤 /docs에 들어가 POST /users를 열어 보면 Response Body가 UserRead 스키마로 표시됩니다.

uv run fastapi dev main.py

이렇게 문서와 실제 응답이 일치하도록 만드는 것이 협업에 큰 도움이 됩니다.

http://127.0.0.1:8000/docs#/users/post_users
LOCAL

Swagger UI

요청과 응답 모델이 바로 비교됨

Try it out을 눌러 이메일과 비밀번호를 입력하면, Response 탭에는 자동으로 UserRead 스키마만 표시됩니다.

UserCreateUserReadresponse_model

Body

POST /users

email + password 필드를 UserCreate 기준으로 작성.

Response

201 Created

문서상에는 id와 email만 노출되어 민감 데이터가 숨겨진다.

Schema

Auto generated

변경 사항이 있을 때마다 OpenAPI 스키마가 즉시 업데이트된다.

실습

  • 따라 하기: UserCreate, UserRead 모델을 분리하고 response_model을 지정한 POST/GET 엔드포인트를 호출한다.
  • 확장하기: ApiEnvelope처럼 메타 데이터를 담는 래퍼 모델을 추가해 /users/enveloped를 만들어 본다.
  • 디버깅: 비밀번호 필드가 응답에 섞이지 않는지 확인하고, 모델 정의를 바꿨을 때 Swagger UI가 즉시 업데이트되는지 검증한다.
  • 완료 기준: 문서와 실제 응답이 동일하게 표시되고, 민감 필드 숨김이 동작한다.

마무리

response_model을 지정하면 API 응답을 더 안전하게 다룰 수 있고, 문서 품질도 자연스럽게 올라갑니다. 다음 글에서는 엔드포인트를 파일로 나누고 APIRouter를 사용해 프로젝트 구조를 정리하는 방법을 살펴보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요