[250828목1994H] Backend & FastAPI (2)

윤승호·2025년 8월 28일

FastAPI 라서 그런지 진도 나가는 속도마저 Fast 하다.

학습시간 09:00~02:00(당일17H/누적1994H)


◆ 학습내용

1. Pydantic

  • 파이썬의 타입 힌트(type hint)를 사용해 데이터의 구조와 타입을 강제하는 라이브러리
  • 복잡한 JSON 데이터를 단순한 파이썬 객체처럼 다룰 수 있게 해줌
  • FastAPI는 이 Pydantic 모델을 보고 자동으로 다음 기능들을 수행
    • 들어오는 요청(request) 데이터의 유효성 검사 (validation)
    • 나가는 응답(response) 데이터를 특정 형태로 변환 (serialization)
    • API 자동 문서 생성 (Swagger UI, ReDoc)

(1) 기본 타입 설정

from pydantic import BaseModel
from typing import Optional

# Item 데이터의 설계도(스키마) 정의
class Item(BaseModel):
    name: str  # 필수 필드, 문자열 타입
    price: float  # 필수 필드, 부동소수점 타입
    description: Optional[str] = None  # 선택적 필드, 문자열 타입, 기본값은 None
    tax: Optional[float] = None # 선택적 필드, 부동소수점 타입, 기본값은 None

(2) Field를 이용한 상세 유효성 검사

  • Pydantic의 Field 함수를 사용하면 단순한 타입 검사 외에 훨씬 상세한 제약조건을 걸 수 있음
  • 예를 들어, 숫자의 최솟값/최댓값, 문자열의 길이, 정규식 패턴 등을 강제할 수 있어
  • API의 안정성을 엄청나게 높여주는 핵심 기능
  • 들어오는 데이터에 대한 방어 로직을 서비스 코드 대신 스키마 단에서 깔끔하게 처리 가능
from pydantic import BaseModel, Field
import re

class Product(BaseModel):
    # 가격은 0보다 커야 함 (gt=greater than)
    price: float = Field(gt=0, description="가격은 0보다 커야 합니다.")
    
    # 이름은 3글자 이상, 50글자 이하
    name: str = Field(min_length=3, max_length=50)
    
    # 상품 코드는 'PROD-'로 시작하고 뒤에 숫자 4자리가 와야 함 (정규식)
    product_code: str = Field(pattern=r"^PROD-\d{4}$")
Field 인자설명예시
gt, ge보다 큼 (greater than), 크거나 같음 (greater or equal)Field(gt=0)
lt, le보다 작음 (less than), 작거나 같음 (less than or equal)Field(le=100)
min_length문자열의 최소 길이Field(min_length=1)
max_length문자열의 최대 길이Field(max_length=20)
pattern정규식(Regex) 패턴 검사Field(pattern=r"...")
description필드에 대한 설명 (API 문서에 표시됨)Field(description="...")

(3) @validator를 이용한 커스텀 검증 로직

  • Field만으로 표현하기 힘든 복잡한 검증 로직이 필요할 때 사용
  • 예를 들어, '할인 종료일'이 '할인 시작일'보다 반드시 나중이어야 한다는 규칙
  • @validator 데코레이터를 사용하여 특정 필드에 대한 검증 함수를 직접 정의
  • 여러 필드의 값을 조합하여 검증하는 것도 가능
from pydantic import BaseModel, validator
from datetime import date

class Event(BaseModel):
    start_date: date
    end_date: date

    # end_date 필드를 검증할 때 실행되는 함수
    @validator('end_date')
    def validate_dates(cls, v, values):
        # values 딕셔너리를 통해 다른 필드(start_date)의 값에 접근
        if 'start_date' in values and v < values['start_date']:
            raise ValueError('종료일은 시작일보다 빠를 수 없습니다.')
        return v

(4) 중첩 모델(Nested Models)

  • JSON 안에 또 다른 JSON 객체가 들어가는 복잡한 구조를 다룰 때 사용
  • 각 객체 구조에 맞춰 Pydantic 모델을 각각 정의하고, 한 모델이 다른 모델을 타입 힌트로 포함하는 방식
  • 예를 들어, '사용자' 정보 안에 '주소' 객체가 포함된 경우
  • 코드가 훨씬 구조화되고 재사용성이 높아짐
from pydantic import BaseModel
from typing import List

class Address(BaseModel):
    city: str
    zip_code: str

class User(BaseModel):
    username: str
    email: str
    # Address 모델을 필드의 타입으로 사용
    address: Address
    # Address 모델의 리스트를 타입으로 사용
    previous_addresses: List[Address] = []
모델역할구조 예시 (JSON)
Address주소 정보라는 하위 객체를 위한 스키마{"city": "서울", "zip_code": "12345"}
UserAddress 모델을 필드 타입으로 포함하는 상위 스키마{"username": "길동", ..., "address": {...}}
List[Address]주소 객체가 여러 개 담긴 배열을 표현"previous_addresses": [{...}, {...}]

(5) 응답 모델(Response Model) 제어

  • 데이터베이스에서 가져온 값을 그대로 보여주지 않고, 가공해서 보여주고 싶을 때가 있음
  • 예를 들어 pricetax를 합친 price_with_tax 필드를 응답에만 추가하고 싶을 경우
  • Pydantic 모델을 입력용(스키마)과 출력용(response_model)으로 분리하여 관리하면 유연성이 극대화됨
  • @computed_field 데코레이터를 사용해 동적으로 계산되는 필드를 만들 수 있음
from pydantic import BaseModel, computed_field

# 입력(Create, Update) 받을 때 사용할 모델
class ItemCreate(BaseModel):
    name: str
    price: float
    tax: float = 0.1

# 응답(Read)할 때 사용할 모델
class ItemResponse(BaseModel):
    name: str
    price: float

    # price와 tax를 이용해 동적으로 계산되는 필드
    @computed_field
    @property
    def price_with_tax(self) -> float:
        return self.price * (1 + 0.1) # tax를 10%로 가정
구분설명
ItemCreate클라이언트로부터 데이터를 받을 때(입력) 사용하는 모델
ItemResponse클라이언트에게 데이터를 보낼 때(출력) 사용하는 모델
@computed_field모델의 다른 필드 값을 기반으로 동적으로 계산되는 값을 가진 필드를 정의
@property해당 함수를 메서드가 아닌 속성(값)처럼 호출할 수 있게 해줌

2. CRUD

(1) Create (@POST)

클라이언트의 데이터를 서버에 기록

  • POST HTTP 메서드는 서버에 새로운 자원(데이터)을 생성해달라고 요청할 때 사용하는 약속
  • FastAPI에서는 @app.post("/경로") 데코레이터를 사용해 POST 요청을 처리할 함수를 지정
  • 함수의 매개변수에 Pydantic 모델을 타입 힌트로 선언하는 것이 핵심 (item: ItemCreate)
    • FastAPI가 이 힌트를 보고 클라이언트가 보낸 JSON 요청 본문을 자동으로 읽음
    • Pydantic 모델(ItemCreate)의 규칙에 맞는지 데이터 유효성을 검사
    • 검사를 통과하면 JSON 데이터를 파이썬 객체(item)로 변환하여 함수로 전달
    • 만약 규칙에 어긋나는 데이터가 들어오면, FastAPI가 알아서 422 Unprocessable Entity 에러를 응답해주므로 개발이 매우 편리해짐
  • status_code 인자를 사용해 성공 응답 코드를 명시적으로 지정하는 것이 좋음 (데이터 생성 성공 시에는 201 Created가 표준)
  • response_model을 지정하면 응답 데이터의 형태까지 Pydantic으로 제어하여 일관성을 유지하고 불필요한 정보 노출을 막을 수 있음
from fastapi import FastAPI, HTTPException, status
from pydantic import BaseModel

app = FastAPI()
db = {}

class ItemCreate(BaseModel):
    name: str
    price: float

class ItemResponse(BaseModel):
    id: int
    name: str
    price: float

@app.post(
    "/items/",
    response_model=ItemResponse,
    status_code=status.HTTP_201_CREATED
)
def create_item(item: ItemCreate):
    new_id = len(db)
    new_item = {"id": new_id, **item.model_dump()}
    db[new_id] = new_item
    return new_item
구분설명
데코레이터@app.post("/items/")
HTTP 메서드POST
주요 역할새로운 데이터를 서버에 생성
입력 데이터Pydantic 모델로 정의된 Request Body (JSON)
성공 상태 코드201 Created (데이터 생성이 성공적으로 완료됨)
핵심 기능Pydantic을 통한 자동 요청 데이터 유효성 검사

(2) Read (@GET)

저장된 데이터 목록 및 특정 데이터 조회

  • GET HTTP 메서드는 서버의 자원(데이터)을 조회(가져오기)할 때 사용
  • 전체 목록 조회 (/items/)
    • 특정 ID 없이 컬렉션 경로에 GET 요청을 보내 전체 목록을 가져옴
    • 응답 모델을 response_model=List[ItemResponse] 와 같이 리스트 형태로 지정하여 여러 개의 아이템을 반환
  • 특정 데이터 조회 (/items/{item_id})
    • 경로에 {item_id} 와 같은 '경로 매개변수'를 사용하여 조회할 데이터의 고유 ID를 전달
    • 함수의 매개변수 item_id: int 처럼 타입 힌트를 주면 FastAPI가 경로의 문자열을 해당 타입(정수)으로 자동 변환 및 검사
    • 만약 DB에 해당 ID의 데이터가 없으면, 404 Not Found 에러를 반환하는 것이 표준적인 REST API 설계
    • FastAPI의 HTTPException을 사용하면 원하는 상태 코드와 에러 메시지를 손쉽게 클라이언트에게 보낼 수 있음
from typing import List
#... 이전 코드 생략 ...

@app.get("/items/", response_model=List[ItemResponse])
def read_items():
    return list(db.values())

@app.get("/items/{item_id}", response_model=ItemResponse)
def read_item(item_id: int):
    if item_id not in db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail=f"ID가 {item_id}인 아이템을 찾을 수 없습니다."
        )
    return db[item_id]
구분설명
데코레이터@app.get("/items/"), @app.get("/items/{item_id}")
HTTP 메서드GET
주요 역할데이터 조회 (목록 또는 단일)
경로 매개변수{item_id}: URL 경로를 통해 동적인 값을 받는 변수
핵심 기능HTTPException을 이용한 명시적인 에러 처리 (e.g. 404)

(3) Update (@PUT & @PATCH)

기존 데이터 수정하기 (PUT vs PATCH)

  • PUTPATCH 두 가지 메서드가 있으며, 용도가 약간 다름
  • PUT (/items/{item_id})
    • 자원 전체를 교체하는 개념
    • 요청 시 모델의 모든 필드 값을 보내야 하는 것이 원칙
    • 특정 필드를 빼고 보내면 해당 필드는 null 이나 기본값으로 덮어 써짐
  • PATCH (/items/{item_id})
    • 자원의 일부만 수정하는 개념
    • 클라이언트가 변경하려는 필드만 요청 본문에 담아 보냄
    • PUT보다 네트워크 효율이 좋고 유연해서 더 선호되는 경향이 있음
    • PATCH용 Pydantic 모델은 모든 필드를 Optional로 만들어 어떤 필드든 선택적으로 받을 수 있도록 설계하는 것이 일반적
    • Pydantic 객체의 .model_dump(exclude_unset=True) 메서드를 사용하면 클라이언트가 실제로 보낸 필드만 딕셔너리로 추출할 수 있어 PATCH 구현에 매우 유용
from typing import Optional

class ItemUpdate(BaseModel):
    name: Optional[str] = None
    price: Optional[float] = None

@app.patch("/items/{item_id}", response_model=ItemResponse)
def update_item_partial(item_id: int, item: ItemUpdate):
    if item_id not in db:
        raise HTTPException(status_code=404, detail="아이템을 찾을 수 없음")
    
    stored_item = db[item_id]
    # 클라이언트가 보낸 데이터만 추출
    update_data = item.model_dump(exclude_unset=True)
    
    # 기존 데이터에 새로운 데이터를 덮어씀
    for key, value in update_data.items():
        stored_item[key] = value

    return stored_item
구분PUTPATCH
데코레이터@app.put(...)@app.patch(...)
개념전체 교체 (Replace)부분 수정 (Modify)
요청 데이터리소스의 모든 필드변경이 필요한 필드만
Pydantic 모델ItemCreate 모델 재사용 가능모든 필드가 Optional인 별도 모델 권장
사용처데이터 구조 전체를 새로 쓸 때일부 필드의 값만 변경할 때

(4) Delete (@DELETE)

불필요한 데이터 영구적으로 삭제하기

  • DELETE HTTP 메서드는 특정 자원을 삭제할 때 사용
  • GET (단일 조회)과 마찬가지로 경로 매개변수를 통해 삭제할 대상의 ID를 받음
  • 삭제할 데이터가 없는 경우 404 Not Found 에러를 보내주는 것이 좋음
  • 데이터 삭제 성공 시에는 크게 두 가지 방식으로 응답
    • {"message": "삭제 성공"} 과 같이 간단한 JSON 메시지를 200 OK 상태 코드와 함께 반환
    • 응답 본문(Body) 없이 204 No Content 상태 코드를 반환. 이는 '요청은 성공했지만 보내줄 내용은 없음'을 의미하며, RESTful API에서 더 선호되는 방식
from fastapi import Response
#... 이전 코드 생략 ...

@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
    if item_id not in db:
        raise HTTPException(status_code=404, detail="아이템을 찾을 수 없음")
    
    del db[item_id]
    # 204 상태 코드는 응답 본문이 없어야 하므로 아무것도 리턴하지 않음
    return Response(status_code=status.HTTP_204_NO_CONTENT)
구분설명
데코레이터@app.delete("/items/{item_id}")
HTTP 메서드DELETE
주요 역할특정 ID를 가진 데이터를 서버에서 삭제
성공 상태 코드200 OK (메시지 포함 시) 또는 204 No Content (내용 없음)
핵심 기능경로 매개변수로 대상을 특정하고, 해당 데이터를 영구 제거
profile
나는 AI 엔지니어가 된다.

0개의 댓글