FastAPI

김기훈·2025년 9월 29일

FastAPI

목록 보기
1/7
post-thumbnail

API(Application Programming Interface)

어떤 프로그램이 다른 프로그램의 기능을 사용할 수 있게 해주는 약속된 방법

  • API를 쓰면 직접 복잡한 내부 로직을 짤 필요 없이, 정해진 규칙대로 요청만 하면 원하는 기능이나 데이터를 쉽게 가져올 수 있다.

1. FastAPI 란?

FastAPI는 파이썬 언어를 위해 설계된 웹 프레임워크로, 특히 API 개발에 최적화 되어 있다.
현대적이고, 빠르며(고성능), 파이썬 표준 타입 힌트에 기초한 Python의 API를 빌드하기 위한 웹 프레임워크

2. FastAPI 특징

  • 높은 성능
    • FastAPI가 빠른 이유는 Starlette(스타레테)라는 핵심 내부 프레임워크를 사용하기 때문
      • Starlette(스타레테)는 FastAPI의 주요 기능을 제공하는 ASGI 웹 프레임워크
        • ASGI는 비동기 웹 서버와 웹 애플리케이션을 연결하기 위한 표준 인터페이스
          • 비동기 처리 지원: 동시에 여러 요청을 빠르게 처리 가능
          • 실시간 기능(WebSocket, 채팅, 알림) 지원
  • 간편한 문법
    • 플라스크와 유사한 데코레이터를 사용하여 쉽게 개발 가능
  • 타입 검증
    • Pydantic: 라이브러리를 사용하여 요청 데이터 타입을 검증
      • Pydantic: 요청 데이터의 타입을 검증하고 변환하는 라이브러리
        • Python 타입 힌트를 기반으로 작동
  • 쉬운 유효성 검사
    • FastAPI는 Pydantic 라이브러리를 활용하여 데이터 유효성 검사를 간단히 수행 가능
      • 플라스크에서는 별도의 라이브러리를 사용하여 데이터 유효성 검사를 해야함
  • 자동 문서화
    • FastAPI애서는 API를 만들면 Swagger UI와 ReDoc를 통해 관련문서가 자동 생성
      • Swagger UI와 ReDoc : API 문서를 자동으로 생성하는 도구
        • 협업과 디버깅 등에서 생산성을 높임
  • 다양한 기능
    • 내장 기능 또는 관련 라이브러리를 통해 데이터베이스, 인증, 배포 등 다양한 기능 사용가능
  • RESTful API쉽게 구축할 수 있게 돕는 것이 FastAPI의 가장 큰 장점
    • RESTful API: 웹 서비스에서 클라이언트와 서버 간에 데이터를 교환하는 데 사용되는 일반적인 방식
    • FastAPI는 이런 API를 구축할 때 필요한 많은 기본 설정과 보일러플레이트 코드를 줄여줌
      • 보일러플레이트 코드: 애플리케이션의 동작과 관련없이 반복되어 작성되어야 하는 코드조각
        • 이러한 코드는 대부분의 프로그래밍 작업에서 기본적인 설정이나 준비작업을 수행하기 위해 사용됨

uvicorn

  • FastAPI 서버 실행기(엔진)
    • FastAPI는 웹 프레임워크일 뿐, 직접 실행할 수 있는 게 아님.
      • ASGI 서버(uvicorn) 위에서 돌아가야 요청을 처리할 수 있다.
  • uvicorn main:app ,uvicorn main:app --reload
    • main:app : main.py 안에 있는 app = FastAPI() 객체를 실행한다는 뜻
    • --reload : 코드 수정하면 서버 자동으로 다시 시작 (개발용 필수)

문서화

  • http://127.0.0.1:8000/docs 열면 자동 대화식 API 문서를 볼 수 있음
    • 파이썬 타입 선언을 하기만 하면 FastAPI는 자동 대화형 API 문서(Swagger UI)를 제공
    • API 엔드포인트를 인터랙티브하게 실행할 수 있음 (Try it out 버튼).
    • 요청/응답 구조를 바로 확인 가능.
    • 개발할 때 가장 많이 쓰이는 기본 문서화 UI.
  • FastAPI는 http://127.0.0.1:8000/redoc 도 가능
    • 한쪽 사이드바에서 전체 API 구조를 계층적으로 탐색할 수 있음.
    • 문서화 가독성이 더 좋고, 배포용/외부용 API 문서로 선호되는 경우가 많음.
    • 기본적으로는 요청 테스트(Try it out) 기능이 없음. (순수 문서 용도에 가까움)

1

경로

"경로"는 첫 번째 / 부터 시작하는 URL의 뒷부분을 의미(ex: https://example.com/~)

  • "경로"는 일반적으로 "엔드포인트" 또는 "라우트" 라고 불림

작동

HTTP 프로토콜에서는 아래와 같은 "메소드"를 하나(또는 이상) 사용하여 각 경로와 통신할 수 있다.

  • POST : 데이터를 생성하기 위해.
  • GET : 데이터를 읽기 위해.
  • PUT : 데이터를 수정하기 위해.
  • DELETE : 데이터를 삭제하기 위해.

경로 작동 데코레이터 정의

  • @app.get("/"): FastAPI에게 바로 아래에 있는 함수가 다음으로 이동하는 요청을 처리한다는 것을 알림
    • @something : 데코레이터라 부르고 함수의 맨위에 놓는다.
      • 데코레이터는 아래 있는 함수를 받아 그것으로 무언가 함
    • 경로 작동 데코레이터 : @app.get("/")는 FastAPI에게 아래 함수가 경로 /get 작동에 해당한다고 알려줌
      • @app.post() / @app.put() / @app.delete() 다 가능

경로 작동 함수

  • 경로: /
  • 작동: get
  • 함수: "데코레이터" 아래에 있는 함수(@app.get("/") 아래)
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root(): 
    return {"message": "Hello World"}
  • async def root():
    • URL / 에 대한 GET 작동 을 사용하는 요청을 받을 때마다 FastAPI 에 의해 호출

2

경로 매개변수

  • 경로 매개변수 item_id의 값은 함수의 item_id 인자로 전달

타입이 있는 매개변수

경로 작동 순서

from fastapi import FastAPI

app = FastAPI()

@app.get("/users/me")
async def read_user_me():
    return {"user_id": "the current user"}

@app.get("/users/{user_id}")
async def read_user(user_id: str):
    return {"user_id": user_id}
  • 경로 작동은 순차적으로 실행되기 때문에 /users/{user_id} 이전에 /users/me 를 먼저 선언해야 함
    • 그렇지 않을 경우 /users/me 요청 또한 매개변수 user_id 의 값이 me 인 것으로 여기게 됨

Enum

  • 가능한 값들에 해당하는 고정된 값의 클래스 어트리뷰트들을 만듬

경로를 포함하는 경로 매개변수

  • 경로 작동: /files/{file_path}
    • file_path 자체가 home/johndoe/myfile.txt 와 같은 경로를 포함해야 함
      • /files/home/johndoe/myfile.txt
        • OpenAPI는 경로를 포함하는 경로 매개변수를 내부에 선언하는 방법을 지원하지 않음
  • Starlette의 내부 도구중 하나를 사용하여 FastAPI에서는 이것이 가능
    • Starlette의 옵션을 직접 이용하여 /files/{file_path:path} 이러한 URL을 사용함으로써 path를 포함하는 경로 매개변수를 선언할 수 있다
      • 매개변수의 이름은 file_path, :path는 매개변수가 경로와 일치해야 함을 명시
                    from fastapi import FastAPI

                    app = FastAPI()


                    @app.get("/files/{file_path:path}")
                    async def read_file(file_path: str):
                        return {"file_path": file_path}
  • /files/ 뒤에 나오는 모든 경로(슬래시 포함)를 file_path 변수 로 받아라.
  • :path 변환기(converter)
    • 슬래시(/)도 경로 일부로 포함할 수 있게 됨
      • 폴더 구조처럼 깊은 경로를 한 번에 받기위해서 사용
                    @app.get("/users/{user_id}")
                    async def get_user(user_id: str):
  • /users/123 → user_id="123" 가능
  • /users/123/profile 같은 경로는 불가능
    • 기본 Path Parameter는 슬래시(/)를 포함하지 못함.

3

쿼리 매개변수

  • 경로 매개변수의 일부가 아닌 다른 함수 매개변수를 선언하면 "쿼리" 매개변수로 자동 해석
    • URL 뒤에 “?”로 시작해서 추가 정보(옵션)를 전달하는 방식
      • http://127.0.0.1:8000/items/?skip=0&limit=10
        • 쿼리 매개변수: skip=0 , limit=10
  • 검색, 필터링, 페이지네이션 등 “요청 옵션” 전달 의 용도로 사용
    • /users?age=20&gender=male → “나이 20, 남성 사용자만 보여줘”
    • /search?keyword=apple&page=2 → “apple 검색 결과의 2페이지를 보여줘”

  • http://127.0.0.1:8000/items/ = http://127.0.0.1:8000/items/?skip=0&limit=10
    • 기본값이기 때문에 두 경로 같음
  • http://127.0.0.1:8000/items/?skip=20
    • 이렇게 이동할 경우 URL에서 지정했기 때문에 skip=20: , 기본값이기 때문에 limit=10:

선택적 매개변수

from typing import Union
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/{item_id}")
async def read_item(item_id: str, q: Union[str, None] = None):
    if q:
        return {"item_id": item_id, "q": q}
    return {"item_id": item_id}
  • 기본값을 None로 설정하여 선택적 매개변수를 선언
    • FastAPI는 q가 = None이므로 선택적이라는 것을 인지할 수 있음
  • q: str | None = None (신버전) , q: Union[str, None] = None (구버전)
    • Union[str, None] → q는 문자열(str)이거나 None일 수 있다
    • = None → 기본값은 None (즉, 안 보내면 None으로 자동 처리)
      • 즉, q는 옵션(선택형) 값

요청 URLitem_idq반환 결과
/items/foo"foo"None{"item_id": "foo"}
/items/foo?q=bar"foo""bar"{"item_id": "foo", "q": "bar"}
/items/123?q=test"123""test"{"item_id": "123", "q": "test"}

이름위치타입필수설명
item_idpathstring아이템의 고유 ID
qquerystring검색어 / 추가 필터

쿼리 매개변수 형변환


매개변수타입역할값이 들어오는 위치기본값설명
item_idstr경로 매개변수/items/{item_id}❌ (필수)어떤 아이템인지 구분하는 고유값
qUnion[str, None]쿼리 매개변수?q=...None선택적인 검색어나 추가 필터
shortbool쿼리 매개변수?short=trueFalse설명을 짧게 할지 여부 (True면 짧게)

요청 URLitem_idqshort반환 결과
/items/apple"apple"NoneFalse{"item_id": "apple", "description": "...long description"}
/items/apple?q=red"apple""red"False{"item_id": "apple", "q": "red", "description": "...long description"}
/items/apple?q=red&short=true"apple""red"True{"item_id": "apple", "q": "red"}

여러 경로/쿼리 매개변수

from typing import Union
from fastapi import FastAPI

app = FastAPI()

@app.get("/users/{user_id}/items/{item_id}")
async def read_user_item(
    user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False
):
    item = {"item_id": item_id, "owner_id": user_id}
    if q:
        item.update({"q": q})
    if not short:
        item.update(
            {"description": "This is an amazing item that has a long description"}
        )
    return item

여러 경로 매개변수와 쿼리 매개변수를 동시에 선언할 수 있으며 FastAPI는 어느 것이 무엇인지 알고 있음

  • 특정 순서로 선언할 필요 없음 / 매개변수들은 이름으로 감지됨
    • user_id: int, item_id: str, q: Union[str, None] = None, short: bool = False
구분예시순서가 중요한가?설명
① 경로 작성 순서 (URL Path Pattern)/users/{user_id}/items/{item_id}중요 ✅FastAPI가 어떤 라우트로 요청을 연결할지 결정함
② 함수 매개변수 순서 (Parameter order)(user_id: int, item_id: str, q: str None = None, short: bool = False)중요 ❌FastAPI가 “이름” 기준으로 자동 매핑함

필수 쿼리 매개변수

  • 경로가 아닌 매개변수에 대한 기본값을 선언할 떄, 해당 매개변수는 필수적이지 않다.
    • 특정값을 추가하지 않고 선택적으로 만들기 위해서는 기본값을 None
    • 하지만 쿼리 매개변수를 필수로 만들기 위해서는 기본값을 선언하지 않으면 된다


4

요청 본문

  • 요청 본문(Request Body) : 클라이언트(사용자)가 서버에 보내는 실제 JSON 데이터
    • 요청 본문을 선언하기 위해서 모든 강력함과 이점을 갖춘 Pydantic 모델을 사용
  • 요청본문을 보내는 방법
    • FastAPI의 Swagger UI에서 직접 입력
    • HTTP 클라이언트 툴로 전송 (예: Postman, Insomnia 등)
    • curl 명령어로 터미널에서 전송
    • 프론트엔드 (JavaScript, HTML 폼 등)에서 전송
      • 보통 class Item(BaseModel): 이러한 필드 구조에 맞춰서 보내야 함
  • 응답 본문: API가 클라이언트로 보내는 데이터
    • API는 대부분의 경우 응답 본문을 보내야 함 하지만 클라이언트는 요청 본문을 매 번 보낼 필요가 없음

Pydantic의 BaseModel 임포트

  • BaseModel : pydantic에서 제공하는 데이터 검증용 클래스
    • 요청 본문의 데이터 구조(형태)를 정의하는 설계도
    • 요청 데이터가 이 모델의 타입과 맞는지 검사하고 자동으로 변환해줌
  • | None = None 은 “이 값이 없어도 된다”는 뜻 : 이 필드는 선택(optional)

item: Item

  • 클라이언트가 보낸 JSON 데이터를 Item 모델에 자동으로 매핑.
    • Item 은 class Item(BaseModel): 의 Item
{
    "name": "Apple",
    "description": "Fresh red apple",
    "price": 3.5,
    "tax": 0.1
}
item = Item(name="Apple", description="Fresh red apple", price=3.5, tax=0.1)
  • return item
    • Item 객체를 그대로 반환하면, FastAPI가 자동으로 JSON으로 변환해서 응답

모델 사용하기

  • 함수 내부에서 모델 객체의 모든 어트리뷰트에 접근 가능

요청 본문 + 경로 매개변수

  • 경로 매개변수요청 본문을 동시에 선언가능
  • item_id: int → 경로(Path) 매개변수
    • PUT /items/5 : item_id = 5
  • item: Item → 요청 본문(Request Body)
    • 클라이언트가 PUT 요청을 보낼 때 Body에 담아서 보내는 JSON 데이터
      • 즉, Item 클래스의 구조에 맞는 JSON을 본문에 담아야 함
  • return {"item_id": item_id, **item.dict()}
    • **item.dict() : Item 모델을 딕셔너리로 변환해서 그 내용을 펼쳐서(**) 병합하는 코드
    • item_id 값과 item 객체의 내용을 하나의 딕셔너리로 병합(merge) 하는 코드
# js
{
  "name": "MacBook Pro",
  "description": "Apple laptop",
  "price": 2500.0,
  "tax": 250.0
}

# python
item = Item(name="MacBook Pro", description="Apple laptop", price=2500.0, tax=250.0)

# **item.dict() 응답
{
  "item_id": 5,
  "name": "MacBook Pro",
  "description": "Apple laptop",
  "price": 2500.0,
  "tax": 250.0
}

요청 본문 + 경로 + 쿼리 매개변수

  • 본문, 경로 그리고 쿼리 매개변수 모두 동시에 선언 가능
from fastapi import FastAPI
from pydantic import BaseModel

class Item(BaseModel):
    name: str
    description: str | None = None
    price: float
    tax: float | None = None

app = FastAPI()

@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item, q: str | None = None):
    result = {"item_id": item_id, **item.dict()}
    if q:
        result.update({"q": q})
    return result
  • 만약 매개변수가 경로에도 선언되어 있다면, 이는 경로 매개변수로 사용
  • 만약 매개변수가 (int, float, str, bool 등과 같은) 유일한 타입으로 되어있으면, 쿼리 매개변수로 해석
  • 만약 매개변수가 Pydantic 모델 타입으로 선언되어 있으면, 요청 본문으로 해석

5

옵션역할 / 의미Swagger 문서에서의 표시실제 동작 / 검증 규칙요청 예시결과 / 비고
alias내부 변수명(q)과는 다른 이름으로 쿼리 파라미터를 받을 수 있게 함Parameter 이름이 alias 값으로 표시됨URL 쿼리 키 이름이 alias로 지정되어야 함/items/?item-query=fixedquery내부 코드에서는 q로 사용되지만, 외부 요청에서는 반드시 item-query로 전달해야 함
titleSwagger 문서에서 해당 파라미터의 제목(Title) 을 표시Parameters 섹션의 title 항목으로 노출코드 실행에는 영향 없음 (문서용)-문서 가독성을 위해 제목만 지정할 때 사용
descriptionSwagger 문서에서 해당 파라미터의 상세 설명 추가Parameters → Description 부분에 표시됨코드 실행에는 영향 없음 (문서용)-“이 파라미터가 무슨 역할인지” 안내용 텍스트
min_length, max_length문자열의 최소·최대 길이를 지정Schema 영역에 (minLength=3, maxLength=50) 로 표시입력값이 범위를 벗어나면 422 에러 반환/items/?item-query=hi → ❌"msg": "String should have at least 3 characters"
pattern정규식(Regular Expression)으로 입력값 검증Schema 영역에 pattern="^fixedquery$" 표시해당 패턴과 완전히 일치해야 통과/items/?item-query=fixedquery
/items/?item-query=fixedquery123
유효하지 않으면 "String should match pattern" 에러
deprecatedSwagger 문서에 “⚠️ Deprecated” 표시Parameter 옆에 (deprecated) 표기 및 노란색 경고 아이콘 표시기능은 그대로 작동함 (제한 없음)/items/?item-query=fixedquery사용은 가능하지만, 문서상 “더 이상 권장되지 않음” 표시
default=None기본값을 None으로 설정 (즉, 선택적 파라미터)Schema에 "nullable": true, "default": null 로 표시값이 없어도 요청 가능 (/items/ OK)/items/ → ✅필수가 아니며, 전달되지 않으면 q=None

쿼리 매개변수와 문자열 검증

  • FastAPI를 사용하면 매개변수에 대한 추가 정보 및 검증을 선언할 수 있음
from typing import Union
from fastapi import FastAPI

app = FastAPI()

@app.get("/items/")
async def read_items(q: Union[str, None] = None):
    results = {"items": [{"item_id": "Foo"}, {"item_id": "Bar"}]}
    if q:
        results.update({"q": q})
    return results
  • q: Optional[str] 자료형 = str | None 과 동일한 표현
    • str 자료형이지만 None 역시 될 수 있음 , =None : 기본값은 None 즉, q는 필수가 아님

추가 검증

Query()를 사용하는 이유

# 기본 파이썬 방식(단순 변수만 정의)
@app.get("/items/")
async def read_items(q: str | None = None):
    return {"q": q}
    
# FastAPI의 Query() 방식 (고급 제어 기능 추가)
from fastapi import Query

@app.get("/items/")
async def read_items(q: str | None = Query(default=None, max_length=50, min_length=2, description="검색어")):
    return {"q": q}

  • 단순한 기본값 지정 + 검증 + 문서 자동화를 한 번에 해결
    • Query() 를 쓰면 Swagger 문서가 훨씬 풍부해지고 실제 API에서도 자동으로 입력값 검증을 수행

Query 임포트

  • Query()는 FastAPI가 제공하는 “쿼리 파라미터의 메타데이터 + 유효성 검증 도구”
    • 기본값을 지정하는 것 뿐만 아니라
      • 길이 제한 / 최소 최대 값 / 설명 / regex 패턴 검증 등 다양한 제약조건을 걸 수 있음
  • q: 쿼리 파라미터(Query Parameter) 즉, URL에 ?q=무언가 형태로 전달되는 값
  • Query(default=None, max_length=50)
    • q는 선택사항 이고, 만약 있다면 최대 50자 까지 허용 초과하면 422 에러
  • q: str = Query(min_length=3) or q: str = Query(..., min_length=3)
    • ...(Ellipsis)는 “기본값 없음 → 필수” 라는 뜻
    • Query()는 필수 값 : 꼭 /items/?q=apple 이런식으로 해야 정상작동함
  • Query(None, min_length=3)
    • default가 없어도 첫 번째 인자가 기본값임

정규식 추가

q: Union[str, None] = Query(
        default=None, min_length=3, max_length=50, pattern="^fixedquery$"
    ),
  • ^ : 이전에 문자가 없고 뒤따리는 문자로 시작
  • fixedquery: 이건 “문자열이 정확히 fixedquery일 때만 통과” 라는 뜻
  • $: 여기서 끝나고 fixedquery 이후로 아무 문자도 갖지 않는다
  • (q: str = Query(default="fixedquery", min_length=3))
    • 기본값이 "fixedquery"인 쿼리 매개변수 q를 선언
      • 기본값을 갖는 것만으로 매개변수는 선택적이 됨

쿼리 매개변수 리스트 / 다중값

  • URL에서 여러번 나오는 q 쿼리 매개변수를 선언하기 위해서 리스트나 다른방법으로 여러 값을 받도록 함

더 많은 메타데이터 선언

  • 매개변수에 대한 정보를 추가 가능

  • title, description 추가

별칭 매개변수

  • 매개변수의 이름이 item-query이길 원한다
    • 이럴경우 alias를 선언할 수 있으며, 해당 별칭은 매개변수 값을 찾는 데 사용

매개변수 사용하지 않게 하기

  • 매개변수를 사용하는 클라이언트가 있기 때문에 한동안은 남겨둬야 하지만, 사용되지 않는다(deprecated)고 확실하게 문서에서 보여주고 싶은 경우
    • deprecated=True 매개변수를 Query로 전달
    • 이 파라미터는 곧 폐기될 예정”이라는 문서용 경고 표시 : 실제 동작에는 영향을 주지 않음


6

path() 사용 이유

  • 경로 매개변수에 더 세부적인 제약조건을 걸고 싶을 때 사용
    • 경로 매개변수(Path Parameter) == “동적 파라미터(Dynamic Parameter)”

Query()와의 차이점

구분PathQuery
전달 방식URL 경로 일부 (/items/{item_id})URL 뒤 파라미터 (?q=search)
필수 여부항상 필수기본값 없으면 필수, default=None이면 선택
사용 목적리소스 식별 (ex. /users/5)검색, 필터링, 옵션 (ex. ?sort=desc)
FastAPI 함수Path()Query()

경로 매개변수와 숫자 검증

  • Query()를 사용하여 쿼리 매개변수에 더 많은 검증과 메타데이터를 선언하는 방법과 비슷하게
    • Path()를 사용하여 경로 매개변수에 검증과 메타데이터를 같은 타입으로 선언할 수 있다.
  • 경로 매개변수는 언제나 필수적 즉, ...로 선언해서 필수임을 나타내는게 좋음
    • 기본값 지정을 할지라도 영향을 주지는 않는다.

  • title
    • OpenAPI 스펙엔 title이 포함되어 있지만, Swagger UI는 그걸 화면에 출력하지 않음

필요한 경우 매개변수 정렬하기

  • str 형인 쿼리 매개변수 q를 필수로 선언하기 위해서
    • 해당 매개변수에 대해 아무런 선언을 할 필요가 없으므로 Query를 써야할 필요는 없음
      • 하지만 item_id 경로 매개변수는 여전히 Path를 사용해야 함
    • 파이썬 함수 인자 정의: “기본값이 있는 인자(default argument)기본값이 없는 인자보다 항상 뒤에 와야 한다
  • 매개변수를 재정렬함으로써 기본값(쿼리 매개변수 q)이 없는 값을 처음 부분에 위치 할 수 있다.
from fastapi import FastAPI, Path

app = FastAPI()

@app.get("/items/{item_id}")
# 순서변경
async def read_items(q: str, item_id: int = Path(title="The ID of the item to get")):

    results = {"item_id": item_id}
    if q:
        results.update({"q": q})
    return results

필요한 경우 매개변수 정렬하기, 트릭(*)

  • * 를 함수의 첫 번째 매개변수로 전달하면,
    • 이후의 모든 인자는 키워드 전용(keyword-only) 으로만 받을 수 있다
  • 함수의 파라미터 순서기본값 위치 때문에 가끔 Python이 “이건 위치 인자야”로 인식 함
    • 이때, * 를 넣으면 “이 뒤의 매개변수들은 위치로 받지 말고 이름으로만(keyword) 받아라.” 라고 알려줌
    • 이렇게 하면 파이썬은 “기본값 순서 규칙” 을 더 이상 신경 쓰지 않아도 됨
  • async def read_items(item_id: int = Path(title="The ID"), q: str):
    • item_id: int = Path(...) → 기본값이 있다고 간주
    • q: str → 기본값이 없음 (즉, 필수 인자) = 필수인자가 앞
      • 파이썬 함수 인자 정의에 의해서 오류가 발생할 수 있음
      • 파이썬은 함수를 호출할 때 "위치"로 인자를 매칭하기 때문에 오류가 발생 가능
  • async def read_items(*, item_id: int = Path(title="The ID of the item to get"), q: str):

숫자 검증

  • gt : 크거나(greater than) / ge : 크거나 같은(greater equal)
  • lt : 작거나(less than) / le : 작거나 같은(less than or equal)

숫자 검증은 float 값에도 동작함

  • ge뿐만 아니라 gt를 선언 할 수 있음


7

쿼리 매개변수 모델

  • 연관된 쿼리 매개변수 그룹이 있다면 Pydantic 모델 을 사용해 선언할 수 있다.
    • 여러 곳에서 모델을 재사용할 수 있을 뿐만 아니라
      • 매개변수에 대한 검증 및 메타데이터도 한 번에 선언할 수 있다.

쿼리 매개변수와 Pydantic 모델

  • order_by: Literal["created_at", "updated_at"]
    • Literal: 고정된 문자열 중 하나만 허용한다는 뜻
    • 허용
      • /items/?order_by=created_at
      • /items/?order_by=updated_at
    • 에러 발생
      • /items/?order_by=random
  • limit: int = Field(100, gt=0, le=100)
    • Field(default값, 제약조건...)
      • 제약조건: gt,ge,le,lt
  • filter_query: Annotated[FilterParams, Query()]
    • Annotated[FilterParams, Query()]
      • Annotated: 추가 메타데이터(여기서는 Query 설정) 를 붙이는 타입힌트 문법
      • Query(): 데이터 출처와 메타데이터 설정
        • FastAPI에게 “이 값은 쿼리 파라미터에서 받아와라.” 라고 알려주는 역할
      • FilterParams: BaseModel이므로, 내부 필드(limit, offset 등)가 자동으로 쿼리 매개변수로 풀려서 매핑

추가 쿼리 매개변수 금지

  • 몇몇의 특이한 경우에 (흔치 않지만), 허용할 쿼리 매개변수를 제한해야할 수 있음
    • Pydantic 모델 설정에서 extra 필드를 forbid 로 설정할 수 있음

  • model_config = {"extra": "forbid"}
    • 이 한 줄은 “정의되지 않은 필드를 거부하라”는 의미
      • 모델의 동작 방식을 제어하는 설정 딕셔너리
    • 클라이언트가 쿼리 매개변수로 추가적인 데이터를 보내려고 하면, 클라이언트는 에러 응답을 받게 됨
      • 클라이언트가 tool 쿼리 매개변수에 plumbus 라는 값을 추가해서 보내려고 하면,
      • https://example.com/items/?limit=10&tool=plumbus
      • 클라이언트는 쿼리 매개변수 tool 이 허용되지 않는다는 에러 응답을 받게 됨

8

본문 - 다중 매개변수

  • Path와 Query를 어떻게 사용하는지 확인, 요청 본문 선언에 대한 심화 사용법

Path, Query 및 본문 매개변수 혼합

  • Path, Query 및 요청 본문 매개변수 선언을 자유롭게 혼합해서 사용 가능
    • FastAPI는 어떤 동작을 할지 알고 있음
      • 기본 값을 None으로 설정해 본문 매개변수를 선택사항으로 선언할 수 있음

  • item: Union[Item, None] = None
    • Item은 바로 위에서 정의한 class Item(BaseModel) 을 의미
      • 이 요청 본문(body)에 들어오는 데이터는 Item 클래스 구조에 맞게 써야 한다
        • item은 JSON 본문에서 받을 건데, Item 모델 구조를 따라야 한다
        • 만약 안 보내면(None), 그냥 기본값 None을 사용해라
  • FastAPI는 요청을 자동으로 변환해, 매개변수의 item과 user를 특별한 내용으로 받도록 할 것
  • 복합 데이터의 검증을 수행하고 OpenAPI 스키마 및 자동 문서를 문서화 함

본문 내의 단일 값(Body())

  • importance: int = Body()
    • FastAPI에서 단일 스칼라 값(숫자, 문자열 등)요청 본문(body)으로 받기 위해 사용하는 구문
    • 즉, “importance라는 정수형 값을 request body에서 읽어와라” 라는 뜻

body 로 받는 방법

  • FastAPI는 기본형 스칼라 값(int, str, bool 등)은 자동으로 body에서 읽지 않음
    • 기본적으로 Query 파라미터로 처리
    1. Body()로 명시
    • JSON 본문에 간단한 값(숫자, 문자열, 불린 등)을 “추가로” 포함시키고 싶을 때 사용하는 도구
    • ex: importance: int = Body()
      • Body(5 or default=5) / Body(ge=1, le=10) / Body(description="중요도 점수") 가능
      • Body(embed=True)
        • {"importance": 5} 이게 {"importance": {"importance": 5}} 이렇게 됨
    1. 모델(BaseModel)로 묶어서 받기
    • class ImportanceData(BaseModel):
      • async def update_item(item_id: int, data: ImportanceData):

다중 본문 매개변수와 쿼리

단일 본문 매개변수 삽입하기

  • embed=True
    • FastAPI에서 JSON body의 구조를 한 단계 감싸서 받게 하는 옵션
      • Body에 여러 데이터가 함께 들어갈 때 구조를 명확히 하기 위해 사용
      • body 안에서 필드 이름을 명시적으로 표현하고 싶을 때 사용

9

본문 - 필드(Field)

구분사용 위치역할예시
🧩 Query()함수의 쿼리 매개변수/items/?q=search 처럼 URL 쿼리로 전달q: str = Query(default="hello")
🧩 Path()함수의 경로 매개변수/items/{item_id}처럼 URL 일부로 전달item_id: int = Path(gt=0)
🧩 Body()함수의 요청 본문(body)JSON body에서 값 받기data: dict = Body()
🧱 Field()클래스 내부(BaseModel)JSON body에서 받을 값의 제약조건, 설명, 기본값price: float = Field(gt=0, description="가격")
  • Query, Path와 Body를 사용해 경로 작동 함수 매개변수 내에서 추가적인 검증이나 메타데이터를 선언한 것처럼 Pydantic의 Field를 사용하여 모델 내에서 검증과 메타데이터를 선언할 수 있음

    • Field는 @app 함수 밖, 클래스 안(BaseModel) 에서 Query, Path, Body와 비슷한 역할을 하는 도구


10

리스트 필드

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: list = []
  • tags: list = []
    • tags: → 필드 이름 / list → 타입힌트 (리스트 형태의 데이터만 허용)
    • = [] : 기본값이 빈 리스트
      • “tags는 리스트이며, 아무 값이 없으면 기본적으로 빈 리스트로 시작한다”
표현사용 가능 여부설명
list✅ 가능 (Python 기본)리스트임을 의미하지만, 내부 요소의 타입을 지정하지 않음
list[str]✅ 권장 (Python 3.9+)리스트의 요소 타입(str) 을 명확히 지정
List[str]✅ 가능 (Python 3.8 이하, 또는 구버전 호환)typing 모듈에서 가져온 제네릭 리스트 (from typing import List)

집합 타입(set)

  • tags 를 중복되지 않는 고유(unique) 한 데이터 집합으로 바꾸기 위해서 특별 데이터 타입 : set
    • from typing import Set 필요
    • 중복된 값이 자동으로 제거되고, 요소의 순서가 보장되지 않으며, Pydantic이 리스트로 입력받더라도 set으로 변환해줌
  • tags: Set[str] = set()
    • Set[str] : 요소가 문자열(str) 인 집합(set) 타입
    • set() : 기본값은 빈 집합 {}

중첩모델

서브모델 정의

class Image(BaseModel):
    url: str
    name: str

class Item(BaseModel):
    name: str
    description: Union[str, None] = None
    price: float
    tax: Union[float, None] = None
    tags: Set[str] = set()
    image: Union[Image, None] = None
  • Item 클래스 안에 Image를 필드 타입으로 포함하고 있는 것
    • 즉, Item은 Image를 “가지고 있다(has-a)” 관계

특별한 타입과 검증

타입 공식문서: https://docs.pydantic.dev/latest/concepts/types/

  • str, int, float 등과 같은 단일 타입과는 별개로, str을 상속하는 더 복잡한 단일 타입을 사용 가능
    • url: HttpUrl 은 단순 문자열이 아니라 URL 형식이 올바른지 자동 검증해주는 타입
타입의미예시
Image하나의 이미지 객체"image": {"url": "...", "name": "..."}
List[Image]여러 이미지 객체"images": [{"url": "...", "name": "..."}, {...}]

깊게 중첩된 모델

순수 리스트의 본문

단독 dict의 본문

장점설명
데이터 구조 명확어떤 형태로 데이터를 보내야 하는지 한눈에 알 수 있음
자동 검증타입이 틀리면 FastAPI가 알아서 422 에러 반환
자동 변환"1": "0.5"{1: 0.5} 로 자동 캐스팅
문서 자동화Swagger에 {int: float} 구조 자동 표시
유지보수 용이코드만 봐도 데이터 의도 명확 (“id별 가중치구나”)

  • 일부 타입의 키와 다른 타입의 값을 사용하여 dict로 본문을 선언할 수 있음
    • (Pydantic을 사용한 경우처럼) 유효한 필드/어트리뷰트 이름이 무엇인지 알 필요가 없음
      • 아직 모르는 키를 받으려는 경우 유용
  • 데이터의 구조를 명확하게 표현 가능
    • 의도가 명확해지고, 코드만 봐도 데이터 구조를 알 수 있음
  • 타입 검증과 자동 변환 가능
    • FastAPI는 내부적으로 Pydantic을 사용하기 때문에, 타입을 지정해주면 그에 맞게 자동 변환·검증을 수행

11

Pydantic 모델 속 추가 JSON 스키마 데이터

  • 생성된 JSON 스키마에 추가될 Pydantic 모델을 위한 examples을 선언할 수 있다
    • "json_schema_extra" : 공식 키워드로 다른 이름이면 작동 안함
      • "examples" : "json_schema_extra" 안에서 Swagger 문서에 표시할 예시 데이터 목록을 지정하는 키워드

Field 추가 인자

  • Pydantic 모델과 같이 Field()를 사용할 때 추가적인 examples를 선언할 수 있다

JSON Schema에서의 examples

  • Path() / Query() / Header() / Cookie() / Body() / Form() / File() 중 사용

examples를 포함한 Body

다중 예제를 포함한 Body


일반적인 Sweager에서는 다중 예제를 지원하지 않음

  • summary: 예제에 대한 짧은 설명문.
  • description: 마크다운 텍스트를 포함할 수 있는 긴 설명문.
  • value: 실제로 보여지는 예시, 예를 들면 dict.
  • externalValue: value의 대안이며 예제를 가르키는 URL.
    • value처럼 많은 도구를 지원하지 못할 수 있음

12

다른 데이터 자료형

  • start_datetime: Annotated[datetime, Body()]
    • 이 값은 요청 본문(body)에서 받아오며, datetime 형식이어야 한다
    • start_datetime
      • datetime만 쓰면 FastAPI는 쿼리 파라미터로 인식할 수도 있기 때문에, Body()로 명시
  • start_process = start_datetime + process_after
    • 시작 시각(start_datetime)에 process_after(지연시간)를 더함
      • 즉, 실제 프로세스가 시작되는 시각을 계산
  • duration = end_datetime - start_process
    • 종료 시각(end_datetime)에서 실제 시작 시각(start_process)을 빼서 전체 수행 기간을 계산

타입역할예시 값자동 변환
UUID고유 식별자"550e8400-e29b-41d4-a716-446655440000"문자열 → UUID 객체
datetime날짜 + 시간"2025-10-11T09:00:00"문자열 → datetime
time시각만"10:30:00"문자열 → time
timedelta시간 차이3600.0숫자(초) → timedelta(hours=1)

매개변수타입데이터 출처설명
item_idUUID경로(/items/{item_id})아이템의 고유 식별자 (UUID 형식)
start_datetimedatetimeBody작업 시작 시간
end_datetimedatetimeBody작업 종료 시간
process_aftertimedeltaBody시작 시간 이후 얼마 뒤에 처리 시작할지 (시간 차)
repeat_attime \| NoneBody (선택)반복 실행 시간이 있을 경우 특정 시각 지정

  • 모든 유효한 pydantic 데이터 자료형
  • UUID
    • 표준 "범용 고유 식별자"로, 많은 데이터베이스와 시스템에서 ID로 사용
    • 요청과 응답에서 str로 표현
  • datetime.datetime
    • 파이썬의 datetime.datetime.
    • 요청과 응답에서 2008-09-15T15:53:00+05:00와 같은 ISO 8601 형식의 str로 표현
  • datetime.date
    • 파이썬의 datetime.date.
    • 요청과 응답에서 2008-09-15와 같은 ISO 8601 형식의 str로 표현
  • datetime.time
    • 파이썬의 datetime.time.
    • 요청과 응답에서 14:23:55.003와 같은 ISO 8601 형식의 str로 표현
  • datetime.timedelta
    • 파이썬의 datetime.timedelta.
    • 요청과 응답에서 전체 초(seconds)의 float로 표현
    • Pydantic은 "ISO 8601 시차 인코딩"으로 표현하는 것 또한 허용
  • frozenset
    • 요청과 응답에서 set와 동일하게 취급
      • 요청 시, 리스트를 읽어 중복을 제거하고 set로 변환
      • 응답 시, set는 list로 변환
      • 생성된 스키마는 (JSON 스키마의 uniqueItems를 이용해) set의 값이 고유함을 명시
  • bytes
    • 표준 파이썬의 bytes.
    • 요청과 응답에서 str로 취급
    • 생성된 스키마는 이것이 binary "형식"의 str임을 명시
  • Decimal
    • 표준 파이썬의 Decimal.
    • 요청과 응답에서 float와 동일하게 다뤄짐

13

쿠키 매개변수

  • from fastapi import Cookie, FastAPI 필요
    • Cookie() : 브라우저가 서버에 보내는 쿠키값을 가져오는 역할
      • 쿠키: 웹사이트가 내 컴퓨터(브라우저)에 잠깐 저장해두는 정보
        • 로그인한 사용자 ID / 최근 본 상품 ID 등등

  • ads_id: Annotated[str | None, Cookie()] = None
    • Annotated는 타입 힌트 + 메타데이터(추출 방식) 를 같이 지정하는 용도
    • ads_id → 매개변수 이름 (쿠키 이름도 동일하게 “ads_id”를 찾음)
    • Cookie() → 이 값이 쿠키에서 추출되어야 함을 FastAPI에게 알려줌
    • str | None → 쿠키 값의 타입 (문자열 또는 쿠키가 없으면 None)
    • = None → 쿠키가 없을 때 기본값

14

헤더 매개변수

  • 관련 있는 헤더 매개변수 그룹이 있는 경우, Pydantic 모델을 생성하여 선언 가능
    • 이를 통해 여러 위치에서 모델을 재사용 할 수 있고 모든 매개변수에 대한 유효성 검사 및 메타데이터를 한 번에 선언 가능

Pydantic 모델을 사용한 헤더 매개변수

  • from fastapi import Header 필요
    • Header()의 역할
      • 이 파라미터는 요청의 Header에서 값을 가져와라
        • Query(), Path(), Cookie()처럼 요청이 어디에서 데이터를 꺼내올지 지정하는 도구

  • async def read_items(headers: Annotated[CommonHeaders, Header()]):
    • 이 엔드포인트(/items/)는 클라이언트가 보낸 HTTP 요청 헤더(Header) 중에서
      - CommonHeaders 모델에 정의된 항목들을 자동으로 읽어옴
  • headers: Annotated[CommonHeaders, Header()]
    • CommonHeaders → 받을 데이터의 구조 (Pydantic 모델)
    • Header() → “이 데이터는 Header에서 꺼내라”는 지시
    • Annotated → 타입 힌트 + FastAPI 메타데이터를 함께 지정하기 위한 문법
  • model_config = {"extra": "forbid"} 를 사용하여
    • 수신하려는 헤더를 제한할 수 있음

FastAPI 매개변수 차이 비교

구분가져오는 위치예시 URL / 요청사용 예시 (FastAPI 코드)설명기본 특징
🟩 Query? 뒤의 쿼리스트링/items/?q=apple&limit=5`q: Annotated[strNone, Query()]`URL에 붙는 데이터 (검색, 필터용)선택적(기본값 설정 가능)
🟦 Path경로 일부/users/123user_id: Annotated[int, Path()]리소스 식별용 (URL 안의 변수)필수 (URL 구조와 일치해야 함)
🟨 HeaderHTTP 요청 헤더User-Agent: Chrome
Accept: application/json
user_agent: Annotated[str, Header()]브라우저나 API 클라이언트가 보낸 부가 정보자동으로 대소문자 무시
🟧 Cookie브라우저 쿠키Cookie: session_id=abc123`session_id: Annotated[strNone, Cookie()]`브라우저가 저장하고 자동으로 보내는 값서버가 Set-Cookie로 생성 가능
🟥 Body요청 본문 (JSON/Form){"username": "kihoon", "age": 25}data: Annotated[UserCreate, Body()]API에서 가장 자주 쓰는 실제 데이터POST/PUT/PATCH 등에서 사용

각 항목별 실제 예시

종류요청 예시서버 코드서버가 받는 값
Query/items/?q=appleq: Annotated[str, Query()]"apple"
Path/users/42user_id: Annotated[int, Path()]42
HeaderUser-Agent: Chromeua: Annotated[str, Header(alias="User-Agent")]"Chrome"
CookieCookie: ads_id=abc123ads_id: Annotated[str, Cookie()]"abc123"
Body{ "username": "kim" }data: Annotated[User, Body()]User(username="kim")

15

응답 모델 ( response_model )

  • 어떤 경로 작동이든 매개변수 response_model를 사용하여 응답을 위한 모델을 선언할 수 있다
    • response_model: 응답으로 내보낼 데이터의 모양과 타입을 정해주는 옵션
    • @app.get() / @app.post() / @app.put() / @app.delete()

구분역할데이터 방향시점목적
item: Item요청 본문(JSON)을 검증클라이언트 → 서버요청 시클라이언트가 보낸 데이터를 올바른 구조로 받기
response_model=Item응답 데이터를 검증·필터링서버 → 클라이언트응답 시서버가 돌려줄 데이터를 정해진 형태로 보장하기

동일한 입력 데이터 반환

  • UserIn: 요청(Request Body)의 데이터 구조를 정의한 Pydantic 모델
    • “클라이언트가 회원가입할 때 보내야 하는 데이터의 설명서(틀)”

  • email: EmailStr
    • email-validator라는 별도 패키지에 의존
      • pip install pydantic[email] or poetry add "pydantic[email]"
    • 이메일 (형식 자동 검증), "test@example.com" 형식이 아니면 자동으로 오류를 발생

출력 모델 추가

response_model_exclude_unset 매개변수 사용

  • 경로 작동 데코레이터 매개변수를 response_model_exclude_unset=True로 설정 가능
    • response_model_exclude_unset=True
      • 요청 결과에 실제로 들어 있지 않은 필드(기본값만 있는 필드)는 응답에 포함하지 말라
    • response_model_exclude_defaults=True
      • 기본값과 동일한 값을 가진 필드는 응답에서 제외
    • response_model_exclude_none=True
      • 값이 None인 필드는 응답에서 제외
      • 0, [], False 같은 기본값(비어 있는 값)은 “None이 아니기 때문에” 그대로 응답에 포함

response_model_include 및 response_model_exclude

  • response_model_include → 이 필드들만 포함해서 보내라
  • response_model_exclude → 이 필드들은 빼고 보내라
    • 둘 다 response_model=Item 과 함께 쓰이는 설정

결론

  • 응답 모델을 정의하고 개인정보가 필터되는 것을 보장하기 위해 경로 작동 데코레이터의 매개변수 response_model을 사용
  • 명시적으로 설정된 값만 반환하려면 response_model_exclude_unset을 사용

16

추가 모델

다중 모델

  • user_in.dict()
  • **user_in.dict()
    • dict를 함수(또는 클래스)에 **user_dict로 전달하면, Python은 이를 "언팩(unpack)"
    • 이 과정에서 user_dict의 키와 값을 각각 키-값 인자로 직접 전달

  • UserInDB(**user_in.dict(), hashed_password=hashed_password)

중복 줄이기

  • UserBase 모델을 선언하여 다른 모델들의 기본(base)으로 사용할 수 있음
    • 그런 다음 이 모델을 상속받아 속성과 타입 선언(유형 선언, 검증 등)을 상속하는 서브클래스를 만들 수 있다.

Union 또는 anyOf

  • 두 가지 이상의 타입을 포함하는 Union으로 응답을 선언
    • 이는 응답이 그 중 하나의 타입일 수 있음을 의미
    • OpenAPI에서는 이를 anyOf로 정의

모델 리스트

  • 객체 리스트 형태의 응답을 선언도 가능

임의의 dict 응답

  • Pydantic 모델을 사용하지 않고, 키와 값의 타입만 선언하여 평범한 임의의 dict로 응답을 선언도 가능
    • 이는 Pydantic 모델에 필요한 유효한 필드/속성 이름을 사전에 알 수 없는 경우에 유


17

응답 상태 코드

  • status_code 매개변수를 사용하여 응답에 대한 HTTP 상태 코드를 선언 가능
    • status_code 는 "데코레이터" 메소드(get, post 등)의 매개변수
      • http.HTTPStatus 와 같은 IntEnum 을 입력받을 수도 있다

  • @app.post("/items/", status_code=201)
    • “이 엔드포인트가 성공적으로 동작했을 때 HTTP 상태 코드 201(Created)을 반환하라”

HTTP 상태 코드

  • HTTP는 세자리의 숫자 상태 코드를 응답의 일부로 전송
    • 1xx 상태 코드는 "정보"용
      • 직접적으로는 잘 사용되지는 않음, 이 상태 코드를 갖는 응답들은 본문을 가질 수 없다.
    • 2xx 상태 코드는 "성공적인" 응답을 위해 사용
      • 가장 많이 사용되는 유형, 200 은 디폴트 상태 코드로, 모든 것이 "성공적임"을 의미
      • 다른 예로는 201 "생성됨", 일반적으로 데이터베이스에 새로운 레코드를 생성한 후 사용
      • 단, 204 "내용 없음"은 특별한 경우, 클라이언트에게 반환할 내용이 없는 경우 사용
        • 따라서 응답은 본문을 가질 수 없음
    • 3xx 상태 코드는 "리다이렉션"용
      • 본문을 가질 수 없는 304 "수정되지 않음"을 제외하고,
        • 이 상태 코드를 갖는 응답에는 본문이 있을 수도, 없을 수도 있다
    • 4xx 상태 코드는 "클라이언트 오류" 응답을 위해 사용
      • 가장 많이 사용하게 될 두번째 유형, 404 는 "찾을 수 없음" 응답을 위해 사용
      • 일반적인 클라이언트 오류의 경우 400 을 사용할 수 있음
    • 5xx 상태 코드는 서버 오류에 사용
      • 직접 사용할 일은 거의 없음
      • 응용 프로그램 코드나 서버의 일부에서 문제가 발생하면 자동으로 이들 상태 코드 중 하나를 반환

이름을 기억하는 쉬운 방법

  • status_code=201 과 status_code=status.HTTP_201_CREATED 는 결과는 똑같이 작동
    • 두 번째 방식을 사용하면 FastAPI 코드를 더 깔끔하고 확장성 있게 쓸 수 있다.
@app.post("/users/", status_code=status.HTTP_201_CREATED) 
@app.delete("/users/{id}", status_code=status.HTTP_204_NO_CONTENT)
@app.get("/users/{id}", status_code=status.HTTP_200_OK)
  • PyCharm, VSCode 등 IDE에서 status.HTTP_까지만 입력해도 자동완성으로 모든 HTTP 코드 목록이 나옴
HTTP_200_OK
HTTP_201_CREATED
HTTP_400_BAD_REQUEST
HTTP_404_NOT_FOUND
HTTP_500_INTERNAL_SERVER_ERROR

18

폼 데이터

  • JSON 대신 폼 필드를 받아야 하는 경우 Form을 사용 가능
  • Form을 사용하면 유효성 검사, 예제, 별칭(예: username 대신 user-name) 등을 포함하여 Body(및 Query, Path, Cookie)와 동일한 구성을 선언할 수 있다.
    • Annotated[str, Form()]
      • 이 매개변수는 문자열 타입이고, 요청 본문(body) 중 Form 형식으로 전달된다
      • Annotated
        • “타입(str)” + “입력 위치(Form, Query 등)”를 묶어서 표현하는 도구
        • FastAPI는 이걸 이용해서 “이 값은 어떤 방식으로 들어오는지”를 표현

"폼 필드"에 대해

  • HTML 폼(<form></form>)이 데이터를 서버로 보내는 방식은 일반적으로 해당 데이터에 대해 "특수" 인코딩을 사용 (JSON과 다름)
    • FastAPI는 JSON 대신 올바른 위치에서 해당 데이터를 읽음

Form() 왜 필요한가?

  • FastAPI는 원래 JSON API 서버를 만들기 위한 프레임워크
    • 기본값: application/json
  • 하지만 웹 브라우저(HTML)의<form> 태그는 JSON을 전송하지 않음
    • Form()이 FastAPI는 “이 엔드포인트는 Form으로 데이터를 받는다”고 알려줌

언제 사용 하는가?

  • 로그인 / 회원가입 화면 (웹 form에서 전송)
    • 파일 업로드(Form + File 같이 사용)
      • 프론트엔드에서<form>으로 보내는 요청

19

폼 모델

  • FastAPI에서 Pydantic 모델을 이용하여 폼 필드를 선언할 수 있다.
  • Form을 사용하기 위해서
    • pip install python-multipart or poetry add python-multipart
      • from fastapi import FastAPI, Form
  • Form()은 FastAPI에서 아래 형식의 데이터를 받기 위해 사용
    • application/x-www-form-urlencoded
    • multipart/form-data
      • 즉, HTML <form>에서 전송되는 데이터를 처리하기 위한 타입

Pydantic 모델을 사용한 폼

  • 폼 필드로 받고 싶은 필드를 Pydantic 모델로 선언한 다음, 매개변수를 Form으로 선언하면 됨
항목Body() 사용 시Form() 사용 시
Swagger 입력창JSON 코드 영역폼 입력 필드 여러 개
Content-Typeapplication/jsonapplication/x-www-form-urlencoded 또는 multipart/form-data
용도API 호출 시 JSON 데이터 전송웹 폼, 로그인 등 form 전송 처리용
눈에 띄는 변화JSON 입력창 → 필드 입력 UI로 변경✅ 있음 (입력 방식 변화)

추가 폼 필드 금지하기

  • Pydantic 모델에서 정의한 폼 필드를 제한하길 원할 수도 있으며, 그리고 추가 필드를 금지할 수도 있다
from typing import Annotated

from fastapi import FastAPI, Form
from pydantic import BaseModel

app = FastAPI()


class FormData(BaseModel):
    username: str
    password: str
    model_config = {"extra": "forbid"} # ✅


@app.post("/login/")
async def login(data: Annotated[FormData, Form()]):
    return data

# 결과
username: Rick
password: Portal Gun
extra: Mr. Poopybutthole 
# 이렇게 추가 하려고 하면 extra 필드가 허용되지 않는다는 오류 응답을 받게 됨

19

파일 요청(File())

  • File을 사용하여 클라이언트가 업로드할 파일들을 정의 가능
    • 업로드된 파일들은 "폼 데이터"의 형태로 전송되기 때문에 python-multipart 설치 필요

File 사용법

  • from fastapi import FastAPI, File, UploadFile
    • BodyForm 과 동일한 방식으로 파일의 매개변수를 생성
      • async def create_file(file: bytes = File()):
  • FileForm 으로부터 직접 상속된 클래스
    • File의 본문을 선언할 때, 매개변수가 쿼리 매개변수 또는 본문(JSON) 매개변수로 해석되는 것을 방지하기 위해 File 을 사용해야 한다.
    • 경로 작동 함수의 매개변수를 bytes 로 선언하는 경우 FastAPI는 파일을 읽고 bytes 형태의 내용을 전달

File 매개변수와 UploadFile

  • File 매개변수를 UploadFile 타입으로 정의
    • async def create_upload_file(file: UploadFile):

UploadFile 사용의 장점

  • "스풀 파일"을 사용

    • 최대 크기 제한까지만 메모리에 저장되며, 이를 초과하는 경우 디스크에 저장
    • 따라서 대용량의 파일들을 많은 메모리를 소모하지 않고 처리하기에 적합
  • 업로드 된 파일의 메타데이터를 얻을 수 있다.

  • file-like async 인터페이스를 가지고 있음

    • file-like async 인터페이스
      • 파일처럼 읽고 쓸 수 있지만, 그 동작이 비동기(async)로 이루어지는 객체
        • file-like object : 파일처럼 동작하는 객체
          • read(), write(), seek(), close() 같은 메서드를 갖고 있어서
          • “진짜 파일이 아니어도 파일처럼 쓸 수 있는 것”
  • UploadFile 내부는 비동기 파일 핸들러(SpooledTemporaryFile)를 감싸고 있고,

    • read, write, seek, close가 모두 비동기(async def) 버전으로 정의
    • SpooledTemporaryFile
      • 파일 I/O 성능을 높이기 위해 “메모리와 디스크를 자동으로 오가는 임시파일 객체”

UploadFile 어트리뷰트

  • filename : 문자열(str)로 된 업로드된 파일의 파일명입니다 (예: myimage.jpg).
  • content_type : 문자열(str)로 된 파일 형식(MIME type / media type)
    • (예: image/jpeg).
  • file : SpooledTemporaryFile(파일류 객체) 이다.
    • "파일류" 객체를 필요로하는 다른 라이브러리에 직접적으로 전달할 수 있는 실질적인 파이썬 파일

UploadFile async 메소드

  • 내부적인 SpooledTemporaryFile 을 사용하여 해당하는 파일 메소드를 호출
    • write(data): data(str 또는 bytes)를 파일에 작성
    • read(size): 파일의 바이트 및 글자의 size(int)를 읽음
    • seek(offset): 파일 내 offset(int) 위치의 바이트로 이동
      • ex. await myfile.seek(0) 를 사용하면 파일의 시작부분으로 이동
      • await myfile.read() 를 사용한 후 내용을 다시 읽을 때 유용
    • close(): 파일을 닫음
  • 모든 메소드들이 async 메소드이기 때문에 “await”을 사용하여야 한다
    • ex. contents = await myfile.read()
    • 일반적인 def 경로 작동함수의 내부일 경우
      • contents = myfile.file.read()

"폼 데이터"란

  • HTML의 폼들(<form></form>)이 서버에 데이터를 전송하는 방식은 대개 데이터에 JSON과는 다른 "특별한" 인코딩을 사용
    • FastAPI는 JSON 대신 올바른 위치에서 데이터를 읽을 수 있도록 합니다.
  • 폼의 데이터는 파일이 포함되지 않은 경우
    • 일반적으로 "미디어 유형" application/x-www-form-urlencoded 을 사용해 인코딩
  • 파일이 포함된 경우, multipart/form-data 로 인코딩

⚠️ 주의

  • 다수의 File 과 Form 매개변수를 한 경로 작동에 선언하는 것이 가능하지만,
    • 요청의 본문이 application/json 가 아닌 multipart/form-data 로 인코딩 됨
      • 그로인해, JSON으로 받아야하는 Body 필드 를 함께 선언할 수는 없다.
        • 이는 FastAPI의 한계가 아니라, HTTP 프로토콜에 의한 것

다중 파일 업로드

  • 여러 파일을 동시에 업로드 가능
    • 파일들은 "폼 데이터"를 사용하여 전송된 동일한 "폼 필드"에 연결된다.
  • 이 기능을 사용하기 위해서
    • async def create_files(files: List[bytes] = File()):
    • async def create_upload_files(files: List[UploadFile]):

20

폼 및 파일 요청

  • FileForm 을 사용하여 파일과 폼을 함께 정의할 수 있다.
    • poetry add python-multipart 설치 필요

  • 파일과 폼 필드는 폼 데이터 형식으로 업로드되어 파일과 폼 필드로 전달된다.
    • 어떤 파일들은 bytes로, 또 어떤 파일들은 UploadFile로 선언 가능

21

Handling Errors(오류 처리)

  • API를 사용하는 클라이언트에게 오류를 알려야 하는 상황은 많다
    • 클라이언트에게 해당 작업을 수행할 수 있는 권한이 없습니다.
    • 클라이언트는 해당 리소스에 접근할 수 없습니다.
    • 클라이언트가 엑세스하려고 했던 항목이 존재하지 않습니다. 등등
      • 이런 경우에는 일반적으로 400(400~499) 범위의 HTTP 상태 코드를 반환

HTTPException

  • 오류가 포함된 HTTP 응답을 클라이언트에게 반환하기 위해서 사용
    • from fastapi import FastAPI, HTTPException 필요
      • return 하는것이 아니라 raise 하는 것

사용자 정의 헤더 추가

  • HTTP 오류에 사용자 지정 헤더를 추가하는 것이 유용한 몇 가지 상황이 존재
    • API Gateway, Load Balancer, Proxy 같은 중간 시스템이 쓸 수 있음
      • 대규모 시스템에서는 FastAPI 앞단에 Nginx / Cloudflare / AWS ALB / API Gateway 같은 게 붙음
        • 이런 장비들은 헤더 기반으로 라우팅, 로깅, 모니터링을 많이 하기 때문에 에러의 원인을 빠르게 식별하려고 커스텀 헤더를 사용
    • 디버깅, 로깅, 보안 목적
      • 개발 중에는 “왜 500이 떴는지” 빠르게 확인하려고 x-error 같은 헤더를 잠깐 넣어두기도 함
  • 필수는 아니고 API 설계 관점에서 "추가적인 메타정보"를 전달하기 위해서 추가 함
    • 에러 본문(detail)은 사람이 읽는 메시지지만,
    • 헤더는 클라이언트(예: React, 앱, API Gateway 등) 가 “이 응답이 어떤 종류의 에러인지”를 기계적으로 판단할 때 사용

사용자 정의 예외 처리기 설치

  • 사용자 정의 예외 처리기를 추가 가능, 사용자 정의 예외가 있다고 가정
    • UnicornException(또는 사용자가 사용하는 라이브러리) raise.
      • FastAPI를 사용하여 이 예외를 전역적으로 처리하려고 함

예외를 전역적으로 처리

  • API 전체(즉, 모든 엔드포인트)에 걸쳐 발생할 수 있는 오류를
    • 한 곳에서 통합적으로 처리하도록 설정
  • 보통은 각 라우터나 함수 안에서 예외를 일일이 처리함
    • API마다 try/except를 넣거나 HTTPException을 직접 발생시켜야 함
    • 프로젝트가 커지면 이렇게 모든 엔드포인트에 처리하는건 비효율적
      • 따라서, 전역 예외 처리기(global exception handler) 를 등록
      • @app.exception_handler()
        • 발생하는 오류를 공통된 로직으로 다룰 수 있다

JSONResponse

  • from fastapi.responses import JSONResponse
    • FastAPI가 클라이언트에게 “JSON 형태의 응답”을 보낼 때 쓰는 응답 객체

  • JSONResponse를 직접 쓰는 경우는 응답 상태 코드, 헤더, 포맷 등을 세밀하게 제어하고 싶을 때

기본 예외 처리기를 재정의

  • FastAPI 에는 몇 가지 기본 예외 처리기가 있다.
    • raise이러한 핸들러는 요청 HTTPException에 잘못된 데이터가 있을 때
      • 기본 JSON 응답을 반환하는 역할을 함
      • 이러한 예외 처리기를 사용자 고유의 예외 처리기로 재정의 가능

요청 검증 예외 재정의

  • 요청에 잘못된 데이터가 포함되어 있으면
    • FastAPI는 내부적으로 RequestValidationError
      • 이를 재정의하기 위해서 예외처리기를 장식한다.
        • @app.exception_handler(RequestValidationError)
        • 예외 처리기는 Request및 예외를 수신

RequestValidationError

  • Pydantic의 하위 클래스
  • FastAPI는 Pydantic 모델을 사용
    • response_model 데이터에 오류가 있는 경우 로그에서 오류를 확인할 수 있도록 이를 사용
    • 하지만 클라이언트/사용자는 이를 볼 수 없고,
      • 클라이언트는 HTTP 상태 코드와 함께 "내부 서버 오류"를 받게 된다.(500)

HTTPException오류 처리기를 재정의

  • from fastapi.responses import PlainTextResponse
    • 응답(response) 을 “JSON이 아니라 순수 텍스트(plain text)” 로 보내고 싶을 때 쓰는 클래스
      • FastAPI는 기본적으로 모든 딕셔너리 응답을 JSON으로 직렬화해서 보냄
      • 하지만 그냥 문자열 그대로 보내고 싶은 경우
        • 단순한 메시지 반환 ("서버 정상 작동 중")
        • 헬스체크(health check) 응답
        • 텍스트 로그, HTML, XML 등 JSON이 아닌 응답을 직접 전달할 때
  • from starlette.exceptions import HTTPException
    • 예외를 일으킬 때 사용하는 Starlette의 HTTP 예외 클래스
      • FastAPI는 사실 Starlette 위에서 동작하는 프레임워크
      • 많은 핵심 기능이 Starlette에서 상속되거나 래핑(wrapping)된 형태로 존재
  • FastAPI의 HTTPException과 Starlette의 차이
    • from fastapi import HTTPException
    • from starlette.exceptions import HTTPException
      • 사실상 둘은 같은 클래스
      • 유일한 차이는
        • FastAPI 는 필드 HTTPException에 JSON 형식의 데이터를 허용하는 detail
        • Starlette는 HTTPException문자열만 허용

RequestValidationError , Body()

  • from fastapi.encoders import jsonable_encoder
    • 파이썬 객체를 “JSON으로 변환할 수 있는 형태”로 바꿔주는 함수
      • 즉, “응답하기 전에 직렬화 가능한 형태로 바꿔주는 인코더”
    • FastAPI나 JSONResponse는 내부적으로 json.dumps()를 써서 JSON 문자열로 변환
      • 하지만 json.dumps()는 기본적으로 “JSON으로 바꿀 수 없는 타입” 을 만나면 에러 발생
      • {"timestamp": datetime.now()}, datetime은 json.dumps()가 변환 못함
        • 이런 걸 자동으로 처리하기 위해 jsonable_encoder()


FastAPI 의 예외 핸들러 재사용

  • fastapi.exception_handlers.
from fastapi.exception_handlers import (
    http_exception_handler,
    request_validation_exception_handler,
)
  • http_exception_handler / request_validation_exception_handler
    • FastAPI 내부에 “원래부터 내장돼 있는 기본 예외 처리기(handler)
      • 즉, “FastAPI가 자동으로 사용하는 기본 핸들러를 직접 가져와서 재사용하는 것”
  • 내가 커스텀 로직을 추가하고 싶을 경우 “기존 FastAPI의 기본 처리 방식”을 완전히 새로 짜는 대신,
    • 기본 핸들러를 재사용해서 그 위에 추가 기능을 얹을 수 있다
  • http_exception_handler
    • HTTPException이 발생했을 때 FastAPI가 기본적으로 사용하는 예외 처리 함수
      • raise HTTPException(status_code=404, detail="Not Found")
      • status_code 와 detail 값을 꺼내서 → JSONResponse 형태로 클라이언트에게 돌려준다
  • request_validation_exception_handler
    • 요청 데이터(body, query, path)가 Pydantic 검증에서 실패했을 때 (ValidationError)
      • FastAPI가 기본적으로 사용하는 예외 처리기
표현실제 호출되는 함수예시 출력용도
{exc}str(exc)"Nope! I don't like 3."사용자가 보기 좋음
{repr(exc)}repr(exc)"HTTPException(status_code=418, detail='Nope! I don't like 3.')"디버깅용 (어떤 예외 객체인지 한눈에 확인 가능)

22

경로 작동 설정

  • 경로 작동 데코레이터를 설정하기 위해서 전달할수 있는 몇가지 매개변수 존재
    • 아래의 매개변수들은 경로작동 함수가 아닌 경로 작동 데코레이터에 직접 전달된다

응답 상태 코드

  • 경로 작동의 응답에 사용될 (HTTP) status_code를 정의 가능
    • 404와 같은 int형 코드를 직접 전달 가능
      • 하지만 각 코드의 의미를 모른다면, status에 있는 단축 상수들을 사용 가능

태그

  • str로 구성된 list와 함께 매개변수 tags를 전달하여, 경로 작동에 태그를 추가 가능

요약과 기술

  • summary와 description을 추가 가능

독스트링으로 만든 기술

  • 설명은 보통 길어지고 여러 줄에 걸쳐있기 때문에, 경로 작동 기술을 함수 독스트링 에 선언 가능
    • 이를 FastAPI가 독스트링으로부터 읽음
      - 독스트링: 함수안에 있는 첫번째 표현식으로, 문서로 사용될 여러줄에 걸친 문자열

응답 기술

  • response_description 매개변수로 응답에 관한 설명을 명시 가능
    • response_description은 구체적으로 응답을 지칭, description은 일반적인 경로 작동을 지칭

단일 경로 작동 지원중단

  • 단일 경로 작동을 없애지 않고 지원중단을 해야한다면, deprecated 매개변수를 전달하면 됨


23

JSON 호환 가능 인코더(jsonable_encoder())

  • from fastapi.encoders import jsonable_encoder 필요
  • 데이터 유형(예: Pydantic 모델)을 JSON과 호환된 형태로 반환해야 하는 경우(ex.dict,list )
    • ex. 데이터베이스에 저장해야하는 경우, 이를 위해 FastAPI에서는 jsonable_encoder()
  • datetime 객체는 JSON과 호환되는 데이터가 아니므로 이 데이터는 db에 받아들여지지 않음
    • 같은 방식으로 데이터베이스는 Pydantic 모델(속성이 있는 객체)을 받지 않고, dict 만을 받음
      • jsonable_encoder가 Pydantic 모델과 같은 객체를 받고 JSON 호환 가능한 버전으로 반환

  • Pydantic 모델을 dict로, datetime 형식을 str로 변환
  • 이렇게 호출한 결과는 파이썬 표준인 json.dumps()로 인코딩 가능

구분originalencoded
타입Item (Pydantic 모델)dict (JSON 호환 데이터)
직렬화 여부❌ Python 객체 그대로✅ JSON으로 변환 가능한 형태
Swagger에서 보이는 형태같아 보임같아 보임
실제 내부 데이터 구조datetime, set, UUID 등 Python 타입 그대로"2025-10-16T21:20:00" 같은 문자열 형태

24

본문 - 업데이트(PUT)

부분 업데이트PATCH

exclude_unset

  • 부분적인 업데이트를 위해 Pydantic exclude_unset 모델의 매개변수를 사용하는 것이 매우 유용
    • ex. item.model_dump(exclude_unset=True)

Pydantic의 매개변수 사용 - update

  • 기존 모델의 복사본을 만들고 .model_copy()
    • 업데이트할 데이터가 포함된 update 매개변수를 전달할 수 있다.
      - stored_item_model.model_copy(update=update_data):
      - 원래 있던 데이터를 유지하면서 일부 필드만 바꾼 새 모델을 만드는 것

25

의존성

  • 의존성 주입
    • 프로그래밍에서 코드(fastapi에서는 경로 작동 함수)가 작동하고 사용하는 데 필요로 하는 것
      • 즉 "의존성"을 선언할 수 있는 방법을 의미
  • 의존성 주입이 유용한 경우
    • 공용된 로직을 가졌을 경우 (같은 코드 로직이 계속 반복되는 경우).
    • 데이터베이스 연결을 공유하는 경우.
    • 보안, 인증, 역할 요구 사항 등을 강제하는 경우.
      • 이러한 사항을 할 때 코드 반복을 최소화 함

의존성 주입 시스템의 작동 방식

의문

  • 함수나 클래스로 호출해도 같은 결과가 나올 것 같은데 왜 의존성주입을 사용해야 하는가
    • 의존성주입은 클래스나 함수로 직접 호출하는 것보다 훨씬 큰 목적이 있다
      • 의존성주입은 단순하게 코드를 공유하기 위한 방법이 아닌
      • 요청 단위로 실행되어야 하는 공통 로직을 자동으로, 선언적으로 연결하기 위한 시스템
        • 매번 직접 호출하지 않아도 되고
        • 테스트/교체/재사용이 쉬운 구조를 만들기 위한 것
구분클래스(직접 호출)Depends(의존성 주입)
호출 방식직접 Common().do_something() 호출FastAPI가 알아서 호출
실행 시점코드에서 명시적으로 호출요청(request) 들어올 때마다 자동
주입 구조개발자가 직접 인스턴스 생성FastAPI가 알아서 생성하고 파라미터 주입
스코프 관리수동으로 제어해야 함 (전역, 지역 등)요청 단위(request-scope), 앱 단위(singleton) 자동 관리 가능
테스트mock 만들기 귀찮음의존성 오버라이드로 손쉽게 대체 가능
유지보수코드 간 결합도 높음결합도 낮음 (loosely coupled)

의존성 혹은 "디펜더블" 만들기

async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}
  • 주입받은 코드들이 common_parameters 함수의 반환값(=리턴 구조)을 그대로 따르는 것”이 바로 의존성 주입의 핵심 동작
  • “의존성 주입”은 엔드포인트가 특정 함수(common_parameters)의 결과에 의존한다는 걸 선언하고
    • FastAPI가 그 함수를 자동 실행해서 반환값을 주입해주는 기능

Depends 불러오기 , "의존자"에 의존성 명시하기

  • from fastapi import Depends, FastAPI
  • Depends()는 “이 매개변수는 이 함수를 먼저 실행한 결과를 넣어줘” 라는 명령
  • commons: Annotated[dict, Depends(common_parameters)]
    • "()" 가 없기 때문에 직접 호출한 것은 아님, 단지 Depends()에 매개변수로 넘겨 줬을 뿐
      • 그리고 그 함수는 경로 작동 함수가 작동하는 것과 같은 방식으로 매개변수를 받음

  • 이렇게 하면 공용 코드를 한번만 적어도 되며, FastAPI는 경로 작동을 위해 이에 대한 호출을 처리

Annotated인 의존성 공유하기

  • commons: Annotated[dict, Depends(common_parameters)]
    • “이 값은 dict 타입이고, 그 값은 common_parameters()의 실행 결과를 주입받아 온다.”
from typing import Annotated
from fastapi import FastAPI, Depends

app = FastAPI()

async def common_parameters(q: str | None = None, skip: int = 0, limit: int = 100):
    return {"q": q, "skip": skip, "limit": limit}

@app.get("/items/")
async def read_items(commons: Annotated[dict, Depends(common_parameters)]): ⭐️
    return commons

# 결과
{
  "q": "apple",
  "skip": 10,
  "limit": 5
}
  • 요청이 /items/?q=apple&skip=10&limit=5 이런 식으로 들어오면
    • FastAPI가 common_parameters(q="apple", skip=10, limit=5) 실행
      • 결과: {"q": "apple", "skip": 10, "limit": 5}
        • 이 결과를 commons 매개변수로 넣어줌

축약 버전

CommonsDep = Annotated[dict, Depends(common_parameters)] ⭐️

@app.get("/users/")
async def read_users(commons: CommonsDep):
    return commons

26

의존성으로서의 클래스

  • 의존성을 선언하는 핵심 요소는 의존성이 "호출 가능" 해야 한다는 것
    • 파이썬에서의 "호출 가능" 은 파이썬이 함수처럼 "호출"할 수 있는 모든 것
class Cat:
    def __init__(self, name: str):
        self.name = name


fluffy = Cat(name="Mr Fluffy")
  • fluffy는 클래스 Cat의 인스턴스, fluffy를 만들기 위해서 Cat을 "호출"
    • 따라서, 파이썬 클래스는 호출 가능

  • CommonQueryParams 클래스를 호출
    • 이것은 해당 클래스의 "인스턴스"를 생성하고 그 인스턴스는 함수의 매개변수 commons로 전달
  • commons: CommonQueryParams = Depends(CommonQueryParams)
    • commons: CommonQueryParams...
      • FastAPI는 CommonQueryParams 변수에 어떠한 특별한 의미도 부여X
      • FastAPI는 이 변수를 데이터 변환, 검증 등에 활용하지 않음
  • commons = Depends(CommonQueryParams) 이렇게 작성해도 무방함
    • 하지만, 자료형을 선언하면 에디터가 매개변수 commons로 전달될 것이 무엇인지 알게 되고,
      • 이를 통해 코드 완성, 자료형 확인 등에 도움이 될 수 있으므로 권장

코드 단축

  • commons: CommonQueryParams = Depends(CommonQueryParams)
    • commons: CommonQueryParams = Depends() 이렇게 단축하는것도 가능
  • 의존성을 매개변수의 타입으로 선언하는 경우 Depends(CommonQueryParams) 처럼
    • 클래스 이름 전체를 다시 작성하는 대신, 매개변수를 넣지않은 Depends()의 형태로 사용 가능
@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends(CommonQueryParams)):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

-------------------------------------------------------------------------------

@app.get("/items/")
async def read_items(commons: CommonQueryParams = Depends()):
    response = {}
    if commons.q:
        response.update({"q": commons.q})
    items = fake_items_db[commons.skip : commons.skip + commons.limit]
    response.update({"items": items})
    return response

27

종속성(Sub-dependencies)

read_query() 
 └── Depends(query_or_cookie_extractor)
       └── Depends(query_extractor)
  • query_extractor: 첫 번째 종속성
  • query_or_cookie_extractor: 두 번째 종속성
  • read_query는 FastAPI가 최종 실행하는 엔드포인트 함수

동일한 종속성을 여러 번 사용

  • use_cache=True는 ‘성능 최적화’를 위해 존재
  • use_cache=False는 ‘항상 새로 계산해야 하는 의존성’을 위해 존재
  • 동일한 경로 작업에 대해 종속성 중 하나가 여러 번 선언된 경우
    • 여러 종속성에 공통 하위 종속성이 있는 경우 FastAPI는 요청당 해당 하위 종속성을 한 번만 호출
async def needy_dependency(fresh_value: Annotated[str, Depends(get_value, use_cache=False)]):
    return {"fresh_value": fresh_value}
  • Depends(get_value)

    • get_value라는 함수를 의존성 함수로 등록하는 것.
    • FastAPI는 needy_dependency()를 실행하기 전에 먼저 get_value()를 호출해서
      • 그 결과를 fresh_value 인자로 넣어줌
  • FastAPI는 기본적으로 같은 요청 내에서 동일한 의존성은 한 번만 실행하고 결과를 캐시(cache)

    • 즉, 같은 요청 안에서 여러 곳에서 Depends(get_value)를 사용하면 기본적으로 한 번만 실행 하고 그 결과가 재사용 됨
  • use_cache=False 를 사용하면 캐시를 쓰지 않아서, 매번 새로 호출됨

    • 같은 값이 나올 거면 굳이 다시 계산할 필요가 있나? = FastAPI가 기본적으로 캐시를 켜는 이유

use_cache=False가 필요한 경우

예시이유
get_current_timestamp()매번 새 시각 필요
get_random_number()매번 다른 난수 필요
get_fresh_token()요청마다 새 인증토큰 필요
get_fresh_csrf_nonce()폼마다 새로운 보안 토큰 필요

28

경로 작동 데코레이터에서의 의존성

  • 몇몇의 경우에 경로 작동 함수 안에서 의존성의 반환 값이 필요하지 않다.
    • 또는 의존성이 값을 반환하지 않는다.
      • 그러나 여전히 실행할 필요가 있다면
      • Depends를 사용하여 경로 작동 함수의 매개변수로 선언하는 것보다
      • 경로 작동 데코레이터에 dependencies의 list를 추가할 수 있다.

경로 작동 데코레이터에 dependencies 추가하기

  • 경로 작동 데코레이터는 dependencies라는 선택적인 인자를 받는다.Depends()로 된 list이어야 함
  • @app.get("/items/", dependencies=[Depends(verify_token), Depends(verify_key)])
    • “이 /items/ 엔드포인트를 실행하기 전에 verify_token()과 verify_key() 함수를 실행해라.
    • 만약 이 둘 중 하나라도 실패하면(예외 발생 시) 실제 라우트 함수(read_items())는 실행하지 마라.”
async def verify_token(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")
  • 값을 반환하거나, 그러지 않을 수 있으며 값은 사용되지 않는다.(return 값 미사용)

dependencies=[...] 의 핵심 개념

  • “의존성은 실행하지만, 그 결과를 함수의 매개변수로 받지 않는다”는 의미
    • 즉, 위의 코드는 verify_token과 verify_key를 호출은 하지만 read_items() 함수에는 인자로 전달X

29

전역 의존성

  • app = FastAPI(dependencies=[...])
from fastapi import Depends, FastAPI, Header, HTTPException
from typing_extensions import Annotated

async def verify_token(x_token: Annotated[str, Header()]):
    if x_token != "fake-super-secret-token":
        raise HTTPException(status_code=400, detail="X-Token header invalid")

async def verify_key(x_key: Annotated[str, Header()]):
    if x_key != "fake-super-secret-key":
        raise HTTPException(status_code=400, detail="X-Key header invalid")
    return x_key

app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])

@app.get("/items/")
async def read_items():
    return [{"item": "Portal Gun"}, {"item": "Plumbus"}]

@app.get("/users/")
async def read_users():
    return [{"username": "Rick"}, {"username": "Morty"}]
  • FastAPI 앱 전체에 공통적으로 적용되는 의존성
    • 어떤 경로(path) 로 요청이 들어오든
    • 어떤 라우터(router) 나 엔드포인트(endpoint) 를 실행하든
      • 항상 먼저 실행돼서 검증이나 공통 로직을 처리하는 역할
  • app = FastAPI(dependencies=[Depends(verify_token), Depends(verify_key)])
    • FastAPI 인스턴스를 만들 때,
      • “이 앱에 속한 모든 요청이 실행되기 전에 이 두 의존성을 먼저 수행하라”는 설정
    • 즉, “모든 API 요청은 verify_token과 verify_key를 통과해야만 실행된다.”

전역 의존성이 유용한 이유

  • 프로젝트가 커질수록 모든 라우터마다 같은 인증 코드를 넣는 건 비효율적
    • app = FastAPI(dependencies=[Depends(auth_check)])
      • 모든 엔드포인트에서 자동 인증 처리
      • 코드는 깔끔하게 유지

30

yield를 사용하는 의존성

  • FastAPI는 작업 완료 후 추가 단계를 수행하는 의존성을 지원
    • 이를 구현하기 위해 return 대신 yield를 사용하고
      • 추가로 실행할 단계 (코드)를 그 뒤에 작성
    • 각 의존성마다 yield는 한 번만 사용 해야함

Python 제너레이터에서의 yield

yield를 사용하는 데이터베이스 의존성

  • ex. 이 기능을 사용하면 데이터베이스 세션을 생성하고 작업이 끝난 후에 세션을 종료 가능
    • 응답을 생성하기 전에는 yield문을 포함하여 그 이전의 코드만이 실행
  • yield된 값은 경로 작업 및 다른 의존성들에 주입되는 값
    • yield문 다음의 코드는 응답을 생성한 후 보내기 전에 실행
      • async 함수와 일반 함수 모두 사용 가능

  • async def get_db(): ... yield db ... 이거 하나로 의존성이 자동으로 되는건 아니고
    • Depends(get_db) 로 연결해야만 의존성 주입이 실제로 동작
  • 이건 “의존성 함수”로 사용할 준비가 된 함수일 뿐
    • 이 상태에서는 아무데서도 사용하지 않았기 때문에, FastAPI는 이 함수를 호출하거나 주입하지 않음

yield와 try를 사용하는 의존성

  • yield를 사용하는 의존성에서 try 블록을 사용한다면
    • 의존성을 사용하는 도중 발생한 모든 예외를 받을 수 있다

yield를 사용하는 하위 의존성

  • FastAPI는 yield를 사용하는 각 의존성의 "종료 코드"가 올바른 순서로 실행되도록 보장

yield와 HTTPException를 사용하는 의존성

yield와 except를 사용하는 의존성

  • Depends(get_username)
    • 의존성 함수(get_username)를 FastAPI는 라우트를 실행하기 전에
      • get_username()을 자동으로 실행하고, 그 결과("Rick")를 username 매개변수에 주입
  • get_username() 호출됨 → yield "Rick" 실행됨 → "Rick"이 반환값으로 보관됨 → username = "Rick"
  • 이제 get_item(item_id="portal-gun", username="Rick") 실행됨 → 내부에서 InternalError 발생
  • FastAPI가 InternalError를 잡고, 이 예외가 get_username()의 yield 블록으로 전파됨
    • 즉, yield에서 멈췄던 지점으로 돌아가서 except InternalError: 블록이 실행됨.

yield와 except를 사용하는 의존성에서 항상 raise 하기

  • yield가 있는 의존성에서 예외를 잡았을 때는 HTTPException이나 유사한 예외를
    • 새로 발생시키지 않는 한, 반드시 원래의 예외를 다시 발생시켜야 함

컨텍스트 관리자

  • Python에서 with 문에서 사용할 수 있는 모든 객체를 의미(ex. with를 사용하여 파일읽기 가능)
with open("./somefile.txt") as f:
    contents = f.read()
    print(contents)
  • open("./somefile.txt")
    • "컨텍스트 관리자(Context Manager)"라고 불리는 객체를 생성
      • with 블록이 끝나면, 예외가 발생했더라도 파일을 닫도록 보장
  • yield가 있는 의존성을 생성하면 FastAPI는 내부적으로 이를 위한 컨텍스트 매니저를 생성하고
    • 다른 관련 도구들과 결합

yield를 사용하는 의존성에서 컨텍스트 관리자 사용하기(hard)

class MySuperContextManager:
    def __init__(self):
        self.db = DBSession()

    def __enter__(self):
        return self.db

    def __exit__(self, exc_type, exc_value, traceback):
        self.db.close()

async def get_db():
    with MySuperContextManager() as db:
        yield db

31

보안(Security)

  • FastAPI는 모든 보안 사양을 공부하고 학습할 필요 없이 표준적인 방식으로
    • 쉽고 빠르게 보안 문제를 해결하는 데 도움이 되는 여러 도구를 제공

OAuth2

  • 인증 및 권한 부여를 처리하는 여러 가지 방법을 정의하는 사양
    • "제3자"를 사용하여 인증하는 방법이 포함

OpenID Connect

  • OAuth2를 기반으로 하는 또 다른 사양

OPEN API(이전 명칭: Swagger)

  • API를 구축하기 위한 개방형 사양, FastAPI는 OpenAPI를 기반
  • OpenAPI에는 여러 가지 보안 "체계"를 정의하는 방법이 존재
    • apiKey: 다음에서 나올 수 있는 애플리케이션별 키
      • 쿼리 매개변수
      • 헤더
      • 쿠키
    • http: 다음을 포함한 표준 HTTP 인증 시스템
      • bearer: 토큰과 Authorization값을 갖는 헤더입니다 . OAuth2에서 상속되었습니다.Bearer
      • HTTP 기본 인증.
      • HTTP 다이제스트 등
    • oauth2: 보안을 처리하는 모든 OAuth2 방식(흐름이라고 함)
      • 이러한 흐름 중 일부는 OAuth 2.0 인증 공급자를 구축하는 데 적합
      • (예: Google, Facebook, X(Twitter), GitHub 등)
        • implicit
        • clientCredentials
        • authorizationCode
    • 동일한 애플리케이션에서 직접 인증을 처리하는 데 완벽하게 사용할 수 있는 특정 "흐름"이 하나 존재
      • password (32번에서 정리)
  • openIdConnect: OAuth2 인증 데이터를 자동으로 검색하는 방법을 정의하는 방법 존재
    • 이러한 자동 검색은 OpenID Connect 사양에 정의 되어 있음

32

보안(Security) - 1단계

    1. 어떤 도메인에 백엔드 API가 있다고 가정한다.
    1. 동일한 도메인의 다른 도메인이나 다른 경로(또는 모바일 애플리케이션)에 프런트엔드가 있다고 가정
      1. 사용자 이름 과 비밀번호를 사용하여 프런트엔드가 백엔드에 인증할 수 있는 방법이 필요
        1. OAuth2를 사용하여 FastAPI 로 이를 구축 가능

password

  • OAuth2에서 정의된 보안 및 인증을 처리하는 방법 중 하나
    • OAuth2는 백엔드나 API가 사용자를 인증하는 서버로부터 독립적일 수 있도록 설계되었음
      • 하지만 이 경우에는 동일한 FastAPI 애플리케이션이 API와 인증을 처리함

token

  • 나중에 사용자를 검증하는 데 사용할 수 있는 내용이 담긴 문자열
    • 일반적으로 토큰은 일정 시간이 지나면 만료되도록 설정됨
      • 따라서 사용자는 나중에 다시 로그인해야 함
      • 토큰이 도난당하더라도 위험은 적음
        • (대부분의 경우) 영원히 작동하는 영구 키와는 다르기 때문

FastAPI 의 OAuth2PasswordBearer

from typing import Annotated

from fastapi import Depends, FastAPI
from fastapi.security import OAuth2PasswordBearer

app = FastAPI()

oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") ⭐️


@app.get("/items/")
async def read_items(token: Annotated[str, Depends(oauth2_scheme)]):
    return {"token": token}
  • oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
    • OAuth2PasswordBearer
      • FastAPI에 "이 엔드포인트는 Authorization 헤더에서 Bearer Token을 요구한다"는 사실을 알림
    • tokenUrl="token"
      • 토큰을 발급받는 경로(URL)를 지정해주는 부분
    • 이 객체는 실제로는 의존성 함수처럼 동작
      • 요청 헤더에서 "Authorization": "Bearer <token>"을 찾고
      • <token> 문자열을 리턴

추가

  • 변수 oauth2_scheme 는 인스턴스 OAuth2PasswordBearer 이지만 "호출 가능" 변수이기도 함
    • oauth2_scheme(some, parameters) 이렇게 사용도 가능

33

현재 사용자 가져오기

def fake_decode_token(token):
    return User(
        username=token + "fakedecoded", email="john@example.com", full_name="John Doe"
    )
  • 실제 JWT 디코딩을 하는 대신, 단순히 토큰 문자열 뒤에 "fakedecoded"를 붙여서 User 객체를 만듬
    • JWT 인증이 실제로 동작하는 구조를 흉내내는 예시용 함수
async def get_current_user(token: str = Depends(oauth2_scheme)):
    user = fake_decode_token(token)
    return user
  • Depends(oauth2_scheme) 덕분에 요청 헤더에서 Bearer 토큰을 자동으로 가져옴
    • 그 토큰을 fake_decode_token()에 전달해 User 객체를 생성하고 반환
    • 즉, FastAPI가 실행할 때:
      • oauth2_scheme이 헤더에서 토큰을 읽음
      • fake_decode_token()이 유저 객체로 변환
      • 그 결과를 current_user 매개변수로 주입

34

username와 password 얻기

  • FastAPI 보안 유틸리티를 사용하여 username 및 password를 가져오기
    • OAuth2는 "패스워드 플로우"을 사용할 때 클라이언트/유저가 username 및 password 필드를
      • 폼 데이터로 보내야 함을 지정한다.
    • user-name, email 은 작동X, 하지만 프런트엔드에서 최종 사용자에게 원하는대로 표시 가능
  • OAuth2PasswordRequestForm
    • /token에 대한 경로 작동에서 Depends의 의존성으로 사용
    • 아래를 사용하여 폼 본문을 선언하는 클래스 의존성
      • username
      • password
      • scope
      • grant_type (선택적으로 사용)
      • client_id(선택적으로 사용)
      • client_secret(선택적으로 사용)

OAuth2PasswordRequestForm

  • from fastapi.security import OAuth2PasswordRequestForm
    • FastAPI가 OAuth2 로그인용 폼 데이터를 자동으로 파싱해주는 클래스
  • OAuth2PasswordBearer 와 같이 FastAPI에 대한 특수 클래스가 아님
    • OAuth2PasswordBearer 는 FastAPI가 보안 체계임을 알도록 함
  • OAuth2PasswordRequestForm
    • 직접 작성하거나 Form 매개변수를 직접 선언할 수 있는 클래스 의존성

scope vs scopes

이름등장 위치역할예시
scope (단수)OAuth2PasswordRequestForm 안에 있는 요청 필드로그인 시 사용자가 요청하는 권한 목록을 공백으로 구분해 전달"read write admin"
scopes (복수)OAuth2PasswordBearer / OAuth2PasswordRequestForm / Security() 등 FastAPI 보안 구성 내의 속성API가 어떤 권한(scope)을 요구하는지를 선언scopes={"read": "읽기 권한", "write": "쓰기 권한"}

scope (요청에서 들어오는 것)

  • 사용자가 로그인 시 서버에 전달하는 “원하는 권한” 목록
POST /token
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=rick&password=secret&scope=⭐️read write
  • form_data.scope == "read write"
    • 서버는 이 값을 이용해 “이 사용자가 요청한 권한을 허용할지 말지”를 판단 가능

scopes (서버 측에서 정의하는 것)

  • 서버가 “이 API는 어떤 권한이 있어야 접근 가능하다”라고 명시하는 부분

grant_type

  • OAuth2 사양은 실제로 password 라는 고정 값이 있는 grant_type 필드를 요구
  • OAuth2PasswordRequestFormgrant_type 필드를 강요X
    • 사용해야 한다면, OAuth2PasswordRequestFormStrict 를 사용

실습 예제

폼 데이터 사용하기

  • 노랑
    • 폼 필드의 username을 사용하여 (가짜) 데이터베이스에서 유저 데이터를 가져옴
    • 해당 사용자가 없으면 "잘못된 사용자 이름 또는 패스워드"라는 오류가 반환
      • 오류의 경우 HTTPException 예외를 사용
  • 패스워드 확인(초록)
    • 데이터를 Pydantic UserInDB 모델에 넣음((가짜) 암호 해싱 시스템을 사용)
    • **user_dict
      • user_dict의 키와 값을 다음과 같은 키-값 인수로 직접 전달

토큰 반환하기(보라)

  • token 엔드포인트의 응답은 JSON 객체여야 함
    • token_type 필요
      • 여기서는 "Bearer" 토큰을 사용하므로 토큰 유형은 "bearer"여야 함
    • 액세스 토큰을 포함하는 문자열과 함께 access_token 필요

의존성 업데이트하기(파랑)

  • 사용자가 활성화되어 있는 경우에만 current_user를 가져 올 것
    • get_current_user를 의존성으로 사용하는 추가 종속성 get_current_active_user를 만듬
      • 이러한 의존성 모두, 사용자가 존재하지 않거나 비활성인 경우 HTTP 오류를 반환
    • 엔드포인트에서는 사용자가 존재하고 올바르게 인증되었으며 활성 상태인 경우에만 사용자를 얻는다.

35

JWT(JSON Web Tokens)

  • JSON 데이터를 안전하게 주고받기 위한 “서명된 문자열”
    • 즉, 서버가 “이 사용자는 인증된 사용자다”라는 사실을
      • 짧은 문자열 형태로 암호화해서 클라이언트에게 전달하는 방식
  • 사용자 인증(Authentication)/인가(Authorization) 에서 아주 자주 쓰이는 토큰 기반 인증 방식
  • JWT는 암호화되지 않아 누구든지 토큰에서 정보를 복원할 수 있음
  • JWT는 서명되어 있음, 그래서 자신이 발급한 토큰을 받았을 때, 실제로 자신이 발급한게 맞는지 검증 가능

PyJWT 설치

  • 파이썬으로 JWT 토큰을 생성하고 검증하려면 PyJWT 를 설치 필요
    • poetry add pyjwt

JWT 구조 (세 부분)

  • JWT는 세 부분으로 나뉘며, 각 부분은 .으로 구분됨("xxxxx.yyyyy.zzzzz")
    1. Header: 어떤 알고리즘으로 서명했는지 명시 (alg,typ)
    1. Payload: 실제 데이터(사용자 정보, 권한 등)를 담음
    • 사용자 정보, 만료 시간 (sub, exp, scope)
    1. Signature: Header + Payload를 비밀키로 서명한 값 (위조 방지용 HMAC 서명)

JWT의 동작 흐름

단계설명
1. 로그인 요청클라이언트가 /token 에 username/password를 보냄
2. 서버 검증서버가 사용자 정보 확인 후 JWT 발급
3. JWT 발급JWT를 생성해 클라이언트에게 응답 (access_token)
4. 요청 시 사용클라이언트가 요청할 때 헤더에 JWT 포함
5. 서버 검증서버는 JWT를 해독해 서명 검증 후 사용자 신원 확인
# 로그인(bash)
POST /token
username=rick&password=portalgun

# 서버 응답(json)
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer"
}

# 이후 요청 시(http)
GET /users/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

JWT의 장점

    1. 무상태(Stateless)
    • 서버가 세션을 저장하지 않아도 됨 — 모든 정보가 토큰 안에 들어있음
    1. 확장성 좋음
    • 여러 서버나 마이크로서비스에서도 공통 SECRET_KEY만 있으면 인증 가능
    1. 빠름
    • DB조회 없이 토큰 자체로 유저 식별 가능

JWT의 단점

    1. 무효화 어려움
    • 이미 발급된 토큰을 강제로 만료시키기 어렵다 (보통 블랙리스트 사용)
    1. 토큰 크기 큼
    • 세션보다 길고, 요청마다 전송되므로 트래픽 부담 있음
    1. 만료 관리 필요
    • exp(만료시간)을 꼭 설정해야 안전함

패스워드 해싱

  • "해싱(Hashing)"
    • 어떤 내용(여기서는 패스워드)을 해석할 수 없는 일련의 바이트 집합(단순 문자열)으로 변환하는 것
      • 동일한 내용(똑같은 패스워드)을 해싱하면 동일한 문자열을 얻음
      • 하지만 그 문자열을 다시 패스워드로 돌리는건 불가능

passlib설치

  • 패스워드 해시를 다루는 파이썬 패키지, 많은 안전한 해시 알고리즘과 도구들을 지원
    • poetry add passlib , poetry add Bcryp(공식문서 추천 알고리즘)

패스워드의 해시와 검증

  • 필요한 도구를 passlib에서 임포트
    • PassLib "컨텍스트(context)"를 생성, 이건 패스워드를 해싱하고 검증하는데 사용
      • 사용자로부터 받은 패스워드를 해싱하는 유틸리티 함수를 생성
        • 그리고, 받은 패스워드가 저장된 해시와 일치하는지 검증하는 또 다른 유틸리티 함수도 생성
        • 그리고, 사용자를 인증하고 반환하는 또 다른 함수도 생성

pwdlib vs passlib

  • 공통점
    • 역할
      • 비밀번호를 안전하게 해시(Hash)하고 검증(verify)하는 기능 제공
    • 핵심 목적
      • 평문 비밀번호를 그대로 저장하지 않고, 안전한 해시로 관리하기
    • FastAPI에서 사용 예시
      • 사용자 로그인 시 입력된 비밀번호를 해시와 비교하는 데 사용

passlib (전통적인, 많이 쓰이는 라이브러리)

  • 가장 널리 쓰이는 비밀번호 해싱 라이브러리
    • 안정적이지만 라이브러리가 무겁고, 오래된 구조
  • 특징
    • 수많은 알고리즘 지원 (bcrypt, argon2, pbkdf2_sha256 등)
    • 해시를 자동으로 관리하고, 필요한 경우 업그레이드도 지원
from passlib.context import CryptContext

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 해싱
hashed = pwd_context.hash("mysecretpassword")

# 검증
pwd_context.verify("mysecretpassword", hashed)

pwdlib (경량 버전)

  • passlib 의 현대적이고 가벼운 대체 라이브러리
  • 이름처럼 “Password Library” 지만, 구조는 훨씬 간단하고 가볍다.
  • 비동기 환경(FastAPI 등) 에서 쓰기 쉽게 설계됨
  • 내부적으로도 argon2나 bcrypt 등을 사용하지만, API가 훨씬 단순
from pwdlib import PasswordHash

pwd_context = PasswordHash.recommended()  # 추천 알고리즘 사용

# 해싱
hashed = pwd_context.hash("mysecretpassword")

# 검증
pwd_context.verify("mysecretpassword", hashed)
상황추천
FastAPI 튜토리얼 / 기본 예제 연습 중passlib (공식 문서와 동일)
새로운 프로젝트 + 비동기 코드 중심pwdlib (가볍고 modern)
장기 유지보수 / 기존 레거시 코드 있음passlib 유지
성능·간결성 중시, async 함수 내 사용pwdlib 유리

FastAPI + JWT 인증 구조 (초보자 버전)

  • 로그인 → 토큰 발급 → 인증 요청

pwdlib 포함 FastAPI + JWT

from datetime import datetime, timedelta, timezone
from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
import jwt
from pwdlib import PasswordHash  # ✅ pwdlib 임포트

# ===== 설정 =====
SECRET_KEY = "mysecretkey"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# 앱 인스턴스
app = FastAPI()

# 비밀번호 해싱 도구 초기화
password_context = PasswordHash.recommended()

# ===== 가짜 유저 데이터베이스 =====
# 실제 DB에서는 hashed_password가 저장되어 있음
fake_users_db = {
    "rick": {
        "username": "rick",
        "full_name": "Rick Sanchez",
        "email": "rick@example.com",
        # "portalgun"을 미리 해싱한 값 (비밀번호 원문은 저장 안 함!)
        "hashed_password": password_context.hash("portalgun"),
        "disabled": False,
    }
}

# ===== 모델 =====
class Token(BaseModel):
    access_token: str
    token_type: str


class User(BaseModel):
    username: str
    full_name: str | None = None
    email: str | None = None
    disabled: bool | None = None


class UserInDB(User):
    hashed_password: str


# ===== 유틸 함수 =====
def verify_password(plain_password: str, hashed_password: str):
    """입력받은 평문 비밀번호가 해시와 일치하는지 검증"""
    return password_context.verify(plain_password, hashed_password)


def get_user(db, username: str):
    """유저 조회"""
    if username in db:
        return UserInDB(**db[username])
    return None


def authenticate_user(db, username: str, password: str):
    """로그인 검증"""
    user = get_user(db, username)
    if not user:
        return None
    if not verify_password(password, user.hashed_password):
        return None
    return user


def create_access_token(data: dict, expires_delta: timedelta | None = None):
    """JWT 생성"""
    to_encode = data.copy()
    expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    token = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return token


# ===== 인증 흐름 =====
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


@app.post("/token", response_model=Token)
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    """로그인 후 JWT 발급"""
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        {"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}


@app.get("/users/me", response_model=User)
async def read_users_me(token: str = Depends(oauth2_scheme)):
    """JWT 검증 → 사용자 정보 반환"""
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Invalid token")
    except jwt.PyJWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

    user = get_user(fake_users_db, username)
    if user is None:
        raise HTTPException(status_code=401, detail="User not found")
    return user


36

미들웨어

  • 특정 경로 작동에 의해 처리되기 , 모든 요청에 대해서 동작하는 함수
    • 모든 응답이 반환되기 전에도 동일하게 동작
  • 미들웨어는 응용 프로그램으로 오는 요청를 가져옴
    • 요청 또는 다른 필요한 코드를 실행 시킬 수 있음
      • 요청을 응용 프로그램의 경로 작동으로 전달하여 처리함
  • 애플리케이션의 경로 작업에서 생성한 응답를 받음
    • 응답 또는 다른 필요한 코드를 실행시키는 동작 가능
      • 응답을 반환함

yield 존재 시

  • 만약 yield를 사용한 의존성을 가지고 있다면, 미들웨어가 실행되고 난 후에 exit이 실행됨
    • 만약 (나중에 문서에서 다룰) 백그라운드 작업이 있다면, 모든 미들웨어가 실행되고 난 후에 실행됨

미들웨어 만들기

  • @app.middleware("http")
  • 미들웨어 함수는 아래의 항목들을 받음
    • request
    • request를 매개변수로 받는 call_next 함수
      • 이 함수는 request를 해당하는 경로 작업으로 전달
      • 그런 다음, 경로 작업에 의해 생성된 response 를 반환
    • response를 반환하기 전에 추가로 response를 수정 가능
import time

from fastapi import FastAPI, Request

app = FastAPI()


@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    start_time = time.perf_counter()
    response = await call_next(request)
    process_time = time.perf_counter() - start_time
    response.headers["X-Process-Time"] = str(process_time)
    return response
  • 모든 HTTP 요청이 들어올 때마다 자동으로 실행되는 전처리/후처리 로직

  • @app.middleware("http")

    • @app.get(),@app.post() 같은 라우트보다 먼저 실행되고,
      • 응답을 반환하기 전에 가장 마지막으로 실행되는 코드
    • "http"를 지정하면 HTTP 요청 전체(모든 엔드포인트, 라우터 포함)에 대해 실행
  • async def add_process_time_header(request: Request, call_next)

    • request : 클라이언트 요청 객체 (Request 타입).
      • URL, 헤더, 바디 등 요청 정보가 들어 있음
    • call_next : 다음 단계(즉, 실제 라우트 함수)를 호출하는 콜백 함수야.
      • 이걸 호출해야 FastAPI가 라우터로 요청을 전달
        • call_next(request)가 없으면 라우터에 도달하지 않고, 응답X
  • start_time = time.perf_counter()

    • 요청이 들어온 순간의 "초 단위 타이머" 값을 기록
    • time.time()보다 perf_counter()는 훨씬 정확해서 처리시간 측정에 자주 사용
  • response = await call_next(request)

    • 실제 라우터로 요청을 넘겨서 실행시키는 부분
    • await를 붙이는 이유는 call_next가 비동기 함수이기 때문

실행순서

[Client Request]
       ↓
[Middleware 실행 시작]
       ↓ (1) start_time 기록
       ↓
(2) call_next(request) → 실제 라우터 실행
       ↓
(3) 라우터의 response 반환
       ↓
(4) 처리 시간 계산 및 헤더 추가
       ↓
[Client Response]
  • call_next(request)
    • “이 아래에 어떤 라우터가 있든, 그 라우터가 실행되도록 넘겨라.”


37

교차 출처 리소스 공유(CORS)

  • 공식문서
  • 브라우저에서 동작하는 프론트엔드가 자바스크립트 코드로 백엔드와 통신하고
    • 백엔드는 해당 프론트엔드와 다른 "출처"에 존재하는 상황을 의미

CORS가 필요한 이유

  • 웹 브라우저는 보안을 위해 “다른 출처(origin)”에서의 요청을 기본적으로 차단
    • ex. 백엔드(API): http://127.0.0.1:8000
    • 프론트엔드(Vue/React): http://localhost:3000
      • 이 경우 출처(origin)가 다르기 때문에, 브라우저는 API 요청을 차단
        • 이를 허용하기 위하여 서버 쪽에서 이 출처는 괜찮다 라고 명시적으로 허용 필요
          • 이게 CORS

출처

  • 프로토콜(http , https)
    • 도메인(myapp.com, localhost, localhost.tiangolo.com )
      • 포트(80, 443, 8080 )의 조합을 의미
  • ex. http://localhost / https://localhost / http://localhost:8080
    • 모두 localhost 에 있지만, 서로 다른 프로토콜과 포트를 사용하고 있으므로 다른 "출처"

단계

  • 예시
    • 브라우저 내 http://localhost:8080 에서 동작하는 프론트엔드
    • 자바스크립트는 http://localhost 를 통해 백엔드와 통신
      • (포트를 명시하지 않는 경우, 브라우저는 80 을 기본 포트로 간주)
  • 해결
    • 브라우저는 백엔드에 HTTP OPTIONS 요청을 보내고
    • 백엔드에서 다른 출처( http://localhost:8080 )와의 통신을 허가하는 적절한 헤더를 보내면
      • 브라우저는 프론트엔드의 자바스크립트가 백엔드에 요청을 보낼 수 있도록 함
        • 백엔드는 "허용된 출처(allowed origins)" 목록을 가지고 있어야만 함
        • 프론트엔드가 제대로 동작하기 위해 http://localhost:8080 을 목록에 포함 해야함

와일드카드

  • 모든 출처를 허용하기 위해 목록을 "*" ("와일드카드")로 선언하는 것도 가능
    • 이것은 특정한 유형의 통신만을 허용
    • 쿠키 및 액세스 토큰과 사용되는 인증 헤더(Authoriztion header) 등이
      • 포함된 경우와 같이 자격 증명(credentials)이 포함된 통신은 허용되지 않음
  • 따라서 모든 작업을 의도한대로 실행하기 위해, 허용되는 출처를 명시적으로 지정하는 것이 좋음

CORSMiddleware

  • FastAPI에서 CORS를 처리하려면 CORSMiddleware를 추가해야 함
    • 이 코드는 FastAPI의 미들웨어 중 하나로,요청(Request)과 응답(Response) 사이에 끼어들어서
      • ORS 관련 HTTP 헤더(Access-Control-Allow-Origin 등)를 자동으로 추가해줌
  • 실행
    • CORSMiddleware 임포트.
    • 허용되는 출처(문자열 형식)의 리스트 생성.
    • FastAPI 응용 프로그램에 "미들웨어(middleware)"로 추가.
  • 백엔드에서 다음의 사항을 허용할지에 대해 설정 가능
    • 자격증명 (인증 헤더, 쿠키 등).
    • 특정한 HTTP 메소드(POST, PUT) 또는 와일드카드 "*" 를 사용한 모든 HTTP 메소드.
    • 특정한 HTTP 헤더 또는 와일드카드 "*" 를 사용한 모든 HTTP 헤더.

CORSMiddleware 주요 옵션

옵션설명기본값 / 예시
allow_origins교차 출처 요청을 허용할 도메인 목록. ['*'] 로 모든 출처 허용 가능.기본값: []
allow_origin_regex허용할 출처를 정규표현식으로 지정.예: "https://.*\.example\.org"
allow_methods허용할 HTTP 메서드 목록. ['*'] 로 모든 메서드 허용.기본값: ['GET']
allow_headers요청 시 허용할 헤더 목록. ['*'] 로 모든 헤더 허용.기본값: [] (단, Accept, Content-Type 등은 항상 허용)
allow_credentials쿠키·인증정보 포함 요청 허용 여부. True일 경우 allow_origins=['*'] 사용 불가.기본값: False
expose_headers브라우저에서 접근 가능한 응답 헤더 목록.기본값: []
max_age브라우저가 CORS 사전 요청(Preflight) 결과를 캐싱하는 시간(초).기본값: 600

38

SQL (관계형) 데이터베이스

  • SQLModel 공식문서 , SQLAlchemy 공식문서
    • SQLModel은 SQLAlchemy와 Pydantic을 기반으로 구축
      • SQLModel은 SQL 데이터베이스를 사용하는 FastAPI 애플리케이션에 완벽히 어울리도록 FastAPI의 제작자가 설계한 도구

SQLModel 설치하기

  • poetry add sqlmodel

단일 모델로 애플리케이션 생성하기

모델 생성하기

from typing import Annotated

from fastapi import Depends, FastAPI, HTTPException, Query
from sqlmodel import Field, Session, SQLModel, create_engine, select


class Hero(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)
    secret_name: str
  • table=True
    • SQLModel에 이 모델이 테이블 모델이며, 단순한 데이터 모델이 아니라
    • SQL 데이터베이스의 테이블을 나타낸다는 것을 명시 (Pydantic 클래스처럼 단순 데이터모델X)
  • Field(primary_key=True) : 데이터베이스의 기본키
  • Field(index=True) : SQLModel에 해당 열에 대해 SQL 인덱스를 생성하도록 지시
    • 이를 통해 데이터베이스에서 이 열으로 필터링된 데이터를 읽을 때 더 빠르게 조회 가능
  • SQLModel 은 str으로 선언된 항목이 SQL 데이터베이스에서 TEXT (or VARCHAR) 유형의 열로 저장된다는 것을 인식 한다.

엔진 생성하기

  • SQLModel의 engine (내부적으로는 SQLAlchemy engine)
    • 데이터베이스에 대한 연결을 유지하는 역할
  • 하나의 단일 engine 객체를 통해 코드 전체에서 동일한 데이터베이스에 연결 가능
sqlite_file_name = "database.db"
sqlite_url = f"sqlite:///{sqlite_file_name}"

connect_args = {"check_same_thread": False}
engine = create_engine(sqlite_url, connect_args=connect_args)
  • sqlite_file_name = "database.db"
    • 사용할 SQLite 데이터베이스 파일 이름을 지정
    • FastAPI 또는 SQLAlchemy가 실행될 때, 같은 폴더에 database.db 파일이 자동 생성
    • 즉, "이 파일을 SQLite 데이터베이스로 쓰겠다"는 의미
  • sqlite_url = sqlite:///{sqlite_file_name}
    • sqlite: DB종류 , ///: 로컬 파일을 의미 , database.db: 사용할 DB 파일 이름
    • SQLAlchemy는 데이터베이스 연결을 URL 형식으로 표현
  • connect_args = {"check_same_thread": False}
    • SQLite는 기본적으로 “하나의 스레드만 동일한 연결을 사용 가능” 하도록 제한
      • FastAPI는 비동기(async) 방식이기 때문에, 여러 스레드에서 동시에 DB에 접근할 일이 생김
    • check_same_thread=True: (기본값) 같은 스레드만 DB 연결 사용 가능
    • check_same_thread=False: 여러 스레드에서도 같은 연결 사용 허용
  • engine = create_engine(sqlite_url, connect_args=connect_args)
    • create_engine() 함수는 실제 DB와 연결할 엔진(Engine) 객체를 생성
    • DB 연결을 관리 / SQL 실행을 처리 / ORM(Session, Base 등)과 연결

테이블 생성하기

def create_db_and_tables():
    SQLModel.metadata.create_all(engine)
  • SQLModel.metadata.create_all(engine)
    • 모든 테이블 모델의 테이블을 생성하는 함수

세션 의존성 생성하기

def get_session():
    with Session(engine) as session:
        yield session


SessionDep = Annotated[Session, Depends(get_session)]
  • Session
    • 메모리에 객체를 저장하고 데이터에 필요한 모든 변경 사항을 추적한 후,
    • engine을 통해 데이터베이스와 통신
    • yield를 사용해 FastAPI의 의존성을 생성하여 각 요청마다 새로운 Session을 제공
    • 이 의존성을 사용하는 코드를 간소화하기 위해 Annotated 의존성 SessionDep을 생성

시작 시 데이터베이스 테이블 생성하기

app = FastAPI()

@app.on_event("startup")
def on_startup():
    create_db_and_tables()
  • 애플리케이션 시작 이벤트 시 테이블을 생성

Hero 생성하기

@app.post("/heroes/")
def create_hero(hero: Hero, session: SessionDep) -> Hero:
    session.add(hero)
    session.commit()
    session.refresh(hero)
    return hero
  • SessionDep 의존성 (즉, Session)을 사용하여 새로운 Hero를 Session 인스턴스에 추가하고
    • 데이터베이스에 변경 사항을 커밋하고, hero 데이터의 최신 상태를 갱신한 다음 이를 반환

Heroes 조회하기

@app.get("/heroes/")
def read_heroes(
    session: SessionDep,
    offset: int = 0,
    limit: Annotated[int, Query(le=100)] = 100,
) -> list[Hero]:
    heroes = session.exec(select(Hero).offset(offset).limit(limit)).all() ⭐️
    return heroes
  • select()를 사용하여 데이터베이스에서 Hero를 조회 가능
    • 결과에 페이지네이션을 적용하기 위해 limit와 offset을 포함 가능

단일 Hero 조회하기

@app.get("/heroes/{hero_id}")
def read_hero(hero_id: int, session: SessionDep) -> Hero:
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    return hero

Hero 삭제하기

@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    session.delete(hero)
    session.commit()
    return {"ok": True}

여러 모델 생성하기

class HeroBase(SQLModel):
    name: str = Field(index=True)
    age: int | None = Field(default=None, index=True)


class Hero(HeroBase, table=True):
    id: int | None = Field(default=None, primary_key=True)
    secret_name: str


class HeroPublic(HeroBase):
    id: int
    
class HeroCreate(HeroBase):
    secret_name: str
    
class HeroUpdate(HeroBase):
    name: str | None = None
    age: int | None = None
    secret_name: str | None = None    
    
  • Hero는 HeroBase를 상속하므로 HeroBase에 선언된 필드도 포함
  • HeroPublic - 공개 데이터 모델
    • 이 모델은 API 클라이언트에 반환되는 모델
    • HeroBase와 동일한 필드를 가지며, secret_name은 포함하지 않음
  • HeroCreate - hero 생성용 데이터 모델
    • 이 모델은 클라이언트로부터 받은 데이터를 검증하는 역할
    • HeroBase와 동일한 필드를 가지며, 추가로 secret_name을 포함
    • 클라이언트가 새 hero을 생성할 때 secret_name을 보내고, 이는 데이터베이스에 저장되지만
      • 해당 비밀 이름은 API를 통해 클라이언트에게 반환되지 않음
  • HeroUpdate - hero 수정용 데이터 모델
    • 다중 모델을 통해 수정 기능을 추가 가능
    • 새 hero을 생성할 때 필요한 모든 동일한 필드를 가지지만, 모든 필드가 선택적(기본값이 있음)
      • 이렇게 하면 hero을 수정할 때 수정하려는 필드만 보낼 수 있음
    • 모든 필드가 변경되기 때문에(타입이 None을 포함하고, 기본값이 None으로 설정됨)
      • 모든 필드를 다시 선언 필요

HeroCreate로 생성하고 HeroPublic 반환하기

@app.post("/heroes/", response_model=HeroPublic)
def create_hero(hero: HeroCreate, session: SessionDep):
    db_hero = Hero.model_validate(hero)
    session.add(db_hero)
    session.commit()
    session.refresh(db_hero)
    return db_hero
  • 다중 모델을 사용하기 때문에 애플리케이션의 관련 부분을 업데이트 가능
    • 요청에서 HeroCreate 데이터 모델을 받아 이를 기반으로 Hero 테이블 모델을 생성
  • response_model로 HeroPublic 데이터 모델을 선언했기 때문에,
    • FastAPI는 HeroPublic을 사용하여 데이터를 검증하고 직렬화

HeroPublic으로 Heroes 조회하기

@app.get("/heroes/", response_model=list[HeroPublic]) ⭐️
def read_heroes(
    session: SessionDep,
    offset: int = 0,
    limit: Annotated[int, Query(le=100)] = 100,
):
    heroes = session.exec(select(Hero).offset(offset).limit(limit)).all()
    return heroes
  • 이전과 동일하게 Hero를 조회 가능
    • response_model=list[HeroPublic] 을 사용하여 데이터가 올바르게 검증되고 직렬화되도록 보장

HeroPublic으로 단일 Hero 조회하기(단일 히어로 조회)

@app.get("/heroes/{hero_id}", response_model=HeroPublic)
def read_hero(hero_id: int, session: SessionDep):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    return hero

HeroUpdate로 Hero 수정하기

  • HTTP PATCH 작업을 사용
  • 코드에서는 클라이언트가 보낸 데이터를 딕셔너리 형태(dict)로 가져옴
    • 이는 클라이언트가 보낸 데이터만 포함, 기본값으로 들어가는 값은 제외
      • 이를 위해 exclude_unset=True를 사용
@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(hero_id: int, hero: HeroUpdate, session: SessionDep):
    hero_db = session.get(Hero, hero_id)
    if not hero_db:
        raise HTTPException(status_code=404, detail="Hero not found")
    hero_data = hero.model_dump(exclude_unset=True)
    hero_db.sqlmodel_update(hero_data)
    session.add(hero_db)
    session.commit()
    session.refresh(hero_db)
    return hero_db

Hero 다시 삭제하기

@app.delete("/heroes/{hero_id}")
def delete_hero(hero_id: int, session: SessionDep):
    hero = session.get(Hero, hero_id)
    if not hero:
        raise HTTPException(status_code=404, detail="Hero not found")
    session.delete(hero)
    session.commit()
    return {"ok": True}

39

더 큰 응용 프로그램 - 여러 파일

  • FastAPI는 모든 유연성을 유지하면서 애플리케이션을 구조화할 수 있는 편리한 도구를 제공
    • Flask의 Blueprint 와 비슷한듯

APIRouter

  • 원래는 @app.get() 같이 FastAPI 인스턴스(app)에 직접 경로를 등록
    • 프로젝트가 커지면 모든 API를 한 파일에 넣기 어려움
      - 따라서, APIRouter를 사용해 기능별로(예: users, items, admin) 라우터를 분리
      - 나중에 main.py에서 app.include_router(router)로 메인 앱에 합치는 구조

APIRouter - plus

구분설명
APIRouter()여러 경로를 묶는 미니 앱
prefix="/items"/items 하위 경로로 통합
Depends(get_token_header)모든 요청에 헤더 토큰 검사 적용
read_items()/items/ 목록 조회
read_item()/items/{item_id} 단일 조회
update_item()/items/{item_id} 수정 (단, plumbus만)
HTTPException요청이 잘못되면 예외 발생 및 HTTP 상태 코드 반환
tags, responsesSwagger 문서용 메타데이터

dependencies

  • 의존성(Dependency)
    • 라우터가 실행되기 전에 반드시 실행되어야 하는 공통 로직”을 분리한 함수 또는 클래스
  • 여러 경로(path operation)에서
    • 공통으로 필요한 작업(예: 인증, 토큰 검증, DB 연결, 로그 기록 등)을
    • 따로 함수로 만들어놓고, FastAPI가 자동으로 주입(실행)해주는 구조
  • get_token_header()와 get_query_token()
    • FastAPI에서 “라우터가 실행되기 전에 자동으로 호출되어 요청의 유효성을 검사하는 의존성 함수”
      • 반복되는 인증/검증 로직을 한 번만 정의하고, 여러 API에 재사용 가능

token , header

구분담기는 위치누가 보냄예시
HeaderHTTP 요청 헤더클라이언트X-Token: fake-super-secret-token
Query ParameterURL 뒤에 ?token=jessica 형태클라이언트/items?token=jessica
BodyPOST/PUT 요청의 JSON 등클라이언트{ "title": "Test" }

. , .. , ...

구문의미경로 기준
from .module import something현재 디렉토리(.)같은 폴더
from ..module import something한 단계 위(..)상위 폴더
from ...module import something두 단계 위(...)상위의 상위 폴더


40

디버깅

  • Visual Studio Code 또는 PyCharm을 사용하여 편집기에서 디버거를 연결 가능

uvicorn 호출


41

백그라운드 작업

profile
안녕하세요.

0개의 댓글