데이터 업데이트

데이터 업데이트 (Body - Updates)

기존 데이터를 갱신하는 건 API에서 흔한 일이죠. FastAPI는 눈여겨볼 점이 하나 있는데, HTTP의 PUTPATCH를 어떻게 나눠 쓰는 게 의도된 방식인지를 보여줘요. PUT은 "통째로 교체", PATCH는 "부분 수정"이라는 뉘앙스예요.

출처: 공식문서

PUT으로 교체 업데이트하기 (Update replacing with PUT)

아이템을 업데이트하려면 HTTP PUT 연산을 쓰면 돼요.

jsonable_encoder를 사용하면 입력 데이터를 JSON으로 저장 가능한 데이터로 변환할 수 있어요(예: NoSQL 데이터베이스에 저장). 예를 들어 datetimestr로 변환해 주죠.

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를 사용해요. 그래서 저장되는 데이터도 그 "새로운" tax10.5가 돼요. PUT이 온전히 교체이기 때문에, 빠진 필드(여기선 tax)는 기본값으로 채워지게 되는 거예요.

PATCH로 부분 업데이트하기 (Partial updates with PATCH)

부분 업데이트에는 HTTP PATCH 연산을 쓸 수 있어요. 업데이트할 데이터만 보내고, 나머지는 그대로 두는 방식이죠.

참고: PATCHPUT만큼 흔히 쓰이거나 잘 알려지진 않았어요. 많은 팀이 부분 업데이트조차 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

이 로직을 한 줄씩 짚어 볼게요.

  1. stored_item_data = items[item_id] — 현재 저장돼 있는 데이터를 dict로 가져와요.
  2. stored_item_model = Item(**stored_item_data) — 그 dictItem 모델을 다시 만들어요. 그래야 model_copy 같은 모델 메서드를 쓸 수 있어요.
  3. update_data = item.model_dump(exclude_unset=True) — 요청에서 실제로 설정된 필드만 담은 dict를 만듭니다. 기본값은 빠져요.
  4. updated_item = stored_item_model.model_copy(update=update_data) — 저장돼 있던 모델을 복사하면서, 요청으로 온 값들만 덮어써요.
  5. items[item_id] = jsonable_encoder(updated_item) — 결과를 JSON으로 저장 가능한 형태로 바꿔서 다시 저장해요.

exclude_unset=True 덕분에, 클라이언트가 tax를 보내지 않으면 기존 저장값 20.2가 그대로 유지돼요. PUT과 달리 빠진 필드가 기본값으로 채워지지 않아요. 이것이 PATCH로 "부분 수정"을 구현하는 핵심이에요.

더 알아보기 (Learn more)