CRUD 흐름을 만들었다면 이제 응답 구조를 통일하는 것이 중요합니다. response_model은 FastAPI가 "이 엔드포인트는 어떤 JSON을 돌려준다"라고 선언하도록 돕는 옵션입니다. 미리 선언해 두면 문서와 실제 응답이 일치하고, 민감한 필드를 숨길 수 있습니다. 5편에서 만든 리스트 기반 API를 계속 확장해 FastAPI의 response_model 옵션을 적용해 보겠습니다.
이번 글에서 새로 나오는 용어
- response_model: FastAPI 엔드포인트가 반환해야 할 JSON 모양을 미리 정해 두는 옵션으로, 응답을 자동으로 필터링하고 문서를 일치시키는 데 쓰입니다.
- 읽기/쓰기 모델 분리: 입력 전용(
UserCreate)과 출력 전용(UserRead) 모델을 따로 만드는 패턴으로, 비밀번호 같은 민감 정보를 응답에서 숨길 때 필수입니다. - Field: Pydantic 필드에 설명, 기본값, 길이 제한 등을 붙여 Swagger 문서와 검증을 동시에 강화하는 헬퍼입니다.
- 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_model이 UserRead이기 때문에 JSON 응답에서는 id와 email만 노출됩니다. 자동 문서에서도 이 스키마만 보여 주어, 클라이언트 개발자가 헷갈리지 않습니다.
요청/응답 모델 시각화
하나의 엔드포인트에서 두 모델이 어떻게 쓰이는지 시각화하면 초반에 헷갈리기 쉬운 "비밀번호는 받지만 응답에는 없다"는 흐름이 훨씬 직관적으로 이해됩니다.
리스트 응답에도 적용
@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
이렇게 문서와 실제 응답이 일치하도록 만드는 것이 협업에 큰 도움이 됩니다.
💬 댓글
이 글에 대한 의견을 남겨주세요