Development environment

[FastAPI] FastAPI 개발하기4 - Request / Response 모델(Pydantic)

hjjummy 2026. 2. 13. 07:39

앞선 글에서는 엔드포인트와 라우터 구조를 정리하였다.
URL, 메서드, 함수가 하나의 엔드포인트를 구성하고, 라우터를 통해 이를 구조적으로 관리할 수 있다는 점을 살펴보았다.

2026.02.12 - [Development environment] - [FastAPI] FastAPI로 백엔드 개발하기 3 - 엔드포인트와 라우터

 

[FastAPI] FastAPI로 백엔드 개발하기 3 - 엔드포인트와 라우터

앞서 FastAPI 개발하기 2편에서 CORS 개념을 정리하였다.CORS를 통해 브라우저 보안 모델을 이해했다면, 이제는 서버 내부 구조를 이해할 차례이다.2026.02.06 - [Development environment] - [FastAPI] FastAPI로 백

hjjummy.tistory.com

 

 

 

이제 한 단계 더 들어가 보자.

 

API 서버는 단순히 “요청을 받고 응답을 주는 코드”가 아니다.

 

어떤 형식의 데이터를 받고, 어떤 형식으로 데이터를 반환할지 명확히 정의하는 시스템이다.

 

이 역할을 담당하는 것이 바로 Request / Response 모델(Pydantic) 이며, 보통 schemas.py에 작성한다.


1. Request / Response 모델이란?

FastAPI에서 Request / Response 모델은 API의 입출력 형식을 정의하는 클래스

 

쉽게 말해,

  • Request 모델 → “이 API는 이런 JSON만 받는다.”
  • Response 모델 → “이 API는 이런 JSON을 반환한다.”

요청, 응답 형식을 코드로 명확하게 정의하는 것이다.

이때 사용하는 것이 Pydantic의 BaseModel이다.

 

이를 schemas.py에 작성하는데 장점은 다음과 같다.

  • 팀 협업에서 API 스펙 논쟁이 줄어듦
  • Swagger 문서가 자동으로 최신 상태 유지됨
  • 서버 내부 로직이 바뀌어도 응답 형태는 안정적으로 유지됨

2. Pydantic이란?

Pydantic은 파이썬 타입 힌트를 기반으로 동작하는 데이터 검증 라이브러리

 

FastAPI는 내부적으로 Pydantic을 활용하여 다음을 자동 처리한다.

  • 입력 데이터 타입 검증
  • 필수값 여부 확인
  • 범위 검사 (ge, le 등)
  • JSON 스키마 생성
  • Swagger(OpenAPI) 문서 자동 생성

즉,

타입 힌트가 곧 검증 규칙이며, 문서이며, 계약이 된다.

 

이것이 FastAPI의 핵심 철학이다.


3. Request 모델: 입력을 검증하는 구조

예를 들어 키워드 검색 API의 요청 모델은 다음과 같이 정의할 수 있다

class KeywordSearchRequest(BaseModel):
    query: str = Field(..., description="검색어")
    search_type: str = Field(..., description="검색 유형 (일반검색=SEARCH/AI검색=AI)")
    page: int = Field(..., ge=1, description="조회할 페이지 번호 (1부터 시작)")
    page_size: int = Field(..., ge=1, le=100, description="페이지당 결과 수")

이 모델은 단순한 변수 선언뿐만 아니라 아래 3가지 기능을 한다!


3-1. 타입 기반 자동 검증

  • query: str → 문자열만 허용
  • page: int → 정수만 허용

잘못된 타입으로 클라이언트가

{
  "page": "abc"
}

처럼 보내면 FastAPI가 자동으로 422 Validation Error를 반환한다.

개발자가 직접 if type(...) 검사로직을 작성할 필요가 없다.


3-2. Field의 범위 검증

page: int = Field(..., ge=1)
  • ge=1 → 1 이상만 허용
  • le=100 → 100 이하

page=0이 들어오면 자동으로 422에러가 발생한다.


3-3. example과 description의 역할

example은 Swagger 문서 품질을 확 올린다

class Config:
    json_schema_extra = {
        "example": {
            "query": "전명훈",
            "search_type": "SEARCH",
            "page": 1,
            "page_size": 10
        }
    }

이 설정을 넣으면 Swagger에서 “예시 JSON”이 자동으로 채워진다.
API 문서를 따로 만들지 않아도 된다.
코드가 곧 문서가 된다.


4. Response 모델: 응답을 통일하는 구조

많은 초보 개발자가 응답은 단순 dict로 반환해도 충분하다고 생각한다.

return {"success": True, "data": result}

하지만 서비스가 커지고, 협업의 경우에 문제가 발생한다.

  • 응답 필드가 개발자마다 다름
  • 디버깅용 필드가 그대로 노출됨
  • 프론트엔드와의 계약이 깨짐

이를 방지하기 위해 Response 모델을 정의한다.

class KeywordSearchResponse(BaseModel):
    success: bool
    message: str
    query: str
    paging: PagingInfo
    results: List[EmployeeResult]

그리고 라우터에서 다음과 같이 사용한다.

@router.post("/search", response_model=KeywordSearchResponse)
async def search(req: KeywordSearchRequest):
    ...

여기서  response_model=KeywordSearchResponse 설정으로 이 API는 반드시 KeywordSearchResponse 구조로 응답한다.

 

* Response 모델 vs response_model의 차이

많이 헷갈리는 부분이다.

 

✔ Response 모델(BaseModel) → 응답 구조를 “정의”하는 클래스

✔ response_model 옵션 → 해당 모델을 실제 API에 “적용하고 강제”하는 옵션

 

비유하자면

  • Response 모델 → 설계도
  • response_model → router 설정 시 설계도 대로 강제하는 옵션 파라미터

response_model을 사용하면

  • 정의되지 않은 필드 자동 제거
  • 타입 자동 변환
  • Swagger 문서에 정확히 반영
  • 응답 구조 강제

즉,

Response 모델은 정의이고, response_model은 그 정의를 따라가도록 하는 옵션이다.

 


4.1. Optional[str]

Response 모델을 설계하다 보면 자연스럽게 등장하는 개념이 Optional과 중첩 모델 구조이다.

 

email: Optional[str]

이는 “문자열일 수도 있고, None일 수도 있다”는 뜻이다.
즉, 해당 필드는 존재할 수도 있고, 없을 수도 있다는 의미이다.

여기서 중요한 점은 다음이다.

  • 타입은 여전히 문자열이다.
  • 단, 값이 없을 경우 None을 허용한다.
  • 필수 입력값(required)이 아니라는 것을 명확히 표현한다.

실서비스에서는 모든 데이터가 항상 존재하지 않는다.
휴대전화 번호가 없을 수도 있고, 이미지 링크가 비어 있을 수도 있다.

이처럼 “없을 수 있음”을 명확하게 선언하는 것이 API 계약을 안정적으로 만드는 핵심이다.
Optional을 사용하지 않으면, 예상치 못한 None 값 때문에 런타임 오류가 발생할 수 있다.

 

4.1. 중첩 모델 설계의 

이제 조금 더 중요한 구조 설계를 살펴보자.

현재 응답 구조는 다음처럼 계층적으로 나뉘어 있다.

  • EmployeeResult → 검색 결과 1건
  • PagingInfo → 페이징 정보
  • KeywordSearchResponse → 최종 응답

-1. 하위 모델 정의

class EmployeeResult(BaseModel):
    emp_nm: Optional[str]
    emp_no: Optional[str]
    dept_nm: Optional[str]
    email_id: Optional[str]
class PagingInfo(BaseModel):
    page: int
    page_size: int
    total_count: Optional[int]
    total_pages: Optional[int]
    has_next: Optional[bool]

 

-2. 최상위 응답 모델

class KeywordSearchResponse(BaseModel):
    success: bool
    message: str
    query: str
    paging: PagingInfo
    results: List[EmployeeResult]

여기서 핵심은 이 두 줄이다.

paging: PagingInfo
results: List[EmployeeResult]

즉,

  • paging은 또 다른 모델을 포함하고 있고
  • results는 EmployeeResult의 리스트이다

이것이 중첩 모델 구조이다.

 

이 모델이 반환하는 실제 JSON은 다음처럼 생긴다.

{
  "success": true,
  "message": "SUCCESS",
  "query": "정정정",
  "paging": {
    "page": 1,
    "page_size": 10,
    "total_count": 23,
    "total_pages": 3,
    "has_next": true
  },
  "results": [
    {
      "emp_nm": "정정정",
      "emp_no": "31159",
      "dept_nm": "DS팀",
      "email_id": "example@company.com"
    }
  ]
}

 

 

이렇게 계층적으로 나누면 장점이 매우 크다.

1. 구조가 한눈에 보인다

응답 JSON이 어떤 형태인지 코드만 봐도 이해된다.


2. 재사용 가능하다

EmployeeResult는 다른 검색 API에서도 그대로 재사용 가능하다.

예를 들어:

  • 전체검색 API
  • 관리자 검색 API
  • 추천 검색 API

모두 같은 모델을 쓸 수 있다.


3. Swagger 문서가 깔끔해진다

Swagger에서 모델이 계층적으로 정리된다.

  • KeywordSearchResponse
    • PagingInfo
    • EmployeeResult

자동으로 구조가 시각화된다.


4. 유지보수가 쉬워진다

예를 들어 직원 정보에 position 필드를 추가한다고 가정하자.

class EmployeeResult(BaseModel):
    emp_nm: Optional[str]
    position: Optional[str]
 

이 한 줄 추가만으로:

  • 모든 응답 구조 자동 반영
  • Swagger 자동 업데이트
  • 프론트와 계약 일관성 유지

이것이 “계약 기반 설계”의 장점이다.


6. Swagger 문서는 왜 자동으로 만들어지는가?

Swagger는 FastAPI가 OpenAPI 스펙을 자동 생성하기 때문에 생긴다.

그러나 Pydantic 모델이 없다면 Swagger는 매우 빈약해진다.

  • 요청 필드 정보 없음
  • 타입 정보 부족
  • 예시 없음
  • 응답 구조 불명확

즉,

FastAPI → Swagger를 생성
Pydantic → Swagger를 구체화

 

라고 이해하면 된다.


7. ErrorResponse 모델

class ErrorResponse(BaseModel):
    success: bool = False
    message: str
    detail: Optional[str]

에러 응답도 “스키마”로 고정해두면 장점이 크다.

  • 프론트에서 에러 처리 공통화 가능
  • 운영 로그/모니터링에서 에러 포맷이 일관됨
  • API 문서에서 에러 스펙을 명확히 보여줄 수 있음

실서비스에서는 성공 응답보다 에러 응답이 더 중요해지는 순간이 자주 온다.


마무리

엔드포인트와 라우터가 서버의 구조라면, schemas.py 의 Request / Response 모델은 서버의 “계약”이다.

  • Request 모델 → 입력 검증 자동화
  • Response 모델 → 응답 구조 고정
  • response_model → 응답 계약 강제
  • Field → 검증 + 문서화
  • Swagger → 자동 문서 생성

즉, Pydantic을 사용하는 순간 API는 단순한 코드가 아니라 계약 기반 시스템이 된다.

 

FastAPI를 잘 사용한다는 것은 단순히 API를 만드는 것이 아니라,

  • 어떤 데이터를 받을 것인지
  • 어떤 형식으로 반환할 것인지
  • 검증은 어떻게 할 것인지
  • 문서는 어떻게 유지할 것인지

를 함께 설계하는 것이다.

 

다음 글에서는 의존성 주입(Depends)과 인증 구조를 중심으로 FastAPI를 서비스 수준으로 확장하는 방법을 정리해보겠다.