앞선 글에서는 엔드포인트와 라우터 구조를 정리하였다.
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를 서비스 수준으로 확장하는 방법을 정리해보겠다.
'Development environment' 카테고리의 다른 글
| [FastAPI] FastAPI 개발하기3 - 엔드포인트와 라우터 (0) | 2026.02.12 |
|---|---|
| [FastAPI] FastAPI 개발하기2 - CORS 개념 및 설정 (0) | 2026.02.06 |
| [FastAPI] FastAPI 개발하기 1- 환경 셋팅 및 실행명령어 (0) | 2026.02.06 |
| [DBeaver] DBeaver 설치 및 연동하기 (0) | 2026.02.03 |
| [Git] Git 커밋 메세지 규칙 + 커밋 수정하기 (0) | 2025.12.17 |