FastAPI에서 응답 모델(response_model)은 특정 경로 작동 함수의 응답 데이터 구조를 미리 정의하는 데 사용됩니다. 이는 데이터를 검증하고, 자동으로 문서화하며, 출력 데이터의 범위를 제한하는 데 유용합니다. 또한, API 클라이언트가 기대하는 응답 데이터의 구조를 명확히 하여 보안성 높은 API를 구축하는 데 도움이 됩니다.
경로 작동 함수에서 response_model을 지정하면, 해당 함수가 반환할 데이터의 구조를 정의할 수 있습니다. FastAPI는 이 모델을 사용하여 데이터를 자동으로 검증하고, 변환하며, 문서화합니다.
어떤 경로 작동이든 매개변수 response_model를 사용하여 응답을 위한 모델을 선언할 수 있습니다:
@app.get()@app.post()@app.put()@app.delete()기타.from typing import Any, List, Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: Union[float, None] = None
tags: List[str] = []
@app.post("/items/", response_model=Item)
async def create_item(item: Item) -> Any:
return item
@app.get("/items/", response_model=List[Item])
async def read_items() -> Any:
return [
{"name": "Portal Gun", "price": 42.0},
{"name": "Plumbus", "price": 32.0},
]
Item 모델의 구조를 따르며, 응답 데이터 역시 이 모델의 구조에 따라 반환됩니다.-> FastAPI는 이 response_model를 사용하여:
해당 모델의 출력 데이터 제한 이것이 매우 중요하다!
API에서 입력 모델과 응답 모델을 구분하면, 보안성과 데이터 처리 효율성을 높일 수 있습니다. 특히 비밀번호와 같은 민감한 데이터를 응답에서 제외하는 것이 가능합니다.
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
return user # 위험: 비밀번호가 그대로 반환됩니다.
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
return user # 안전: 비밀번호는 반환되지 않음.
평문 비밀번호를 포함하는 UserIn 모델을 선언합니다:
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
return user
그리고 이 모델을 사용하여 입력을 선언하고 같은 모델로 출력을 선언합니다:
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
# Don't do this in production!
@app.post("/user/")
async def create_user(user: UserIn) -> UserIn:
return user
이제 브라우저가 비밀번호로 사용자를 만들 때마다 API는 응답으로 동일한 비밀번호를 반환합니다.
이 경우, 사용자가 스스로 비밀번호를 발신했기 때문에 문제가 되지 않을 수 있습니다.
그러나 동일한 모델을 다른 경로 작동에서 사용할 경우, 모든 클라이언트에게 사용자의 비밀번호를 발신할 수 있습니다.
"위험"
절대로 사용자의 평문 비밀번호를 저장하거나 응답으로 발신하지 마십시오.
대신 평문 비밀번호로 입력 모델을 만들고 해당 비밀번호 없이 출력 모델을 만들 수 있습니다:
from typing import Any, Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
return user
여기서 경로 작동 함수가 비밀번호를 포함하는 동일한 입력 사용자를 반환할지라도:
from typing import Any, Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
return user
...response_model을 UserOut 모델로 선언했기 때문에 비밀번호를 포함하지 않습니다:
from typing import Any, Union
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: Union[str, None] = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: Union[str, None] = None
@app.post("/user/", response_model=UserOut)
async def create_user(user: UserIn) -> Any:
return user
따라서 FastAPI는 출력 모델에서 선언하지 않은 모든 데이터를 (Pydantic을 사용하여) 필터링합니다.
응답 모델은 아래와 같이 기본값을 가짐
from typing import List, Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
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, response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]
예를 들어, NoSQL 데이터베이스에 많은 선택적 속성이 있는 모델이 있지만, 기본값으로 가득 찬 매우 긴 JSON 응답을 보내고 싶지 않습니다.
FastAPI는 응답 데이터에서 기본값이 설정된 필드나 None 값을 필터링할 수 있는 여러 옵션을 제공합니다.
from typing import List, Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
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, response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: Union[str, None] = None
price: float
tax: float = 10.5
items = {
"foo": {"name": "Foo", "price": 50.2},
"bar": {"name": "Bar", "description": "The Bar fighters", "price": 62, "tax": 20.2},
"baz": {
"name": "Baz",
"description": "There goes my baz",
"price": 50.2,
"tax": 10.5,
},
}
@app.get(
"/items/{item_id}/name",
response_model=Item,
response_model_include={"name", "description"},
)
async def read_item_name(item_id: str):
return items[item_id]
@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
return items[item_id]
None 값을 가진 필드는 응답에서 제외.@app.get("/items/{item_id}", response_model=Item, response_model_exclude_unset=True)
async def read_item(item_id: str):
return items[item_id]
이 옵션을 사용하면 기본값이 설정된 필드는 응답에서 제외되며, 오직 명시적으로 설정된 필드만 포함됩니다. 예를 들어, 다음과 같은 응답이 가능합니다:
{
"name": "Foo",
"price": 50.2
}
응답 모델에서 특정 필드만 포함하거나 제외하고 싶은 경우 response_model_include와 response_model_exclude를 사용할 수 있습니다.
@app.get("/items/{item_id}/name", response_model=Item, response_model_include={"name", "description"})
async def read_item_name(item_id: str):
return items[item_id]
response_model_include: 응답에서 포함할 필드를 지정합니다.@app.get("/items/{item_id}/public", response_model=Item, response_model_exclude={"tax"})
async def read_item_public_data(item_id: str):
return items[item_id]
response_model_exclude: 응답에서 제외할 필드를 지정합니다.
필드 제한: 특정 필드를 포함하거나 제외함으로써 더 맞춤화된 응답 데이터를 제공할 수 있습니다.
FastAPI는 기본값이 설정된 필드와 명시적으로 설정된 값을 구분할 수 있습니다. 기본값과 동일한 데이터가 설정되었더라도, 명시적으로 설정되었다면 응답에 포함됩니다.
{
"name": "Baz",
"description": null,
"price": 50.2,
"tax": 10.5,
"tags": []
}
response_model_include와 response_model_exclude를 사용하여 특정 필드만 포함하거나 제외할 수 있습니다.FastAPI의 응답 모델을 통해 API 응답 데이터를 체계적이고 안전하게 관리할 수 있으며, 자동 검증 및 문서화 기능을 활용하여 유지보수성과 보안을 높일 수 있습니다.
FastAPI에서 추가 모델은 주로 여러 가지 사용자 상태에 맞춰 각기 다른 데이터를 관리할 때 사용됩니다. 예를 들어, 입력 시 비밀번호를 받지만, 출력 시 비밀번호는 제외하고, 데이터베이스 모델에는 해시된 비밀번호를 저장하는 경우입니다. FastAPI는 이처럼 상황에 따라 다른 모델을 사용하는 유연성을 제공합니다.
복잡한 시스템에서는 여러 모델을 사용하여 데이터를 다루는 것이 일반적입니다. 특히 사용자 모델의 경우, 입력, 출력, 데이터베이스 저장용으로 각각 다른 모델을 사용할 수 있습니다.
from fastapi import FastAPI
from pydantic import BaseModel, EmailStr
app = FastAPI()
class UserIn(BaseModel):
username: str
password: str
email: EmailStr
full_name: str | None = None
class UserOut(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
class UserInDB(BaseModel):
username: str
hashed_password: str
email: EmailStr
full_name: str | None = None
def fake_password_hasher(raw_password: str):
return "supersecret" + raw_password
def fake_save_user(user_in: UserIn):
hashed_password = fake_password_hasher(user_in.password)
user_in_db = UserInDB(**user_in.dict(), hashed_password=hashed_password)
print("User saved! ..not really")
return user_in_db
@app.post("/user/", response_model=UserOut)
async def create_user(user_in: UserIn):
user_saved = fake_save_user(user_in)
return user_saved
이러한 방식으로 입력, 출력, 저장에 맞는 각기 다른 모델을 사용할 수 있습니다.
Pydantic 모델에서 데이터를 dict() 또는 model_dump()로 변환하여 Python의 dict로 만들 수 있습니다. 이를 통해 모델 데이터를 쉽게 관리할 수 있으며, 함수나 다른 클래스에 전달할 때 매우 유용합니다.
user_in = UserIn(username="john", password="secret", email="john.doe@example.com")
user_dict = user_in.dict()
print(user_dict)
출력은 다음과 같습니다:
{
"username": "john",
"password": "secret",
"email": "john.doe@example.com",
"full_name": None
}
**user_dict를 사용하여 Pydantic 모델을 다른 함수나 클래스에 전달할 수 있습니다. 예를 들어:
UserInDB(**user_dict)
이 코드는 UserInDB 클래스에 user_dict의 데이터를 각각의 인자로 전달합니다.
모델 간의 중복을 줄이기 위해 상속을 사용하여 공통 속성을 한 번만 선언하고, 필요한 부분만 각 모델에서 추가로 정의할 수 있습니다. 이를 통해 코드의 유지보수성과 재사용성을 높일 수 있습니다.
class UserBase(BaseModel):
username: str
email: EmailStr
full_name: str | None = None
class UserIn(UserBase):
password: str
class UserOut(UserBase):
pass
class UserInDB(UserBase):
hashed_password: str
UserBase를 상속하여 비밀번호 필드 추가.UserBase를 상속하여 비밀번호 없이 출력되는 모델.UserBase를 상속하여 해시된 비밀번호가 포함된 모델.이 방식으로 코드를 더 간결하게 만들 수 있으며, 변경이 필요할 때 한 곳만 수정해도 모든 관련 모델에 반영됩니다.
Union을 사용하면 여러 가지 다른 모델 중 하나를 반환할 수 있습니다. 예를 들어, 차(Car)나 비행기(Plane)의 정보를 다룰 때 둘 중 하나의 모델을 반환하는 경우에 유용합니다.
from typing import Union
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class BaseItem(BaseModel):
description: str
type: str
class CarItem(BaseItem):
type: str = "car"
class PlaneItem(BaseItem):
type: str = "plane"
size: int
items = {
"item1": {"description": "All my friends drive a low rider", "type": "car"},
"item2": {"description": "Music is my aeroplane", "type": "plane", "size": 5},
}
@app.get("/items/{item_id}", response_model=Union[PlaneItem, CarItem])
async def read_item(item_id: str):
return items[item_id]
이 예시에서, PlaneItem과 CarItem 중 하나의 모델을 반환할 수 있습니다.
리스트나 딕셔너리 형태의 응답을 반환할 수도 있습니다. FastAPI는 Python의 기본 타입인 list와 dict를 사용할 수 있으며, 이를 통해 복잡한 데이터를 쉽게 처리할 수 있습니다.
@app.get("/items/", response_model=list[Item])
async def read_items():
return [
{"name": "Foo", "description": "There comes my hero"},
{"name": "Red", "description": "It's my aeroplane"},
]
@app.get("/keyword-weights/", response_model=dict[str, float])
async def read_keyword_weights():
return {"foo": 2.3, "bar": 3.4}
dict() 또는 model_dump()로 Pydantic 모델을 쉽게 변환하고 관리할 수 있습니다.Union을 사용하여 더 유연한 API를 설계할 수 있습니다.FastAPI의 추가 모델 기능을 활용하면, 입력, 출력, 데이터베이스 상태에 맞는 유연한 API를 설계할 수 있으며, 데이터 변환과 검증을 더욱 철저하게 수행할 수 있다.
FastAPI에서는 HTTP 응답 상태 코드를 직접 지정하여 반환할 수 있습니다. 각 상태 코드는 클라이언트가 요청에 대해 서버가 어떻게 처리했는지 알 수 있는 중요한 정보를 제공합니다. 상태 코드를 지정하는 것은 단순히 API의 동작을 명시하는 것뿐만 아니라, API 문서화에도 매우 유용하게 사용됩니다.
FastAPI에서 경로 작동 함수에 status_code 매개변수를 사용하여 특정 HTTP 상태 코드를 설정할 수 있습니다.
@app.get()@app.post()@app.put()@app.delete()from fastapi import FastAPI
app = FastAPI()
@app.post("/items/", status_code=201)
async def create_item(name: str):
return {"name": name}
POST 요청이 성공적으로 수행되어 새로운 리소스가 생성되었음을 의미합니다. 200 상태 코드로, 이는 "요청이 성공적으로 완료됨"을 의미합니다.status_code는 FastAPI의 데코레이터 메소드에서 사용됩니다. 즉, @app.get(), @app.post() 등의 매개변수로 사용되며, 함수 내에서 직접 호출되는 것이 아닙니다.
HTTP 상태 코드는 크게 다섯 가지로 분류됩니다. 각 범위의 코드는 특정 유형의 응답을 나타냅니다.
200 OK: 요청이 성공적으로 처리됨.201 Created: 새로운 리소스가 생성됨.204 No Content: 처리되었지만, 응답 본문이 없음.400 Bad Request: 잘못된 요청.404 Not Found: 리소스를 찾을 수 없음.http.HTTPStatus 사용상태 코드는 숫자로만 제공되는 것이 아니라, IntEnum 클래스인 http.HTTPStatus 또는 fastapi.status에서 제공하는 상수로도 사용할 수 있습니다. 이를 사용하면 코드의 가독성을 높이고, 자동완성 기능도 지원됩니다.
from fastapi import FastAPI, status
app = FastAPI()
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(name: str):
return {"name": name}
여기서 status.HTTP_201_CREATED는 상태 코드 201을 의미하며, 이는 편리하게 사용 가능합니다.
status_code 매개변수는 응답의 상태 코드를 반환하는 것뿐만 아니라, 해당 정보를 OpenAPI 스키마와 API 문서에 포함시킵니다. 이는 클라이언트 개발자나 다른 API 사용자들이 어떤 상황에서 어떤 상태 코드가 반환되는지 미리 파악할 수 있도록 해줍니다.
일부 상태 코드는 본문을 포함하지 않는 경우가 있습니다. 예를 들어:
FastAPI는 이러한 상태 코드에 대한 응답이 본문을 포함하지 않도록 자동으로 처리합니다.
모든 상태 코드와 그 의미를 외우는 것은 어렵습니다. FastAPI는 이를 돕기 위해 fastapi.status 또는 starlette.status 모듈을 통해 자주 사용하는 상태 코드에 대한 직관적인 상수를 제공합니다. 이를 통해 코드를 더 읽기 쉽고 이해하기 쉽게 만들 수 있습니다.
from fastapi import FastAPI, status
app = FastAPI()
@app.post("/items/", status_code=status.HTTP_201_CREATED)
async def create_item(name: str):
return {"name": name}
이 방식은 숫자 대신 상수로 상태 코드를 지정할 수 있어 코드의 가독성을 높여줍니다.
기본적으로 FastAPI는 성공적인 요청에 대해 200 상태 코드를 반환합니다. 하지만 상태 코드를 명시적으로 변경하려면, status_code 매개변수를 사용하여 원하는 코드로 설정할 수 있습니다.
이처럼 FastAPI의 응답 상태 코드를 적절히 사용하면, 명확하고 직관적인 API 설계가 가능합니다.