FastAPI 라서 그런지 진도 나가는 속도마저 Fast 하다.
학습시간 09:00~02:00(당일17H/누적1994H)
◆ 학습내용
1. Pydantic
- 파이썬의 타입 힌트(type hint)를 사용해 데이터의 구조와 타입을 강제하는 라이브러리
- 복잡한 JSON 데이터를 단순한 파이썬 객체처럼 다룰 수 있게 해줌
- FastAPI는 이 Pydantic 모델을 보고 자동으로 다음 기능들을 수행
- 들어오는 요청(request) 데이터의 유효성 검사 (validation)
- 나가는 응답(response) 데이터를 특정 형태로 변환 (serialization)
- API 자동 문서 생성 (Swagger UI, ReDoc)
(1) 기본 타입 설정
from pydantic import BaseModel
from typing import Optional
class Item(BaseModel):
name: str
price: float
description: Optional[str] = None
tax: Optional[float] = None
(2) Field를 이용한 상세 유효성 검사
- Pydantic의
Field 함수를 사용하면 단순한 타입 검사 외에 훨씬 상세한 제약조건을 걸 수 있음
- 예를 들어, 숫자의 최솟값/최댓값, 문자열의 길이, 정규식 패턴 등을 강제할 수 있어
- API의 안정성을 엄청나게 높여주는 핵심 기능
- 들어오는 데이터에 대한 방어 로직을 서비스 코드 대신 스키마 단에서 깔끔하게 처리 가능
from pydantic import BaseModel, Field
import re
class Product(BaseModel):
price: float = Field(gt=0, description="가격은 0보다 커야 합니다.")
name: str = Field(min_length=3, max_length=50)
product_code: str = Field(pattern=r"^PROD-\d{4}$")
Field 인자 | 설명 | 예시 |
|---|
gt, ge | 보다 큼 (greater than), 크거나 같음 (greater or equal) | Field(gt=0) |
lt, le | 보다 작음 (less than), 작거나 같음 (less than or equal) | Field(le=100) |
min_length | 문자열의 최소 길이 | Field(min_length=1) |
max_length | 문자열의 최대 길이 | Field(max_length=20) |
pattern | 정규식(Regex) 패턴 검사 | Field(pattern=r"...") |
description | 필드에 대한 설명 (API 문서에 표시됨) | Field(description="...") |
(3) @validator를 이용한 커스텀 검증 로직
Field만으로 표현하기 힘든 복잡한 검증 로직이 필요할 때 사용
- 예를 들어, '할인 종료일'이 '할인 시작일'보다 반드시 나중이어야 한다는 규칙
@validator 데코레이터를 사용하여 특정 필드에 대한 검증 함수를 직접 정의
- 여러 필드의 값을 조합하여 검증하는 것도 가능
from pydantic import BaseModel, validator
from datetime import date
class Event(BaseModel):
start_date: date
end_date: date
@validator('end_date')
def validate_dates(cls, v, values):
if 'start_date' in values and v < values['start_date']:
raise ValueError('종료일은 시작일보다 빠를 수 없습니다.')
return v
(4) 중첩 모델(Nested Models)
- JSON 안에 또 다른 JSON 객체가 들어가는 복잡한 구조를 다룰 때 사용
- 각 객체 구조에 맞춰 Pydantic 모델을 각각 정의하고, 한 모델이 다른 모델을 타입 힌트로 포함하는 방식
- 예를 들어, '사용자' 정보 안에 '주소' 객체가 포함된 경우
- 코드가 훨씬 구조화되고 재사용성이 높아짐
from pydantic import BaseModel
from typing import List
class Address(BaseModel):
city: str
zip_code: str
class User(BaseModel):
username: str
email: str
address: Address
previous_addresses: List[Address] = []
| 모델 | 역할 | 구조 예시 (JSON) |
|---|
Address | 주소 정보라는 하위 객체를 위한 스키마 | {"city": "서울", "zip_code": "12345"} |
User | Address 모델을 필드 타입으로 포함하는 상위 스키마 | {"username": "길동", ..., "address": {...}} |
List[Address] | 주소 객체가 여러 개 담긴 배열을 표현 | "previous_addresses": [{...}, {...}] |
(5) 응답 모델(Response Model) 제어
- 데이터베이스에서 가져온 값을 그대로 보여주지 않고, 가공해서 보여주고 싶을 때가 있음
- 예를 들어
price와 tax를 합친 price_with_tax 필드를 응답에만 추가하고 싶을 경우
- Pydantic 모델을 입력용(스키마)과 출력용(
response_model)으로 분리하여 관리하면 유연성이 극대화됨
@computed_field 데코레이터를 사용해 동적으로 계산되는 필드를 만들 수 있음
from pydantic import BaseModel, computed_field
class ItemCreate(BaseModel):
name: str
price: float
tax: float = 0.1
class ItemResponse(BaseModel):
name: str
price: float
@computed_field
@property
def price_with_tax(self) -> float:
return self.price * (1 + 0.1)
| 구분 | 설명 |
|---|
ItemCreate | 클라이언트로부터 데이터를 받을 때(입력) 사용하는 모델 |
ItemResponse | 클라이언트에게 데이터를 보낼 때(출력) 사용하는 모델 |
@computed_field | 모델의 다른 필드 값을 기반으로 동적으로 계산되는 값을 가진 필드를 정의 |
@property | 해당 함수를 메서드가 아닌 속성(값)처럼 호출할 수 있게 해줌 |
2. CRUD
(1) Create (@POST)
클라이언트의 데이터를 서버에 기록
POST HTTP 메서드는 서버에 새로운 자원(데이터)을 생성해달라고 요청할 때 사용하는 약속
- FastAPI에서는
@app.post("/경로") 데코레이터를 사용해 POST 요청을 처리할 함수를 지정
- 함수의 매개변수에 Pydantic 모델을 타입 힌트로 선언하는 것이 핵심 (
item: ItemCreate)
- FastAPI가 이 힌트를 보고 클라이언트가 보낸 JSON 요청 본문을 자동으로 읽음
- Pydantic 모델(
ItemCreate)의 규칙에 맞는지 데이터 유효성을 검사
- 검사를 통과하면 JSON 데이터를 파이썬 객체(
item)로 변환하여 함수로 전달
- 만약 규칙에 어긋나는 데이터가 들어오면, FastAPI가 알아서
422 Unprocessable Entity 에러를 응답해주므로 개발이 매우 편리해짐
status_code 인자를 사용해 성공 응답 코드를 명시적으로 지정하는 것이 좋음 (데이터 생성 성공 시에는 201 Created가 표준)
response_model을 지정하면 응답 데이터의 형태까지 Pydantic으로 제어하여 일관성을 유지하고 불필요한 정보 노출을 막을 수 있음
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
app = FastAPI()
db = {}
class ItemCreate(BaseModel):
name: str
price: float
class ItemResponse(BaseModel):
id: int
name: str
price: float
@app.post(
"/items/",
response_model=ItemResponse,
status_code=status.HTTP_201_CREATED
)
def create_item(item: ItemCreate):
new_id = len(db)
new_item = {"id": new_id, **item.model_dump()}
db[new_id] = new_item
return new_item
| 구분 | 설명 |
|---|
| 데코레이터 | @app.post("/items/") |
| HTTP 메서드 | POST |
| 주요 역할 | 새로운 데이터를 서버에 생성 |
| 입력 데이터 | Pydantic 모델로 정의된 Request Body (JSON) |
| 성공 상태 코드 | 201 Created (데이터 생성이 성공적으로 완료됨) |
| 핵심 기능 | Pydantic을 통한 자동 요청 데이터 유효성 검사 |
(2) Read (@GET)
저장된 데이터 목록 및 특정 데이터 조회
GET HTTP 메서드는 서버의 자원(데이터)을 조회(가져오기)할 때 사용
- 전체 목록 조회 (
/items/)
- 특정 ID 없이 컬렉션 경로에
GET 요청을 보내 전체 목록을 가져옴
- 응답 모델을
response_model=List[ItemResponse] 와 같이 리스트 형태로 지정하여 여러 개의 아이템을 반환
- 특정 데이터 조회 (
/items/{item_id})
- 경로에
{item_id} 와 같은 '경로 매개변수'를 사용하여 조회할 데이터의 고유 ID를 전달
- 함수의 매개변수
item_id: int 처럼 타입 힌트를 주면 FastAPI가 경로의 문자열을 해당 타입(정수)으로 자동 변환 및 검사
- 만약 DB에 해당 ID의 데이터가 없으면,
404 Not Found 에러를 반환하는 것이 표준적인 REST API 설계
- FastAPI의
HTTPException을 사용하면 원하는 상태 코드와 에러 메시지를 손쉽게 클라이언트에게 보낼 수 있음
from typing import List
@app.get("/items/", response_model=List[ItemResponse])
def read_items():
return list(db.values())
@app.get("/items/{item_id}", response_model=ItemResponse)
def read_item(item_id: int):
if item_id not in db:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"ID가 {item_id}인 아이템을 찾을 수 없습니다."
)
return db[item_id]
| 구분 | 설명 |
|---|
| 데코레이터 | @app.get("/items/"), @app.get("/items/{item_id}") |
| HTTP 메서드 | GET |
| 주요 역할 | 데이터 조회 (목록 또는 단일) |
| 경로 매개변수 | {item_id}: URL 경로를 통해 동적인 값을 받는 변수 |
| 핵심 기능 | HTTPException을 이용한 명시적인 에러 처리 (e.g. 404) |
(3) Update (@PUT & @PATCH)
기존 데이터 수정하기 (PUT vs PATCH)
PUT과 PATCH 두 가지 메서드가 있으며, 용도가 약간 다름
PUT (/items/{item_id})
- 자원 전체를 교체하는 개념
- 요청 시 모델의 모든 필드 값을 보내야 하는 것이 원칙
- 특정 필드를 빼고 보내면 해당 필드는
null 이나 기본값으로 덮어 써짐
PATCH (/items/{item_id})
- 자원의 일부만 수정하는 개념
- 클라이언트가 변경하려는 필드만 요청 본문에 담아 보냄
PUT보다 네트워크 효율이 좋고 유연해서 더 선호되는 경향이 있음
PATCH용 Pydantic 모델은 모든 필드를 Optional로 만들어 어떤 필드든 선택적으로 받을 수 있도록 설계하는 것이 일반적
- Pydantic 객체의
.model_dump(exclude_unset=True) 메서드를 사용하면 클라이언트가 실제로 보낸 필드만 딕셔너리로 추출할 수 있어 PATCH 구현에 매우 유용
from typing import Optional
class ItemUpdate(BaseModel):
name: Optional[str] = None
price: Optional[float] = None
@app.patch("/items/{item_id}", response_model=ItemResponse)
def update_item_partial(item_id: int, item: ItemUpdate):
if item_id not in db:
raise HTTPException(status_code=404, detail="아이템을 찾을 수 없음")
stored_item = db[item_id]
update_data = item.model_dump(exclude_unset=True)
for key, value in update_data.items():
stored_item[key] = value
return stored_item
| 구분 | PUT | PATCH |
|---|
| 데코레이터 | @app.put(...) | @app.patch(...) |
| 개념 | 전체 교체 (Replace) | 부분 수정 (Modify) |
| 요청 데이터 | 리소스의 모든 필드 | 변경이 필요한 필드만 |
| Pydantic 모델 | ItemCreate 모델 재사용 가능 | 모든 필드가 Optional인 별도 모델 권장 |
| 사용처 | 데이터 구조 전체를 새로 쓸 때 | 일부 필드의 값만 변경할 때 |
(4) Delete (@DELETE)
불필요한 데이터 영구적으로 삭제하기
DELETE HTTP 메서드는 특정 자원을 삭제할 때 사용
GET (단일 조회)과 마찬가지로 경로 매개변수를 통해 삭제할 대상의 ID를 받음
- 삭제할 데이터가 없는 경우
404 Not Found 에러를 보내주는 것이 좋음
- 데이터 삭제 성공 시에는 크게 두 가지 방식으로 응답
{"message": "삭제 성공"} 과 같이 간단한 JSON 메시지를 200 OK 상태 코드와 함께 반환
- 응답 본문(Body) 없이
204 No Content 상태 코드를 반환. 이는 '요청은 성공했지만 보내줄 내용은 없음'을 의미하며, RESTful API에서 더 선호되는 방식
from fastapi import Response
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
if item_id not in db:
raise HTTPException(status_code=404, detail="아이템을 찾을 수 없음")
del db[item_id]
return Response(status_code=status.HTTP_204_NO_CONTENT)
| 구분 | 설명 |
|---|
| 데코레이터 | @app.delete("/items/{item_id}") |
| HTTP 메서드 | DELETE |
| 주요 역할 | 특정 ID를 가진 데이터를 서버에서 삭제 |
| 성공 상태 코드 | 200 OK (메시지 포함 시) 또는 204 No Content (내용 없음) |
| 핵심 기능 | 경로 매개변수로 대상을 특정하고, 해당 데이터를 영구 제거 |