이제 파이썬(Python)을 학습했으니 파이썬을 이용하는 FastAPI라는 것에 대해 학습해 볼 것이다.
사실 현재 개인적으로 진행하고 있는 사이드 프로젝트가 있는데 그 프로젝트에서는 Node.js를 이용한 Express를 사용해서 백엔드 기능들을 구현하고, 프론트엔드와 연동하는 작업을 진행하고 있다. FastAPI는 그와 같은 것인데 자바스크립트(JavaScript) 대신 파이썬을 사용하는 것으로 생각하면 된다.
FastAPI를 학습하려면 먼저 REST API가 무엇인지를 알아야 이해하기 좋다. FastAPI는 REST API 서버를 만들기 위한 프레임워크이므로 이 글에서도 REST API를 먼저 간단히 이해한 후에 FastAPI에 대해 자세히 알아볼 것이다.
REST API란?
✅ REST(Representational State Transfer)의 정의
자원(Resource)의 표현(Representation)에 따른 상태(State) 전달(Transfer) 방식으로, 쉽게 말해, "웹에서 URL과 HTTP를 활용해 리소스를 주고받는 방식"이라고 할 수 있다.
REST는 하나의 아키텍처 스타일이고, 웹의 특징을 최대한 활용할 수 있게 설계된 통신 원칙임.
REST 예시
요소 | REST 용어 | 예시 설명 |
메뉴판 | API 문서 | 사용 가능한 API 목록을 확인 |
음식 | 리소스(Resource) | 상품, 사용자 등 서버에 존재하는 데이터 |
주문서 | 요청(Request) | 특정 리소스를 어떻게 처리해달라는 요청 |
음식 배달 | 응답(Response) | 요청에 대한 처리 결과 반환 |
✅ REST의 개념 3가지
- 자원 (Resource)
- 웹에서 조작할 대상
- 보통 명사형 URI로 표현됨 (예: /products/123)
- 표현 (Representation)
- 클라이언트가 받는 데이터의 형식
- JSON, XML, HTML 등 다양함
- 상태 전이 (State Transfer)
- 리소스의 상태가 클라이언트의 요청을 통해 변경됨
- 예: 상품 상세 → 장바구니 추가 → 결제 전환
✅ REST의 6가지 원칙
원칙 | 설명 |
클라이언트-서버 구조 | UI와 데이터 처리 분리 → 독립적 진화 가능 |
무상태성 (Stateless) | 각 요청은 독립적이며 서버는 상태 저장 X |
캐시 가능 (Cacheable) | 응답에 캐싱 여부 명시 → 성능 향상 |
계층화 구조 (Layered System) | 로드 밸런서, 프록시 서버 등의 중간 계층 허용 |
일관된 인터페이스 (Uniform Interface) | HTTP 메서드와 URI 규칙 등 명확한 표준 제공 |
HATEOAS | 응답에 링크 포함 → 다음 상태 전이 유도 |
✅ HTTP 메서드와 REST 매핑
메서드 | 설명 | 예시 |
GET | 리소스 조회 | /books/1 |
POST | 리소스 생성 | /books |
PUT | 리소스 전체 수정 | /books/1 |
PATCH | 리소스 부분 수정 | /books/1 |
DELETE | 리소스 삭제 | /books/1 |
✅ 상태 코드(Status Code)
코드 | 의미 | 설명 |
200 | OK | 성공적인 요청 |
201 | Created | 새 리소스 생성 성공 |
204 | No Content | 응답 본문 없음 (삭제 등) |
400 | Bad Request | 잘못된 요청 |
401 | Unauthorized | 인증 필요 |
403 | Forbidden | 권한 없음 |
404 | Not Found | 리소스 없음 |
500 | Internal Server Error | 서버 내부 오류 |
✅ RESTful URI 설계 규칙
규칙 | 예시 |
명사 사용 | /users, /products |
복수형 사용 | /users (O), /user (X) |
계층 표현 | /users/123/orders |
소문자 사용 | /products (O), /Products (X) |
하이픈 사용 | /product-categories (O), _ 지양 |
확장자 미포함 | /users (O), /users.json (X) |
⚙️ 요약 정리
- REST는 웹 자원을 URI로 표현하고, HTTP 메서드로 조작하는 방식
- 핵심 원칙: 클라이언트-서버 분리, 무상태성, 캐시 가능, 계층화, 일관된 인터페이스, HATEOAS
- 리소스는 명사형 URI로 표현하고, HTTP 상태 코드로 결과를 명확하게 표현
- FastAPI는 이런 RESTful 원칙을 코드로 쉽게 구현하게 도와주는 도구
REST API를 학습했으니 우리는 FastAPI를 학습할 준비가 되었다고 할 수 있다. 파이썬을 기반으로 하는 FastAPI는 무엇이고, 어떻게 활용이 가능한지 정리해 볼 것이다.
FastAPI는 파이썬 표준 타입에 기초한 파이썬 기반 API를 빌드하기 위한 현대적이고, 빠른 웹 프레임워크라고 본인들을 소개하고 있다. 실제로 FastAPI는 Node.js와 Go와 견주어도 손색없을 정도로 높은 성능을 보여준다. 또, 빠른 개발 속도와 직관적이고 쉽게 코드를 작성할 수 있다는 장점으로 파이썬 환경에서 API를 구현할 때, FastAPI를 많이 사용하는 것 같다.
FastAPI의 설치와 같은 것부터 다루려 했으나 이미 많은 곳에 정리되어 있기에 설치 관련 내용은 넘어가도록 하고, FastAPI의 기본 개념부터 정리하며 학습해 볼 예정이다. 그럼 FastAPI의 앱을 생성해서 직관적으로 확인하면서 학습을 진행해 보자.
✅ FastAPI 앱 만들기
main.py 파일 생성:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello FastAPI!"}
✅ 서버 실행하기
uvicorn main:app --reload
- main : 파일명 (main.py)
- app : FastAPI 객체 변수명
- --reload : 코드 수정 시 서버 자동 재시작 (개발용 옵션)
실행 결과:
INFO: Uvicorn running on http://127.0.0.1:8000
INFO: Application startup complete.
✅ Swagger UI & ReDoc 접속
문서 타입 | 접속 경로 | 설명 |
Swagger UI | http://127.0.0.1:8000/docs | 기본 대화형 문서 |
ReDoc | http://127.0.0.1:8000/redoc | 시각적으로 깔끔한 문서 |
FastAPI 라우팅과 엔드포인트
✅ 라우팅(Routing)이란?
클라이언트가 특정 URL로 요청을 보냈을 때, 그 요청을 처리할 함수(=엔드포인트)와 연결해 주는 메커니즘
우편배달에 비유
요소 | 비유 |
URL 경로 | 주소 |
FastAPI 앱 | 우체국 |
라우터 | 우편배달부 |
핸들러 함수 | 수신자(주소지에 있는 사람) |
✅ FastAPI의 라우팅 방식
FastAPI는 데코레이터 기반의 선언적 라우팅을 사용한다. 이건 Flask와 비슷하면서도 훨씬 명확하고 타입 기반의 방식이라고 할 수 있다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/") # GET 요청 처리
def root():
return {"message": "Hello World"}
@app.get("/items/{item_id}") # 경로 매개변수 사용
def read_item(item_id: int):
return {"item_id": item_id}
✅ HTTP 메서드별 데코레이터
HTTP 메서드 | FastAPI 데코레이터 | 용도 |
GET | @app.get() | 조회 |
POST | @app.post() | 생성 |
PUT | @app.put() | 전체 수정 |
PATCH | @app.patch() | 일부 수정 |
DELETE | @app.delete() | 삭제 |
✅ 예시: CRUD 라우팅
from fastapi import FastAPI
app = FastAPI()
# GET 요청: 조회
@app.get("/users")
def get_users():
return {"users": ["Alice", "Bob"]}
# POST 요청: 생성
@app.post("/users")
def create_user(name: str):
return {"message": f"{name} created!"}
# GET 요청: 특정 사용자
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
# PUT 요청: 전체 수정
@app.put("/users/{user_id}")
def update_user(user_id: int, name: str):
return {"user_id": user_id, "new_name": name}
# DELETE 요청: 삭제
@app.delete("/users/{user_id}")
def delete_user(user_id: int):
return {"message": f"user {user_id} deleted"}
✅ 경로 변수의 타입
FastAPI는 자동으로 URL에서 값을 추출하고 타입을 변환해 준다.
@app.get("/items/{item_id}")
def get_item(item_id: int): # 자동으로 str → int 변환
return {"item_id": item_id}
✅ 라우팅 설계
- URI는 명사형, 복수형 사용
- /users, /products
- 계층 구조 표현
- /users/123/orders
- 소문자 + 하이픈 사용
- /product-categories
- 동사 표현 X (예: /getUser ❌)
⚙️ 요약 정리
항목 | 설명 |
라우팅 | URL 요청을 적절한 함수에 매핑 |
방식 | 데코레이터 기반 (@app.get(), @app.post() 등) |
경로 변수 | {}로 정의하며, 타입 힌트 사용 가능 |
URI 규칙 | 명사형, 소문자, 하이픈 사용, 계층 구조 표현 |
경로 매개변수 (Path Parameter)
✅ 정의
URL 경로 안에 포함된 동적인 값으로, 중괄호 {}로 표현하며, 함수 인자와 매칭된다.
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}
→ /users/7을 호출하면 user_id = 7로 함수 실행되고, int로 타입 힌트를 주면 문자열을 숫자로 자동 변환해 준다.
✅ 여러 경로 매개변수
@app.get("/users/{user_id}/orders/{order_id}")
def get_order(user_id: int, order_id: int):
return {"user_id": user_id, "order_id": order_id}
→ /users/5/orders/88 → 두 값을 모두 인자로 받음
✅ 경로 매개변수 + 유효성 검사
from fastapi import Path
@app.get("/items/{item_id}")
def read_item(item_id: int = Path(..., ge=1)):
return {"item_id": item_id}
- ... → 필수 매개변수
- ge=1 → 1 이상이어야 함
쿼리 매개변수 (Query Parameter)
✅ 정의
URL 뒤에? key=value 형태로 붙는 값들로, 함수 인자에 기본값을 주면 자동으로 쿼리 파라미터로 인식한다.
@app.get("/products")
def get_products(category: str = None, min_price: float = 0.0):
return {"category": category, "min_price": min_price}
→ /products?category=books&min_price=10.5
✅ 다중 값 받기 (List 타입)
from typing import List
@app.get("/products")
def get_products(tags: List[str] = Query(None)):
return {"tags": tags}
→ /products?tags=eco&tags=new → ["eco", "new"]
✅ 쿼리 파라미터에 유효성 검사 추가
from fastapi import Query
@app.get("/search")
def search_items(keyword: str = Query(..., min_length=2, max_length=20)):
return {"keyword": keyword}
- min_length, max_length, regex로 제한이 가능하다.
⚙️ Path vs Query 정리
항목 | 경로 매개변수 | 쿼리 매개변수 |
위치 | URL 경로에 포함 (/users/123) | URL ? 뒤에 (?name=tom) |
필수 여부 | 기본적으로 필수 | 기본값 있으면 선택 가능 |
유효성 검사 | Path() 사용 | Query() 사용 |
사용 예 | 특정 리소스 식별 | 필터링, 검색, 정렬 등 |
⚙️ 요약 정리
개념 | 요약 |
경로 매개변수 | URL 경로에 포함, {}로 정의 |
쿼리 매개변수 | ?key=value, 기본값 지정 시 자동 처리 |
유효성 검사 | Path(), Query() 사용 가능 |
타입 자동 변환 | int, float, List[str] 등 타입 힌트 사용 가능 |
FastAPI 문서화: Swagger UI & ReDoc
✅ 왜 API 문서가 중요할까?
이유 | 설명 |
🧑💻 개발자 협업 | 팀원이 API 구조를 쉽게 이해 |
🧪 빠른 테스트 | UI에서 API 호출 가능 |
🛠 유지보수 | 코드 바뀌면 문서도 자동 업데이트 |
❌ 오류 방지 | 요청/응답 형식 미스 줄이기 |
FastAPI의 자동 문서화 흐름
- 타입 힌트와 Pydantic 모델 기반으로
- OpenAPI 3.0 명세를 자동 생성
- Swagger UI와 ReDoc으로 시각화
✅ 문서 접속 방법
문서 종류 | 경로 | 특징 |
Swagger UI | /docs | 기본값, 대화형 테스트 가능 |
ReDoc | /redoc | 보기 깔끔, 정적인 문서 스타일 |
예: http://127.0.0.1:8000/docs
✅ Swagger UI 구성요소
항목 | 설명 |
API 목록 | 라우팅된 모든 엔드포인트가 자동 나열됨 |
Try it out | 직접 요청을 날려보고 응답 확인 가능 |
Response body | 응답 데이터 샘플 자동 생성 |
Status code | API 호출 결과 상태 확인 |
✅ 예시: 자동 문서 생성
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
name: str
age: int
email: str
@app.post("/users")
def create_user(user: User):
return {"message": f"{user.name}님이 등록되었습니다."}
→ /users 엔드포인트가 /docs에 자동 표시
→ 요청 본문 스키마도 자동 표시됨
✅ 태그 (Tag) 활용
@app.get("/menu", tags=["Menu"])
def get_menu():
return {"items": ["아메리카노", "카페라떼"]}
→ Swagger UI에서 같은 태그끼리 그룹화되어 보기 편해짐
✅ 문서 설명 추가하기
@app.post("/orders", summary="주문 생성", description="고객의 새 주문을 생성합니다.")
def create_order():
return {"message": "주문이 접수되었습니다."}
- summary: 요약 설명
- description: 상세 설명
⚙️ 요약 정리
항목 | 설명 |
문서화 | 코드만 작성해도 OpenAPI 기반 문서 자동 생성 |
문서 UI | /docs (Swagger), /redoc (ReDoc) |
장점 | 테스트, 협업, 유지보수에 강력 |
사용자 정의 | 태그, summary, description 등으로 구조화 가능 |
데이터 검증 및 변환 (Pydantic 기반)
✅ Pydantic이란?
Python의 타입 힌트를 활용해서 데이터를 자동으로 검증하고 직렬화/역직렬화해주는 라이브러리로, FastAPI는 내부적으로 Pydantic의 BaseModel을 이용해 데이터 모델을 정의하고 검증한다.
✅ BaseModel 사용법
from pydantic import BaseModel
class User(BaseModel):
name: str
age: int
email: str
이제 API에서 이 모델을 요청 바디에 매핑할 수 있다.
@app.post("/users")
def create_user(user: User):
return user
✅ 결과
- 자동으로 JSON Body 파싱
- 잘못된 타입이 들어오면 422 Unprocessable Entity 오류 발생
- /docs에서 자동으로 입력 폼 생성됨
✅ Field를 통한 세부 제약조건 설정
from pydantic import BaseModel, Field
class Product(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
price: float = Field(..., gt=0)
category: str = Field(..., regex="^(electronics|books|clothing)$")
옵션 | 설명 |
min_length, max_length | 문자열 길이 제한 |
gt, ge, lt, le | 숫자 크기 제한 (초과, 이상, 미만, 이하) |
regex | 정규식 조건 |
✅ 응답 데이터도 검증 가능
from pydantic import BaseModel
class ResponseUser(BaseModel):
name: str
email: str
@app.get("/users/{user_id}", response_model=ResponseUser)
def read_user(user_id: int):
return {"name": "Alice", "email": "alice@example.com", "age": 30}
- response_model은 응답 형식을 제한함
- 반환 값에 포함된 불필요한 필드(age)는 자동 제거됨
✅ 타입 자동 변환
Pydantic은 아래처럼 자동으로 형변환 기능도 포함하고 있다.
class Item(BaseModel):
price: float
# JSON: {"price": "123.45"} → float 변환 OK!
✅ 복잡한 모델도 가능 (중첩 모델)
class Address(BaseModel):
city: str
zipcode: str
class Customer(BaseModel):
name: str
address: Address
- JSON 구조가 중첩되어 있어도 문제없이 검증됨
- 응답 스키마도 자동으로 생성됨
✅ @field_validator 사용 (복잡한 조건)
from pydantic import field_validator
class User(BaseModel):
password: str
@field_validator("password")
@classmethod
def check_password(cls, v):
if "123" in v:
raise ValueError("너무 쉬운 비밀번호입니다!")
return v
→ 복잡한 비즈니스 로직도 추가가 가능하다.
⚙️ 요약 정리
기능 | 설명 |
BaseModel | 입력 데이터 구조 정의 |
Field() | 제약 조건 설정 |
response_model | 응답 형태 제한 |
중첩 모델 | 복잡한 JSON도 대응 가능 |
자동 형변환 | "123" → 123으로 자동 변환 |
검증 실패 | 422 에러 + 어떤 필드에서 실패했는지 명확히 표시 |
요청(Request)과 응답(Response)의 구조
✅ HTTP 기본 구조
구분 | 내용 |
요청(Request) | 클라이언트 → 서버 |
응답(Response) | 서버 → 클라이언트 |
🔹 요청 예시
GET /api/users HTTP/1.1
Host: example.com
Content-Type: application/json
🔹 응답 예시
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 85
{"users": [{"id": 1, "name": "홍길동"}]}
✅ 요청(Request)의 구성 요소
구성 | 설명 |
요청 라인 | GET /users HTTP/1.1 |
요청 헤더 | Content-Type, Authorization, 등 |
요청 본문 | 주로 POST, PUT에서 JSON 등 데이터를 포함 |
✅ 응답(Response)의 구성 요소
구성 | 설명 |
상태 라인 | HTTP/1.1 200 OK |
응답 헤더 | Content-Type, Content-Length 등 |
응답 본문 | 실제 데이터(JSON, HTML, 파일 등) |
✅ FastAPI 응답 유형 예시
1. JSON 응답 (기본)
@app.get("/hello")
def hello():
return {"message": "Hello World"}
2. 문자열 응답
from fastapi.responses import PlainTextResponse
@app.get("/text", response_class=PlainTextResponse)
def get_text():
return "단순 텍스트 응답"
3. HTML 응답
from fastapi.responses import HTMLResponse
@app.get("/html", response_class=HTMLResponse)
def get_html():
return "<h1>HTML 응답</h1>"
✅ 파일 업로드 (Request Body가 파일인 경우)
from fastapi import File, UploadFile
@app.post("/upload")
def upload_file(file: UploadFile = File(...)):
return {"filename": file.filename}
- 파일 이름, 타입, 바이트 등 메타데이터 접근 가능
- Postman에서 테스트 시, form-data > type을 File로 설정
✅ 응답 커스터마이징 (상태코드 & 헤더)
from fastapi import Response, status
@app.get("/custom")
def custom_response():
return Response(content="OK", media_type="text/plain", status_code=status.HTTP_202_ACCEPTED)
⚠️ 상태 코드 정리
코드 | 의미 |
200 | OK |
201 | Created |
204 | No Content |
400 | Bad Request |
401 | Unauthorized |
403 | Forbidden |
404 | Not Found |
500 | Internal Server Error |
✅ 요청 헤더 받기
from fastapi import Header
@app.get("/headers")
def get_headers(user_agent: str = Header(...)):
return {"User-Agent": user_agent}
✅ 응답 헤더 설정
from fastapi.responses import JSONResponse
@app.get("/with-headers")
def response_with_headers():
return JSONResponse(
content={"msg": "헤더 포함 응답"},
headers={"X-Custom-Header": "HeaderValue"}
)
⚙️ 요약 정리
항목 | 설명 |
요청 본문 | POST, PUT 등에서 JSON, 파일 등 포함 |
응답 본문 | 기본은 JSON, 필요시 HTML, 텍스트, 파일 가능 |
커스터마이징 | 상태코드, 헤더, 미디어 타입 설정 가능 |
파일 업로드 | UploadFile, File() 사용 |
문서 자동 반영 | 모든 응답 형태가 /docs에 자동 적용됨 |
의존성 주입(Dependency Injection)
어떤 함수(또는 클래스)가 필요로 하는 객체나 값을 외부에서 제공받도록 하는 설계 방식이다.
바리스타 비유
- 바리스타가 커피를 만들기 위해 매번 원두, 물, 컵을 직접 준비하지 않고, 이미 준비된 재료(=의존성)를 받아서 커피만 만드는 것
✅ FastAPI의 의존성 주입 방식
FastAPI에서는 Depends()를 통해 의존성을 선언한다. 그리고 공통 로직, 설정, 인증, DB 연결 등을 분리할 수 있다.
기본 예제
from fastapi import Depends, FastAPI
app = FastAPI()
def get_token():
return "secret-token"
@app.get("/secure-data")
def read_data(token: str = Depends(get_token)):
return {"token": token}
- Depends(get_token)은 token 자리에 함수를 실행해 반환값을 자동 주입함
- 내부적으로 get_token()이 먼저 실행되고 그 결과가 token으로 들어가게 됨
요청 범위 내 캐싱
def get_value():
print("함수 실행됨")
return "cached"
@app.get("/test1")
def test1(data=Depends(get_value)):
return {"value": data}
@app.get("/test2")
def test2(
data1=Depends(get_value),
data2=Depends(get_value)
):
return {"d1": data1, "d2": data2}
- test1: 요청마다 get_value()가 실행됨
- test2: 한 요청 내에서는 1회만 실행되고 재사용됨
예: 인증 & 권한 분리
from fastapi import HTTPException, status
def get_current_user(token: str = Depends(get_token)):
if token != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
return {"username": "admin"}
@app.get("/admin")
def read_admin(user=Depends(get_current_user)):
return {"msg": "관리자 전용"}
✅ 의존성의 의존성
def get_db_settings():
return {"url": "db-url"}
def get_db(settings = Depends(get_db_settings)):
return f"Connected to {settings['url']}"
@app.get("/db")
def db_test(conn=Depends(get_db)):
return {"conn": conn}
- 의존성 안에 또 다른 의존성을 쓸 수 있음 → 계층 구조 지원
설정을 전역에서 한 번만 로딩 (@lru_cache())
from functools import lru_cache
@lru_cache()
def get_settings():
return {"mode": "production"}
- 설정 같은 불변값은 캐싱해서 매 요청마다 새로 계산하지 않도록 할 수 있음
⚙️ 요약 정리
개념 | 설명 |
Depends() | 의존성 함수 주입 |
캐싱 범위 | 요청 범위 내 1회 실행, 요청마다 초기화 |
사용 용도 | 인증, 설정, DB 연결, 로깅 등 |
중첩 지원 | 의존성 안에 의존성 사용 가능 |
성능 향상 | 불변 값은 @lru_cache()로 캐싱 가능 |
이번엔 FastAPI에 대해 정리하며 학습해 보았다. 사실 FastAPI와 같은 것들은 이론으로 이해하는 것보다 직접 사용해 보고 코드를 작성해 보면서 어떻게 사용하는지를 익히고, 정리하는 것이 좋은 방법이라고 생각된다.
이번 글에서 FastAPI에 대해 정리하면서 학습해 보았으니, 다음에는 예제 코드들을 통해 어떻게 사용되고, 어떻게 기능들이 작동하는지를 알아보면 좋을 것 같다. 그리고 해당 과정의 학습 내용이 아니라 개인적으로 하고 있는 프로젝트(..?)에서든 FastAPI가 아니라 Node.js를 사용한 Express를 사용하여서 백엔드를 구축하고 있어서 서로 비교해 보고, 어떤 점이 다른지 익히기에 좋을 것 같다는 생각이다.
본 후기는 [카카오엔터프라이즈x스나이퍼팩토리] 카카오클라우드로 배우는 AIaaS 마스터 클래스 (B-log) 리뷰로 작성 되었습니다.