WeniVooks

검색

FastAPI 베이스캠프

응답 모델 사용하기

1. 라우팅 및 세팅

1.1 URL 정보

이번 챕터의 URL 구성은 아래와 같습니다.

경로 함수명 메서드 설명
/ index GET 들어온 값을 그대로 출력합니다.
/item item_list GET 물품 목록을 반환합니다.
/item item_create POST 물품을 등록합니다.
/item/{item_id} item_detail GET 물품 상세 정보를 반환합니다.
1.2 기본 세팅

이번 실습 폴더는 02_5_model입니다. VSC 터미널에서 사용할 명령어 입니다. 가상환경은 벗어난 상태에서 실행해야 합니다. 만약 터미널 입력창 앞에 (venv)라고 되어 있다면 deactivate 명령어로 가상환경을 나간 상태에서 cd ..으로 상위 폴더로 나와 아래 명령어를 실행해주세요.

mkdir 02_5_model
cd 02_5_model
python -m venv venv
.\venv\Scripts\activate
pip install fastapi
pip install uvicorn
mkdir 02_5_model
cd 02_5_model
python -m venv venv
.\venv\Scripts\activate
pip install fastapi
pip install uvicorn

2. 응답 모델 소개

응답 모델은 API가 반환하는 데이터의 구조를 정의합니다. FastAPI에서는 Pydantic 모델을 사용하여 응답 데이터의 형식을 명확히 지정할 수 있습니다. API 문서화 또한 자동으로 이루어집니다.

Pydantic은 Python의 타입 힌트를 사용하여 데이터 유효성 검사와 직렬화를 수행하는 라이브러리입니다. FastAPI는 Pydantic을 기반으로 하여 API의 요청과 응답을 검증하고 직렬화합니다. FastAPI를 설치하면 Pydantic도 함께 설치됩니다. Python 라이브러리이기 때문에 FastAPI 없이 Pydantic 단독으로도 사용할 수 있습니다. 아래 코드는 colab에서도 사용이 가능합니다.

아래 코드를 이해하지 못하더라도 수업을 진행하는 것에는 문제가 없으니 가볍게 읽어보세요.

from pydantic import BaseModel, ValidationError
from typing import List
 
class User(BaseModel):
    name: str # 필수 필드
    age: int # 필수 필드
    email: str # 필수 필드
    hobbies: List[str] | None = None # 선택적 필드, 기본값은 None
 
# 유효성 검사 및 객체 생성 함수
def validate_user(user_data: dict) -> User:
    try:
        return User(**user_data)
    except ValidationError as e:
        print(f"유효성 검사 오류: {e}")
        return None
 
# 사용 예시
# 올바른 데이터
valid_data = {
    "name": "홍길동",
    "age": 30,
    "email": "hong@example.com",
    "hobbies": ["독서", "등산"]
}
 
# 잘못된 데이터
invalid_data = {
    "name": "김철수",
    "age": "스물다섯",  # 문자열로 잘못 입력됨
    "email": "invalid-email",  # 올바르지 않은 이메일 형식
    "hobbies": "독서"  # 문자열 대신 리스트여야 함
}
 
# 올바른 데이터로 사용자 생성
user = validate_user(valid_data)
if user:
    print("유효한 사용자:", user)
    print("직렬화된 사용자:", user.model_dump())
 
# 잘못된 데이터로 사용자 생성 시도
invalid_user = validate_user(invalid_data)
from pydantic import BaseModel, ValidationError
from typing import List
 
class User(BaseModel):
    name: str # 필수 필드
    age: int # 필수 필드
    email: str # 필수 필드
    hobbies: List[str] | None = None # 선택적 필드, 기본값은 None
 
# 유효성 검사 및 객체 생성 함수
def validate_user(user_data: dict) -> User:
    try:
        return User(**user_data)
    except ValidationError as e:
        print(f"유효성 검사 오류: {e}")
        return None
 
# 사용 예시
# 올바른 데이터
valid_data = {
    "name": "홍길동",
    "age": 30,
    "email": "hong@example.com",
    "hobbies": ["독서", "등산"]
}
 
# 잘못된 데이터
invalid_data = {
    "name": "김철수",
    "age": "스물다섯",  # 문자열로 잘못 입력됨
    "email": "invalid-email",  # 올바르지 않은 이메일 형식
    "hobbies": "독서"  # 문자열 대신 리스트여야 함
}
 
# 올바른 데이터로 사용자 생성
user = validate_user(valid_data)
if user:
    print("유효한 사용자:", user)
    print("직렬화된 사용자:", user.model_dump())
 
# 잘못된 데이터로 사용자 생성 시도
invalid_user = validate_user(invalid_data)

여기서 입력값에 대한 유효성 검사를 수행하고, 유효한 경우 사용자 객체를 생성합니다. 이렇게 타입 힌트를 사용하여 데이터 유효성 검사를 수행하는 것은 Pydantic의 주요 기능 중 하나입니다.

여기서 hobbies: List[str] | None = None 부분은 선택적 필드를 정의하는 방법입니다. List[str]는 문자열 리스트를 의미하며, | None은 해당 타입이 리스트 또는 None 필드임을 나타냅니다. 선택적 필드는 필수가 아니며, 입력되지 않을 경우 기본값인 = None이 사용됩니다. 이 부분은 다음 챕터에서 자세히 다루겠습니다.

직렬화는 파이썬의 객체를 JSON 형식으로 변환하는 과정을 의미합니다. Pydantic은 이러한 직렬화를 자동으로 수행합니다. model_dump() 메서드를 사용하면 Pydantic 모델을 JSON 형식으로 변환할 수 있습니다.

3. 기본 응답 모델 사용하기

응답 모델을 사용하려면 먼저 Pydantic 모델을 정의합니다. Item을 생성하는 코드를 작성해보겠습니다.

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post("/item/")
async def create_item(item: Item):
    items.append(item)
    return {"message": "Item created successfully", "item": item}
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post("/item/")
async def create_item(item: Item):
    items.append(item)
    return {"message": "Item created successfully", "item": item}
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")

이 예제에서 Item 모델은 응답의 구조를 정의합니다. FastAPI는 반환된 데이터를 이 모델에 맞게 검증하고 직렬화합니다. 직렬화란 파이썬 객체를 JSON 형식으로 변환하는 과정을 의미합니다. 만약 반환된 데이터가 모델과 일치하지 않는다면 FastAPI는 에러를 반환합니다. 예를 들어, 필수 필드인 name이 누락되었다면 FastAPI는 애러가 발생합니다.

아래와 같이 실행하여 FastAPI 서버를 실행합니다.

uvicorn main:app --reload
uvicorn main:app --reload

이제 썬더 클라이언트로 다음 URL에 POST 요청을 보내보세요.

POST http://127.0.0.1:8000/item/

{
    "name": "item1",
    "price": 100
}
POST http://127.0.0.1:8000/item/

{
    "name": "item1",
    "price": 100
}

이제 GET 요청을 보내보세요.

GET http://127.0.0.1:8000/item/
GET http://127.0.0.1:8000/item/

이때 보내는 데이터를 아래와 같이 변경해도 price는 자동으로 숫자로 변경이 됩니다.

{
    "name": "item1",
    "price": "100"
}
{
    "name": "item1",
    "price": "100"
}

다만 숫자로 변경할 수 없는 데이터를 보내면 오류가 발생합니다.

{
    "name": "item1",
    "price": "hello"
}
{
    "name": "item1",
    "price": "hello"
}

메시지는 아래와 같습니다.

{
    "detail": [
        {
            "type": "float_parsing",
            "loc": [
                "body",
                "price"
            ],
            "msg": "Input should be a valid number, unable to parse string as a number",
            "input": "hello"
        }
    ]
}
{
    "detail": [
        {
            "type": "float_parsing",
            "loc": [
                "body",
                "price"
            ],
            "msg": "Input should be a valid number, unable to parse string as a number",
            "input": "hello"
        }
    ]
}

이 메시지는 price 필드가 숫자로 변환할 수 없는 문자열을 포함하고 있음을 알려줍니다. 이러한 기능은 Pydantic이 제공하는 기능 중 하나입니다.

4. 상태 코드 설정

FastAPI에서는 status_code 매개변수를 사용하여 응답의 HTTP 상태 코드를 설정할 수 있습니다.

from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post("/item/", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    items.append(item)
    return {"message": "Item created successfully", "item": item}
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post("/item/", status_code=status.HTTP_201_CREATED)
async def create_item(item: Item):
    items.append(item)
    return {"message": "Item created successfully", "item": item}
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")

이 예제에서는 아이템 생성 시 201 Created 상태 코드를 반환합니다. 썬더 클라이언트로 아래 URL에 POST 요청을 보내보세요. 참고로 이 코드는 숫자로 입력해도 됩니다.

POST http://127.0.0.1:8000/items
Content-Type: application/json

{
    "name": "item1",
    "price": 100
}
POST http://127.0.0.1:8000/items
Content-Type: application/json

{
    "name": "item1",
    "price": 100
}

아래 이미지처럼 201 Created 상태 코드를 확인할 수 있습니다. 이번에는 응답 코드를 @app.post("/item/", status_code=404)와 같이 변경하고 post를 실행해보세요. 실제 데이터는 저장되지만 상태 코드가 404 Not Found로 변경됩니다.

이러한 상태 코드는 \venv\Lib\site-packages\starlette\status.py에 위치하고 있습니다. starlette도 FastAPI와 함께 설치되는 모듈입니다. FastAPI와 별개로도 사용할 수 있습니다.

5. 응답 모델의 필드 제어

때로는 모델의 일부 필드만 응답에 포함시키고 싶을 수 있습니다. FastAPI는 이를 위한 몇 가지 옵션을 제공합니다.

5.1 응답에서 특정 필드 제외하기
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post(
    "/item/",
    response_model=Item,
    status_code=status.HTTP_201_CREATED,
    response_model_exclude={"price"},
)
async def create_item(item: Item):
    items.append(item)
    return item
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post(
    "/item/",
    response_model=Item,
    status_code=status.HTTP_201_CREATED,
    response_model_exclude={"price"},
)
async def create_item(item: Item):
    items.append(item)
    return item
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")

이 예제에서는 price 필드가 응답에서 제외됩니다.

5.2 응답에 특정 필드만 포함하기
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post(
    "/item/",
    response_model=Item,
    status_code=status.HTTP_201_CREATED,
    response_model_include={"name", "price"},
)
async def create_item(item: Item):
    items.append(item)
    return item
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post(
    "/item/",
    response_model=Item,
    status_code=status.HTTP_201_CREATED,
    response_model_include={"name", "price"},
)
async def create_item(item: Item):
    items.append(item)
    return item
 
 
@app.get("/item/")
async def read_items():
    return items
 
 
@app.get("/item/{item_id}")
async def read_item(item_id: int):
    if 0 <= item_id < len(items):
        return items[item_id]
    raise HTTPException(status_code=404, detail="Item not found")

이 예제에서는 nameprice 필드만 응답에 포함됩니다. 여기서 name만 남기게 되면 price는 자동으로 제외됩니다.

6. 여러 가지 응답 모델 사용하기

때로는 하나의 엔드포인트가 여러 가지 다른 응답을 반환해야 할 수 있습니다. 이런 경우 Union 타입을 사용할 수 있습니다.

from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
class Message(BaseModel):
    message: str
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post("/item/")
async def create_item(item: Item):
    items.append(item)
    return item
 
 
@app.get("/item/{item_id}", response_model=Union[Item, Message])
async def read_item(item_id: int):
    if item_id == 1:
        return items[item_id - 1]
    else:
        return Message(message="Item not found")
from fastapi import FastAPI
from pydantic import BaseModel
from typing import Union
 
app = FastAPI()
 
 
# Pydantic 모델 정의
class Item(BaseModel):
    name: str
    price: float
 
 
class Message(BaseModel):
    message: str
 
 
# 메모리에 데이터를 저장할 리스트
items = []
 
 
@app.post("/item/")
async def create_item(item: Item):
    items.append(item)
    return item
 
 
@app.get("/item/{item_id}", response_model=Union[Item, Message])
async def read_item(item_id: int):
    if item_id == 1:
        return items[item_id - 1]
    else:
        return Message(message="Item not found")

이 예제에서는 아이템이 존재하면 Item 모델을, 그렇지 않으면 Message 모델을 반환합니다.

연습문제

  1. 사용자 정보를 반환하는 API를 만들어보세요. 응답 모델은 다음 필드를 포함해야 합니다: id, username, email. 그러나 실제 데이터베이스에는 password 필드도 있다고 가정하고, 이 필드는 응답에서 제외되어야 합니다.

  2. 상품 목록을 반환하는 API를 만들어보세요. 각 상품은 id, name, price, stock을 가지고 있습니다. API는 두 가지 모드를 가집니다:

    • 기본 모드: id, name, price만 반환
    • 상세 모드: 모든 필드 반환 쿼리 파라미터를 사용하여 모드를 선택할 수 있도록 구현해보세요.
2.4 쿼리 매개변수 처리3장 CRUD 애플리케이션 만들기