데이터 업데이트
데이터 업데이트 (Body - Updates)
기존 데이터를 갱신하는 건 API에서 흔한 일이죠. FastAPI는 눈여겨볼 점이 하나 있는데, HTTP의 PUT과 PATCH를 어떻게 나눠 쓰는 게 의도된 방식인지를 보여줘요. PUT은 "통째로 교체", PATCH는 "부분 수정"이라는 뉘앙스예요.
출처: 공식문서
PUT으로 교체 업데이트하기 (Update replacing with PUT)
아이템을 업데이트하려면 HTTP PUT 연산을 쓰면 돼요.
jsonable_encoder를 사용하면 입력 데이터를 JSON으로 저장 가능한 데이터로 변환할 수 있어요(예: NoSQL 데이터베이스에 저장). 예를 들어 datetime을 str로 변환해 주죠.
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str | None = None
description: str | None = None
price: float | None = None
tax: float = 10.5
tags: list[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.put("/items/{item_id}", response_model=Item)
async def update_item(item_id: str, item: Item):
update_item_encoded = jsonable_encoder(item)
items[item_id] = update_item_encoded
return update_item_encoded
PUT은 기존 데이터를 완전히 교체해야 하는 데이터를 받는 데 사용돼요.
교체에 대한 경고 (Warning about replacing)
이게 좀 미묘한데, 교체라는 의미 때문에 주의가 필요해요. 만약 PUT으로 아이템 bar를 아래와 같은 body로 업데이트한다고 해 볼게요:
{
"name": "Barz",
"price": 3,
"description": null,
}
이 body에는 이미 저장돼 있던 "tax": 20.2가 포함돼 있지 않으니, 입력 모델은 tax의 기본값인 10.5를 사용해요. 그래서 저장되는 데이터도 그 "새로운" tax인 10.5가 돼요. PUT이 온전히 교체이기 때문에, 빠진 필드(여기선 tax)는 기본값으로 채워지게 되는 거예요.
PATCH로 부분 업데이트하기 (Partial updates with PATCH)
부분 업데이트에는 HTTP PATCH 연산을 쓸 수 있어요. 업데이트할 데이터만 보내고, 나머지는 그대로 두는 방식이죠.
참고:
PATCH는PUT만큼 흔히 쓰이거나 잘 알려지진 않았어요. 많은 팀이 부분 업데이트조차PUT만 쓰기도 해요. 원하는 대로 자유롭게 써도 되고, FastAPI가 제약을 두지는 않아요. 다만 이 가이드는 이 둘이 "의도된 대로" 쓰인다면 어떻게 쓰이는지 보여주는 거예요.
Pydantic의 exclude_unset 파라미터 사용하기
부분 업데이트를 받으려면 Pydantic 모델의 .model_dump()에서 exclude_unset 파라미터를 쓰는 게 아주 유용해요.
item.model_dump(exclude_unset=True)처럼 쓰면 되죠. 그러면 item 모델을 만들 때 실제로 설정된 데이터만 담긴 dict가 생성돼요. 기본값은 제외되죠.
이걸 이용해서 요청에서 보낸(설정된) 데이터만 담고 기본값은 뺀 dict를 만들 수 있어요:
from fastapi import FastAPI
from fastapi.encoders import jsonable_encoder
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str | None = None
description: str | None = None
price: float | None = None
tax: float = 10.5
tags: list[str] = []
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The bartenders", "price": 62, "tax": 20.2},
"baz": {"name": "Baz", "description": None, "price": 50.2, "tax": 10.5, "tags": []},
}
@app.get("/items/{item_id}", response_model=Item)
async def read_item(item_id: str):
return items[item_id]
@app.patch("/items/{item_id}")
async def update_item(item_id: str, item: Item) -> Item:
stored_item_data = items[item_id]
stored_item_model = Item(**stored_item_data)
update_data = item.model_dump(exclude_unset=True)
updated_item = stored_item_model.model_copy(update=update_data)
items[item_id] = jsonable_encoder(updated_item)
return updated_item
이 로직을 한 줄씩 짚어 볼게요.
stored_item_data = items[item_id]— 현재 저장돼 있는 데이터를dict로 가져와요.stored_item_model = Item(**stored_item_data)— 그dict로Item모델을 다시 만들어요. 그래야model_copy같은 모델 메서드를 쓸 수 있어요.update_data = item.model_dump(exclude_unset=True)— 요청에서 실제로 설정된 필드만 담은dict를 만듭니다. 기본값은 빠져요.updated_item = stored_item_model.model_copy(update=update_data)— 저장돼 있던 모델을 복사하면서, 요청으로 온 값들만 덮어써요.items[item_id] = jsonable_encoder(updated_item)— 결과를 JSON으로 저장 가능한 형태로 바꿔서 다시 저장해요.
exclude_unset=True 덕분에, 클라이언트가 tax를 보내지 않으면 기존 저장값 20.2가 그대로 유지돼요. PUT과 달리 빠진 필드가 기본값으로 채워지지 않아요. 이것이 PATCH로 "부분 수정"을 구현하는 핵심이에요.