REST API 기초
REST (Representational State Transfer) 란?
웹 서비스를 설계하는 아키텍처 스타일
자원(Resource)의 표현(Representation)에 의한 상태(State) 전달(Transfer)을 의미
REST의 핵심 개념
- 자원(Resource)
URI로 표현되는 데이터 (예: /users, /products/1) - 표현(Representation)
자원의 상태를 전달하는 방식 (주로 JSON, XML 등) - 상태 전이(State Transfer)
클라이언트가 서버의 상태를 바꾸려면 데이터를 보내고, 서버는 응답으로 상태를 알려줌
REST의 특징
- 무상태성(Stateless)
각 요청은 독립적이며, 서버는 클라이언트의 상태를 저장하지 않음
예시 : 로그인 후 API 호출 시 매번 인증 토큰을 함께 보내야 함 - 통합 인터페이스(Uniform Interface)
모든 가전제품의 전원 버튼이 비슷한 위치에 있는 것처럼, REST API는 모든 리소스에 대해 일관된 인터페이스를 제공
예시 : HTTP 메서드(GET, POST, PUT, DELETE)를 사용해 모든 리소스를 같은 방식으로 처리
API (Application Programming Interface) 란?
• 프로그램(앱)과 프로그램이 서로 기능이나 데이터를 요청·응답할 수 있게 해주는 문법/약속
ex) GET /users/1
REST API 설계 원칙
- 클라이언트-서버 구조 (Client-Server Architecture)
예: 브라우저(클라이언트)와 FastAPI 백엔드(서버) - 무상태 (Stateless)
예: 요청마다 인증 토큰 포함 필요 (세션 유지 X) - 캐시 가능성 (Cacheable)
예: Cache-Control HTTP 헤더 활용 - 계층화 시스템 (Layered System)
예: 인증 서버, 로드밸런서, 게이트웨이 등을 끼워도 무방함 - 일관된 인터페이스 (Uniform Interface)
예: HTTP 메서드 (GET, POST, PUT, DELETE)
참고) URI 설계 원칙
- 자원(Resource)은 명사로 표현
✅ /users, /products, /orders - 소문자 사용
✅ /users
❌ /Users - 복수형 사용
✅ /orders
❌ /order - HTTP 메서드로 동작을 구분
✅ GET /users (조회), POST /users (생성) - 자원 식별자는 경로에 포함
✅ /users/123 - 중첩 관계는 URL로 표현
✅ /users/123/orders → 사용자 123의 주문 목록 - 동사 사용 ❌ (URL에는 동작 X)
✅ /users + GET
❌ /getUsers - 필터링/정렬은 쿼리 파라미터 사용
✅ /products?category=coffee&sort=price - 하이픈(-)으로 구분, 언더스코어(_)는 ❌
✅ /user-orders
❌ /user_orders - 파일 확장자 붙이지 않음
✅ /users
❌ /users.json
FastAPI 소개 및 설치
FastAPI는 Python으로 웹 API를 빠르고 쉽게 만들 수 있는 현대적인 웹 프레임워크
특징 : 빠른 성능, 자동 문서화(Swagger UI), 타입 안정성
FastAPI 설치 및 환경 설정
# 설치
pip install fastapi
pip install uvicorn[standard]
# 설치 버전 확인
from fastapi import FastAPI
print(fastapi.__version__)
Postman 활용
- 백엔드와 데이터 송수신을 테스트 할 수 있는 프로그램
- get 외에도 post, put 등의 메서드 확인 시 유용
참고) 다운로드 링크
참고) Postman을 활용하여 Get 메서드로 연결 테스트
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "FastAPI 설치 완료!"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

FastAPI 기본개념
예제 - Hello World API
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "안녕하세요! FastAPI 카페입니다!"}
@app.get("/hello")
def say_hello():
return {"greeting": "Hello World", "cafe": "FastAPI Coffee Shot"}
fastapi로 python 앱 실행하기
# 참고) 파일 이름은 main.py여야 한다.
# Dev 버전 실행
# reload : 파일이 변경될 때 자동으로 서버 재실행 (파일을 수정하고 저장하면 내용이 바로 적용됨)
uvicorn main:app --reload
# Production 버전 실행
# --host 0.0.0.0 : 외부환경에서 접속 가능하도록 설정 / 도커 이미지 등으로 인프라 구성시
# --workers : 멀티 프로세싱
uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4

선언적 라우팅이란 ?
Fast API는 선언적 라우팅 방식 사용
• 이 함수가 무엇을 할지 데코레이터로 선언하는 방식
# 선언적 방식: "이 함수는 GET /menu 요청을 처리합니다"
@app.get("/menu") # ← 선언문
def get_menu(): # ← 실제 로직
return {"메뉴": ["커피"]}
< 데코레이터의 의미 >
@app.get() : 리소스 조회에 활용
@app.post() : 리소스 추가에 활용
@app.put() vs @app.patch() :
- Put : 전체 리소스 수정에 활용
- Patch : 일부 필드 수정에 활용
@app.delete() : 리소스 삭제에 활용
* 선언적 라우팅의 반대되는 개념인 명령적 라우팅은 대부분의 웹 프레임워크에서 거의 사용하지 않는다고 하니 넘어가겠다.
참고) Postman에서 JSON 데이터 전송하는 법 (ex. post)
from fastapi import FastAPI
app = FastAPI()
@app.post("/orders")
def create_order(order_data: dict):
return {"메시지": "주문 완료", "주문 번호": 123}
Body - raw (JSON) 선택 후 JSON 데이터 입력하기

경로 매개변수 (Path Parameters)
아래 예제에서 {user_id} 가 경로 변수이다.
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(user_id: int):
return {"user_id": user_id}

쿼리스트링 활용
URL?key1=value1&key2=value2&key3=value3
- ? ← 시작 표시 (쿼리스트링 시작을 알림)
- & ← 구분자 (여러 정보를 나누는 기호)
- = ← 연결자 (이름과 값을 연결)
ex) http://127.0.0.1:8000/products?category=electronics&min_price=100&tags=sale,new,popular
Optional
필수 값이 아닌 경우 Optional로 설정 가능
* import 필요 : from typing import Optional
from fastapi import FastAPI, Query
from typing import Optional, List
app = FastAPI()
@app.get("/products")
def get_products(
category : Optional[str] = None, # 선택항목 기본값: None
min_price : Optional[float] = None, # 선택항목 기본값: None
max_price : Optional[float] = None, # 선택항목 기본값: None
tags : List[str] = Query(default = []), # 선택항목 기본값: []
in_stock : bool = True # 선택항목 기본값: True
):
tags, in_stock 은 Optional을 사용하지 않았지만 기본값이 지정되어 있기 때문에 선택항목이다.
참고) 만약 2개 항목을 필수값으로 지정하고 싶다면...
- tags: List[str] = Query(...)
- in_stock: bool = Query(...)
딥다이브)
Optional을 사용하지 않아도 기본값이 있으면 선택항목으로 취급된다. 그렇다면 아래 두 코드는 같은 의미일까?
category : Optional[str] = None
category: str = None
1. category: str = None
- Python 자체로는 허용되지만, str에는 원래 None을 넣을 수 없습니다.
- None이 들어가면 정적 분석기(mypy, pyright 등)에서 경고가 날 수 있습니다.
- 개발자 입장에서 문서적으로 불명확합니다.
2. category: Optional[str] = None
- Optional[str]은 정확히 str 또는 None이 허용됨을 명시합니다.
- FastAPI는 이를 보고 문서(Swagger)에서도 선택 항목으로 표시합니다.
- 타입 힌트를 지키는 도구들과도 완벽하게 호환됩니다.
기능은 같지만, Optional[...]을 쓰는 것이 훨씬 더 명확하고 안전합니다.
# ❌ 불명확
category: str = None
# ✅ 명확 + 안전 + 권장
category: Optional[str] = None
Query 클래스
쿼리 매개변수에 더 세세한 규칙을 정하는 도구
참고) FastAPI Query(...)에서 자주 쓰는 옵션 (총 8선)
| 옵션 이름 | 예시 코드 | 의미 및 설명 |
| default | Query(default=2) | 기본값 설정 (없으면 필수로 처리) |
| ... (Ellipsis) | Query(...) | 필수 파라미터 지정 |
| title | Query(..., title="사용자 나이") | Swagger 문서에 표시될 제목 |
| description | Query(..., description="18세 이상만 가능") | Swagger 문서에 표시될 설명 |
| gt | Query(..., gt=0) | 초과: 0보다 커야 함 (greater than) |
| ge | Query(..., ge=1) | 이상: 1 이상 허용 (greater or equal) |
| lt | Query(..., lt=100) | 미만: 100보다 작아야 함 (less than) |
| le | Query(..., le=65) | 이하: 65 이하 허용 (less or equal) |
참고) 사용 예제
1. 숫자 제약 조건
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/restaurant/order")
def order_food(
# 나이 제한 (18세 이상 65세 이하)
age: int = Query(..., ge=18, le=65, description="주문자 나이"), # 필수 항목 (...)
# 인원수 (1명 이상 10명 이하)
people: int = Query(2, gt=0, lt=11, description="식사 인원"), # 선택 항목 (기본값 2)
# 예산 (최소 1만원)
budget: float = Query(None, ge=10000, description="예산 (원)"), # 선택 항목 (기본값 None)
):
return {"주문정보": f"나이: {age}, 인원: {people}, 예산: {budget}"}
2. 문자 제약 조건
@app.get("/user/register")
def register_user(
# 사용자명 (3~20자) # 필수 항목 (...)
username: str = Query(..., min_length=3, max_length=20, description="사용자명"),
):
return {"등록정보": {"username": username}}
Pydantic 모델 활용
Pydantic : Python 타입 힌트 기반의 데이터 검증 라이브러리
참고) Pydantic 모델 활용 예제
예제 코드
from pydantic import BaseModel
from fastapi import FastAPI
from typing import List
app = FastAPI()
# 주문 정보를 나타내는 데이터 모델 정의의
class Order(BaseModel) :
menu_item: str
quantity: int
customer_name: str
special_request: str = "없음"
# 여러 주문과 총 개수를 담는 모델 (조회에 사용)
class OrderList(BaseModel) :
orders: List[Order]
total_count: int
# 주문 접수 API
@app.post("/orders")
def create_order(order : Order) :
return {
"message": "주문이 접수되었습니다!",
"order_details": {
"메뉴": order.menu_item,
"수량": order.quantity,
"고객명": order.customer_name,
"특별요청": order.special_request
},
"order_id": 12345
}
# 주문 조회 API
@app.get("/orders", response_model=OrderList)
def get_orders():
return {
"orders" : [
{
"menu_item": "메뉴",
"quantity": 123,
"customer_name": "임꺽정",
"special_request": "시럽추가"
}
],
"total_count" : 1
}
테스트용 JSON 데이터
{
"menu_item": "아메리카노",
"quantity": 2,
"customer_name": "홍길동",
"special_request": "얼음 적게"
}
라우팅과 엔드포인트 설계
라우팅과 엔드포인트
# API 엔드포인트
/users/{user_id}
# 라우팅
@app.get("/users/{user_id}")
참고) 좋은 URL 설계
GET users # 사용자 목록 조회
POST users # 새 사용자 생성
GET users/123 # 특정 사용자 조회
PUT users/123 # 특정 사용자 전체 수정
PATCH users/123 # 특정 사용자 부분 수정
DELETE users/123 # 특정 사용자 삭제
GET users/123/posts # 특정 사용자의 게시글 목록
라우트 그룹화 및 관리
router를 쓰지 않고 app만 사용한다면 모든 API를 한 파일에 몰아서 관리해야 하므로 유지보수가 힘들다.
# 이런 코드가 한 파일에 수십개씩 있다면.. 불편하다
@app.get("/users/")
def read_users(): ...
하지만 아래와 같이 router를 이용해서 API를 모듈화하면 편하다
• 예시 프로젝트 구조
project/
├── main.py
├── routers/
│ ├── __init__.py
│ ├── users.py
│ └── products.py
실무에서는 메인 FastAPI 앱 (main.py) 에서 라우터 (users_router) 를 다음과 같이 등록해서 사용한다.
# main.py
from fastapi import FastAPI
from user_routes import router as user_router
app = FastAPI()
# 이걸 통해 /users/ 이하의 엔드포인트가 등록됨
app.include_router(users_router)
APIRouter 인스턴스를 생성하는 기본 방법
- prefix : 이 라우터의 모든 경로 앞에 붙는 URL 접두어 (/users)
- tags : Swagger 문서에서 그룹 이름으로 사용됨
- responses : 공통 에러 응답 처리 정의 (예: 404 Not found 설명 추가)
# 라우터 인스턴스 생성
# APIRouter : FastAPI의 라우터 인스턴스
users_router = APIRouter(
prefix="/users",
tags=["users"],
responses={404: {"description": "Not found"}},
)
참고) 예제 전체 코드
1. 사용자 관련 라우트 구성
# routers/users.py
from fastapi import APIRouter, HTTPException
from typing import List
from pydantic import BaseModel
# 라우터 인스턴스 생성
users_router = APIRouter(
prefix="/users",
tags=["users"],
responses={404: {"description": "Not found"}},
)
# Pydantic 모델 정의
class User(BaseModel):
id: int
name: str
email: str
is_active: bool = True
# 새 사용자 생성, 사용자 정보 수정시 사용용
class UserCreate(BaseModel):
name: str
email: str
# 모든 사용자 목록 조회 ------------------------------------------------------------
@users_router.get("/", response_model=List[User])
def read_users(skip: int = 0, limit: int = 100):
return fake_users_db[skip : skip + limit]
# 특정 사용자 조회 ---------------------------------------------------------------
@users_router.get("/{user_id}", response_model=User)
def read_user(user_id: int):
for user in fake_users_db:
if user.id == user_id:
return user
raise HTTPException(status_code=404, detail="User not found")
# 새 사용자 생성 -----------------------------------------------------------------
@users_router.post("/", response_model=User)
def create_user(user: UserCreate):
new_id = len(fake_users_db) + 1
new_user = User(id=new_id, **user.dict()) # **user.dict(): 딕셔너리 -> 키워드 인자
fake_users_db.append(new_user)
return new_user
# 사용자 정보 수정 ------------------------------------------------------------
@users_router.put("/{user_id}", response_model=User)
def update_user(user_id: int, user_update: UserCreate):
for i, user in enumerate(fake_users_db):
if user.id == user_id:
updated_user = User(id=user_id, **user_update.dict())
fake_users_db[i] = updated_user
return updated_user
raise HTTPException(status_code=404, detail="User not found")
# 사용자 삭제 -----------------------------------------------------------------
@users_router.delete("/{user_id}")
def delete_user(user_id: int):
for i, user in enumerate(fake_users_db):
if user.id == user_id:
del fake_users_db[i]
return {"message": "User deleted successfully"}
raise HTTPException(status_code=404, detail="User not found")
# 더미 데이터베이스 ------------------------------------------------------------
fake_users_db = [
User(id=1, name="Alice", email="alice@example.com"),
User(id=2, name="Bob", email="bob@example.com"),
]
2. 상품 관련 라우트 구성
# /routers/products.py
from fastapi import APIRouter, Query, HTTPException
from typing import List, Optional
from pydantic import BaseModel
from enum import Enum
# 라우터 인스턴스 생성
products_router = APIRouter(
prefix="/products",
tags=["products"],
)
# 파이썬의 이넘 클래스
class CategoryEnum(str, Enum):
electronics = "electronics"
clothing = "clothing"
books = "books"
class Product(BaseModel):
id: int
name: str
price: float
category: CategoryEnum
description: Optional[str] = None
# 상품 목록 조회 (필터링 지원) ------------------------------------------------
@products_router.get("/", response_model=List[Product])
def read_products(
category: Optional[CategoryEnum] = None,
min_price: Optional[float] = Query(None, ge=0),
max_price: Optional[float] = Query(None, ge=0),
skip: int = 0,
limit: int = 100
):
products = fake_products_db
# 카테고리 필터링
if category:
products = [p for p in products if p.category == category]
# 가격 필터링
if min_price is not None:
products = [p for p in products if p.price >= min_price]
if max_price is not None:
products = [p for p in products if p.price <= max_price]
return products[skip : skip + limit]
# 특정 상품 조회 ------------------------------------------------------------
@products_router.get("/{product_id}", response_model=Product)
def read_product(product_id: int):
for product in fake_products_db:
if product.id == product_id:
return product
raise HTTPException(status_code=404, detail="Product not found")
# 더미 데이터베이스 ------------------------------------------------------------
fake_products_db = [
Product(
id=1,
name="Wireless Headphones",
price=89.99,
category=CategoryEnum.electronics,
description="Noise-cancelling over-ear Bluetooth headphones"
),
Product(
id=2,
name="Cotton T-Shirt",
price=19.99,
category=CategoryEnum.clothing,
description="100% cotton unisex T-shirt"
),
Product(
id=3,
name="The Art of FastAPI",
price=29.50,
category=CategoryEnum.books,
description="Beginner to advanced guide on building APIs with FastAPI"
)
]
3. 메인 어플리케이션
# /main.py
from fastapi import FastAPI
from routers import users, products
app = FastAPI(
title="My Store API",
description="온라인 상점을 위한 RESTful API",
version="1.0.0",
)
# 라우터 등록
app.include_router(users.users_router)
app.include_router(products.products_router)
4. 테스트 시나리오
📦 상품(products) API 테스트
1. 상품 전체 조회
- GET http://localhost:8000/products/
2. 카테고리 필터링
- GET http://localhost:8000/products/?category=electronics
3. 가격 필터링 (최소 20 이상)
- GET http://localhost:8000/products/?min_price=20
4. 가격 필터링 (최대 50 이하)
- GET http://localhost:8000/products/?max_price=50
5. 카테고리 + 가격 필터링
- GET http://localhost:8000/products/?category=books&min_price=10&max_price=40
6. 상품 상세 조회
- GET http://localhost:8000/products/2
(Cotton T-Shirt 정보 조회)
👤 사용자(users) API 테스트
1. 전체 사용자 조회
- GET http://localhost:8000/users/
2. 특정 사용자 조회
- GET http://localhost:8000/users/1
(Alice 정보)
3. 새 사용자 등록
- POST http://localhost:8000/users/
Body (JSON):
{
"name": "Charlie",
"email": "charlie@example.com"
}
4. 사용자 정보 수정
- PUT http://localhost:8000/users/2
Body (JSON):
{
"name": "Bobby",
"email": "bobby@example.com"
}
5. 사용자 삭제
- DELETE http://localhost:8000/users/1
경로 변환기 (Path Converters)
예제: 기본 Path 파라미터
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
def read_item(item_id: int): # ← 여기서 int가 변환기 역할
return {"item_id": item_id}
Path 파라미터에 옵션 추가하기: Path()
FastAPI는 fastapi.Path()를 사용해서 제약 조건이나 메타데이터를 줄 수 있다.
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(
user_id: int = Path(..., ge=1, description="유저 ID (1 이상)")
):
return {"user_id": user_id}
ge, description 등의 조건은 아까 쿼리 매개변수에서도 다뤘다. 그럼 Path와 Query는 무슨 차이일까?
딥다이브) Path 파라미터 vs Query 파라미터
같은 코드를 각각 Path 와 Query 를 사용한 예제를 만들었다.
1. Path 파라미터 사용 예제
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/users/{user_id}")
def get_user(
user_id: int = Path(..., ge=1, description="유저 ID (1 이상)")
):
return {"user_id": user_id}
2. Query 파라미터 사용 예제
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/users/")
def get_user(
user_id: int = Query(..., ge=1, description="유저 ID (1 이상)")
):
return {"user_id": user_id}
큰 차이는 없어보이는데 도대체 무슨 차이가 있으며, 어떤 걸 사용해야 할까?
결론부터 말하면 Path 를 사용하는게 좀더 RESTful한 설계 방식에 알맞다.
그 이유는 API를 요청하는 방법에 있다.
# Path 사용시 요청 방법
GET /users/3
# Query 사용시 요청 방법
GET /users/?user_id=3
1. Path
3번 유저라는 고유한 리소스를 조회하는 것.
ex)
블로그 글 /posts/25
사용자 /users/3
제품 상세페이지 /products/101
2. Query
user_id라는 조건으로 마치 필터링된 결과를 요청하는 것처럼 보임.
즉, "여러 유저 중에서" user_id가 3인 유저를 찾는다는 의미로 해석될 수 있음.
보통 이런 구조는 검색, 정렬, 필터링에 사용됨.
ex)
검색 /users?name=alice
정렬 /users?sort=created_at
페이징 /users?limit=10&offset=20
결론
- user_id, item_id 같은 식별자는 Path 파라미터로 사용하는 게 RESTful 스타일에 더 적합합니다.
- 검색 옵션, 필터링 값은 Query 파라미터로 처리하는 게 일반적입니다.
본 후기는 [카카오엔터프라이즈x스나이퍼팩토리] 카카오클라우드로 배우는 AIaaS 마스터 클래스 (B-log) 리뷰로 작성 되었습니다.
'학습일지 > K-Digital Traing' 카테고리의 다른 글
| [KDT] AIaaS 마스터클래스 12주차 - RAG 개념 (1) | 2025.06.15 |
|---|---|
| [KDT] AIaaS 마스터클래스 11주차 - 머신러닝 입문 (0) | 2025.06.04 |
| [KDT] AIaaS 마스터클래스 9주차 - Python 입문 (0) | 2025.05.19 |
| [KDT] AIaaS 마스터클래스 8주차 - 리액트 상태관리와 Redux (1) | 2025.05.16 |
| [KDT] AIaaS 마스터클래스 8주차 - 이벤트 핸들링과 상태(state) (0) | 2025.05.15 |