13장. REST API 와 FastAPI
Pydantic 으로 입력 검사
은행 창구의 신청서에는 칸마다 규칙이 있습니다. 생년월일은 숫자 여덟 자리, 성별은 고르는 칸입니다. 칸이 비거나 규칙에 어긋나면 창구에서 바로 돌려보냅니다. Pydantic 모델은 요청 본문의 신청서입니다. 칸 이름과 타입, 범위, 고를 수 있는 값을 정해 두면, 어긋난 요청은 모델에 닿기 전에 422 로 돌려보냅니다.
1번에서 JSON 본문이 옵니다. 2번에서 칸마다 타입(정수, 실수, 글)이 맞는지, 3번에서 범위(0~23시)와 고를 수 있는 값(서울, 경기, 인천, 지방)에 드는지 봅니다. 4번에서 모두 통과하면 경로 함수가 검사된 객체를 받고, 하나라도 어긋나면 어느 칸이 왜 틀렸는지 적힌 422 가 돌아갑니다. 여기서 신청서의 칸 규칙이 Pydantic 의 필드 정의입니다.
칸 규칙 적는 법
| 하고 싶은 것 | 적는 법 | 어긋나면 |
|---|---|---|
| 타입 | 거리km: float | "숫자가 아니다" |
| 범위 | 주문시각: int = Field(ge=0, le=23) | "23 보다 작거나 같아야 한다" |
| 고르는 값 | 지역: Literal["서울", "경기", "인천", "지방"] | "다음 가운데 하나여야 한다" |
| 글 길이 | 질문: str = Field(min_length=2, max_length=200) | "적어도 2글자" |
| 목록 길이 | 주문들: list[Order] = Field(max_length=500) | "500개를 넘을 수 없다" |
| 선택 칸 | 날씨: str = "맑음" | 빠지면 기본값 |
검사해 보기
from typing import Literal
from pydantic import BaseModel, Field, ValidationError
class Order(BaseModel):
주문시각: int = Field(ge=0, le=23)
지역: Literal["서울", "경기", "인천", "지방"]
print(Order(주문시각=9, 지역="서울"))
try:
Order(주문시각=25, 지역="부산")
except ValidationError as e:
for err in e.errors(): # 어긋난 칸마다 위치(loc)와 까닭(msg)
print(err["loc"], err["msg"])코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
class Order(BaseModel) | BaseModel(바탕 모델)을 물려받아 요청 본문의 칸을 정하는 클래스다 |
Field(ge=0, le=23) | 칸의 규칙이다. ge 는 greater than or equal(크거나 같다), le 는 less than or equal(작거나 같다)이다 |
Literal["서울", "경기", "인천", "지방"] | literal(글자 그대로의)이라는 뜻이다. 적어 둔 값 가운데 하나만 받는다 |
ValidationError | 검사에 어긋나면 나는 예외다. validation 은 "검증"이라는 뜻이다 |
e.errors() | 어긋난 칸마다의 정보를 목록으로 준다 |
err["loc"], err["msg"] | loc 은 location(위치), 곧 어느 칸인지이고, msg 는 message(까닭을 적은 글)다 |
이 코드가 뜻하는 것
은행 창구 직원은 신청서를 받으면 칸마다 규칙에 맞는지 한꺼번에 보고, 틀린 칸에 모두 표시해서 돌려줍니다. 하나 고치고 다시 오고, 또 하나 고치고 다시 오게 하지 않습니다. 여기서 신청서 양식이 Order 클래스, 칸 규칙이 Field 와 Literal, 틀린 칸 표시가 e.errors() 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
class Order(BaseModel): ... | 주문시각과 지역 두 칸의 규칙을 정한다 | 신청서 양식 |
Order(주문시각=9, 지역="서울") | 규칙에 맞는 값으로 만든다 | 바르게 쓴 신청서 |
Order(주문시각=25, 지역="부산") | 두 칸이 모두 어긋난 값으로 만들어 본다 | 두 칸을 틀리게 쓴 신청서 |
except ValidationError as e: | 검사 실패를 잡는다 | 창구에서 돌려받음 |
for err in e.errors(): ... | 틀린 칸마다 위치와 까닭을 보인다 | 빨간 펜 표시 읽기 |
실행하면 첫 줄에 주문시각=9 지역='서울' 이 나오고, 이어서 ('주문시각',) Input should be less than or equal to 23 과 ('지역',) Input should be '서울', '경기', '인천' or '지방' 이 나옵니다. "23 보다 작거나 같아야 한다", "넷 가운데 하나여야 한다"는 뜻이고, 틀린 두 칸이 한 번에 모두 알려졌습니다.
작게 줄여서 눈으로 보기
기본값이 있는 칸과, 숫자처럼 생긴 글이 오는 경우를 봅니다.
from typing import Literal
from pydantic import BaseModel, Field
class Order(BaseModel):
주문시각: int = Field(ge=0, le=23)
지역: Literal["서울", "경기", "인천", "지방"]
날씨: str = "맑음"
print(Order(주문시각=9, 지역="서울"))
print(Order(주문시각="9", 지역="경기", 날씨="비"))
print(Order.model_validate({"주문시각": 23, "지역": "지방"}).model_dump())주문시각=9 지역='서울' 날씨='맑음'
주문시각=9 지역='경기' 날씨='비'
{'주문시각': 23, '지역': '지방', '날씨': '맑음'}| 결과 | 읽는 법 |
|---|---|
| 첫 줄 | 날씨를 주지 않아 기본값 "맑음"이 들어갔다 |
| 둘째 줄 | 글 "9" 를 주었지만 정수 9 로 바뀌었다. 숫자로 읽을 수 있는 글은 Pydantic 이 기본으로 바꿔 준다 |
| 셋째 줄 | model_validate 는 사전을 받아 검사하고, model_dump 는 검사한 객체를 다시 사전으로 돌려준다. JSON 본문이 들어오고 나갈 때 FastAPI 가 하는 일과 같다 |
주의할 점
- 입력 검사는 모델이 학습한 범위를 지키는 장치이기도 합니다. 학습 데이터에 없던 "부산"이 오면 모델은 그럴듯한 숫자를 내놓지만 믿을 수 없습니다.
- 칸 이름을 한글로 쓰면 파이썬 이름과 JSON 칸 이름이 같아져 읽기 쉽습니다. 다른 팀이나 외부와 주고받는 API 라면 영어 칸 이름을 쓰는 것이 흔합니다.