Skip to content

Latest commit

ย 

History

32 Commits

Folders and files

NameName
Last commit message
Last commit date
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

FastAPI Server Test

FastAPI, PostgreSQL, SQLAlchemy 2๋กœ ์›น๊ณผ ๋ชจ๋ฐ”์ผ ์•ฑ์ด ํ•จ๊ป˜ ์‚ฌ์šฉํ•  REST API๋ฅผ ๊ตฌํ˜„ํ•œ ํ•™์Šต์šฉ ์„œ๋ฒ„๋‹ค. ๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ, ํšŒ์ „ํ˜• Refresh Token, ์—ญํ•  ๊ธฐ๋ฐ˜ ์ƒํ’ˆ ๊ด€๋ฆฌ, ์ •ํ˜•ํ™”๋œ ์„ฑ๊ณต ์‘๋‹ต, RFC 9457 Problem Details, OpenAPI ๊ณ„์•ฝ์„ ์‹ค์ œ ์ฝ”๋“œ์™€ ํ…Œ์ŠคํŠธ๋กœ ๋ณด์—ฌ์ค€๋‹ค.

๋น„๊ต ๋Œ€์ƒ์ธ nestjs-monorepo-test์™€ ํ†ต์‹  ๊ณ„์•ฝ๊ณผ ๊ณ„์ธต์„ ๋น„๊ตํ•˜๋˜, ์ด ์ €์žฅ์†Œ๋Š” ๋ชจ๋…ธ๋ ˆํฌ๊ฐ€ ์•„๋‹ˆ๋‹ค. ํ•˜๋‚˜์˜ FastAPI ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ ํ•˜๋‚˜์˜ PostgreSQL์„ ๊ธฐ๋Šฅ๋ณ„ ํŒจํ‚ค์ง€๋กœ ๋‚˜๋ˆˆ ๋ชจ๋“ˆํ˜• ๋ชจ๋†€๋ฆฌ์Šค๋‹ค.

๋ฌด์—‡์„ ํ•™์Šตํ•  ์ˆ˜ ์žˆ๋‚˜

  • FastAPI์™€ ๊ธฐ๋ฐ˜ ํ”„๋ ˆ์ž„์›Œํฌ Starlette, ASGI ์„œ๋ฒ„ Uvicorn์˜ ์—ญํ•  ๊ตฌ๋ถ„
  • Pydantic ์š”์ฒญ๊ณผ ์‘๋‹ต ๊ฒ€์ฆ, ์ž๋™ OpenAPI ์ƒ์„ฑ
  • SQLAlchemy ๋น„๋™๊ธฐ ORM, asyncpg, Alembic ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • JWT Access Token, ํšŒ์ „ํ˜• ๋ถˆํˆฌ๋ช… Refresh Token, ๊ณ„์—ด ์žฌ์‚ฌ์šฉ ํƒ์ง€
  • USER, ADMIN ์—ญํ• ๊ณผ FastAPI ์˜์กด์„ฑ ๊ธฐ๋ฐ˜ ์ธ์ฆ, ์ธ๊ฐ€
  • ๊ณต๊ฐœ ์ƒํ’ˆ ์กฐํšŒ, ๊ด€๋ฆฌ์ž ์ „์šฉ ์ƒํ’ˆ ์ƒ์„ฑ, ์ˆ˜์ •, ์†Œํ”„ํŠธ ์‚ญ์ œ
  • ์„ฑ๊ณต ์‘๋‹ต ApiResponse[T], ์‹คํŒจ ์‘๋‹ต application/problem+json
  • ์š”์ฒญ ์ถ”์  ID์™€ structlog ๊ตฌ์กฐํ™” ๋กœ๊ทธ
  • ์‹ค์ œ PostgreSQL์„ ์‚ฌ์šฉํ•˜๋Š” ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์™€ HTTP E2E ํ…Œ์ŠคํŠธ
  • OpenAPI ๊ธฐ๋ฐ˜ ํ”„๋ก ํŠธ์—”๋“œ TypeScript ํƒ€์ž…๊ณผ ํด๋ผ์ด์–ธํŠธ ์ƒ์„ฑ

์ž์„ธํ•œ ๋‚ด๋ถ€ ์„ค๊ณ„๋Š” ์„œ๋ฒ„ ์•„ํ‚คํ…์ฒ˜, ์‹คํ–‰ ๋ชจ๋ธ์€ Node.js์™€ Python ๋Ÿฐํƒ€์ž„ ๋น„๊ต๋ฅผ ์ฐธ๊ณ ํ•œ๋‹ค.

๊ธฐ์ˆ  ์Šคํƒ

๋ฒ”์œ„ ์„ ํƒ
์–ธ์–ด์™€ ์„œ๋ฒ„ Python 3.13, FastAPI, Starlette, ASGI, Uvicorn
๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค PostgreSQL 18.4, SQLAlchemy 2, asyncpg, Alembic
๊ณ„์•ฝ๊ณผ ์„ค์ • Pydantic v2, pydantic-settings, OpenAPI
๋ณด์•ˆ PyJWT, pwdlib, Argon2, SHA-256 Refresh Token ํ•ด์‹œ
๊ด€์ฐฐ ๊ฐ€๋Šฅ์„ฑ structlog, x-request-id, RFC 9457 Problem Details
ํ’ˆ์งˆ pytest, HTTPX, Testcontainers, Ruff, mypy, markdown-it-py
๋„๊ตฌ uv, Docker, Docker Compose

์ •ํ™•ํ•œ ๊ณ ์ • ๋ฒ„์ „์€ pyproject.toml๊ณผ uv.lock์—์„œ ํ™•์ธํ•  ์ˆ˜ ์žˆ๋‹ค.

๊ฐ€์žฅ ๋น ๋ฅธ ์‹œ์ž‘, Docker Compose

1. ํ™˜๊ฒฝ ํŒŒ์ผ๊ณผ JWT Secret ์ค€๋น„

cp .env.example .env
python3 -c 'import secrets; print(secrets.token_urlsafe(48))'

๋‘ ๋ฒˆ์งธ ๋ช…๋ น์˜ ์ถœ๋ ฅ์œผ๋กœ .env์˜ JWT_SECRET ๊ฐ’์„ ๊ต์ฒดํ•œ๋‹ค. ์˜ˆ์ œ ๊ฐ’์„ ๊ฐœ๋ฐœ ๋˜๋Š” ์šด์˜ Secret์œผ๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š”๋‹ค. .env๋Š” Git์— ํฌํ•จ๋˜์ง€ ์•Š๋Š”๋‹ค.

Compose๋Š” ํ˜ธ์ŠคํŠธ ํ™˜๊ฒฝ ๋˜๋Š” .env์˜ JWT_SECRET์„ Compose Secret์œผ๋กœ ์ฝ๊ณ  ์ปจํ…Œ์ด๋„ˆ์˜ /run/secrets/jwt_secret์— ํŒŒ์ผ๋กœ ๋งˆ์šดํŠธํ•œ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์€ pydantic-settings๋ฅผ ํ†ตํ•ด ์ด ํŒŒ์ผ์„ ์ฝ๋Š”๋‹ค. Secret ๊ฐ’์€ ์ผ๋ฐ˜ ์„œ๋น„์Šค ํ™˜๊ฒฝ๋ณ€์ˆ˜์— ๋ณต์‚ฌ๋˜์ง€ ์•Š๋Š”๋‹ค.

2. ์ด๋ฏธ์ง€ ๋นŒ๋“œ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, API ์‹œ์ž‘

docker compose build api
docker compose up api

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ API ๋ช…๋ น์— ํฌํ•จ๋˜์ง€ ์•Š๊ณ  ๋ณ„๋„์˜ ์ผํšŒ์„ฑ migrate ์„œ๋น„์Šค๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. docker compose up api๋Š” PostgreSQL์ด ์ค€๋น„๋œ ๋’ค migrate๊ฐ€ ์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ์—๋งŒ API๋ฅผ ์‹œ์ž‘ํ•œ๋‹ค. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์— ์‹คํŒจํ•˜๋ฉด API๋„ ์‹œ์ž‘ํ•˜์ง€ ์•Š๋Š”๋‹ค. ์ƒˆ ์Šคํ‚ค๋งˆ๋งŒ ๋ช…์‹œ์ ์œผ๋กœ ์ ์šฉํ•˜๋ ค๋ฉด docker compose run --rm migrate๋ฅผ ์‹คํ–‰ํ•œ๋‹ค.

๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ์‹คํ–‰ํ•˜๋ ค๋ฉด ๋งˆ์ง€๋ง‰ ๋ช…๋ น์— -d๋ฅผ ์ถ”๊ฐ€ํ•œ๋‹ค.

docker compose up -d api

3. ์ƒํƒœ์™€ API ๋ฌธ์„œ ํ™•์ธ

FastAPI์˜ ๊ธฐ๋ณธ ๊ฒฝ๋กœ์ธ /docs, /redoc, /openapi.json์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•œ๋‹ค.

ํ˜ธ์ŠคํŠธ ํฌํŠธ ๋ณ€๊ฒฝ

๋กœ์ปฌ์˜ 8000 ๋˜๋Š” 5432 ํฌํŠธ๋ฅผ ์ด๋ฏธ ์‚ฌ์šฉ ์ค‘์ด๋ฉด .env์— ๋‹ค์Œ ๊ฐ’์„ ์ถ”๊ฐ€ํ•œ๋‹ค.

API_PORT=18000
POSTGRES_PORT=15432

์ดํ›„ API๋Š” http://localhost:18000์—์„œ ์ ‘๊ทผํ•œ๋‹ค. ์ปจํ…Œ์ด๋„ˆ ์‚ฌ์ด์—์„œ๋Š” ๊ณ„์† postgres:5432๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ COMPOSE_DATABASE_URL์„ ํ˜ธ์ŠคํŠธ ์ฃผ์†Œ๋กœ ๋ฐ”๊พธ์ง€ ์•Š๋Š”๋‹ค. ๊ฐœ๋ฐœ์šฉ PostgreSQL ํฌํŠธ๋Š” ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋ณ€๊ฒฝ๋œ ํฌํŠธ ๋ชจ๋‘ 127.0.0.1์—๋งŒ ๊ฒŒ์‹œ๋˜๋ฏ€๋กœ ๋‹ค๋ฅธ ํ˜ธ์ŠคํŠธ์—์„œ ์ง์ ‘ ์ ‘์†ํ•  ์ˆ˜ ์—†๋‹ค.

์ข…๋ฃŒ

docker compose down

PostgreSQL ๋ฐ์ดํ„ฐ๋Š” fastapi-server-test-postgres-data ๋ณผ๋ฅจ์— ๋‚จ๋Š”๋‹ค. ๋ณผ๋ฅจ ์‚ญ์ œ๋Š” ๋ฐ์ดํ„ฐ ์‚ญ์ œ ์ž‘์—…์ด๋ฏ€๋กœ ์ด ํ”„๋กœ์ ํŠธ์˜ ๊ธฐ๋ณธ ์ข…๋ฃŒ ๋ช…๋ น์— ํฌํ•จํ•˜์ง€ ์•Š๋Š”๋‹ค.

๋กœ์ปฌ ์‹คํ–‰, uv

Python 3.13, uv, ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ PostgreSQL์ด ํ•„์š”ํ•˜๋‹ค. PostgreSQL๋งŒ Compose๋กœ ์‹คํ–‰ํ•˜๊ณ  API๋Š” ํ˜ธ์ŠคํŠธ์—์„œ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

cp .env.example .env
docker compose up -d postgres
uv sync --extra dev
uv run alembic upgrade head
uv run uvicorn app.main:create_app --factory --reload

.env.example์˜ DATABASE_URL์€ ํ˜ธ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค๊ฐ€ localhost:5432์˜ PostgreSQL์— ์ ‘์†ํ•˜๋Š” ๊ฐ’์ด๋‹ค. .env์—๋Š” ์•ž ์ ˆ์—์„œ ์ƒ์„ฑํ•œ ์•ˆ์ „ํ•œ JWT_SECRET์„ ์„ค์ •ํ•œ๋‹ค.

uv run์€ ์ž ๊ธด ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์˜ ๋ช…๋ น์„ ์‹คํ–‰ํ•œ๋‹ค. ์„œ๋ฒ„๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ http://127.0.0.1:8000์—์„œ ์—ด๋ฆฐ๋‹ค. --reload๋Š” ๊ฐœ๋ฐœ ์ค‘ ํŒŒ์ผ ๋ณ€๊ฒฝ ๊ฐ์ง€์šฉ์ด๋ฉฐ ์šด์˜ ๋ฐฐํฌ ์„ค์ •์ด ์•„๋‹ˆ๋‹ค.

๋กœ์ปฌ ์‹คํ–‰, ํ‘œ์ค€ venv์™€ pip

uv๋ฅผ ์„ค์น˜ํ•˜์ง€ ์•Š์€ ํ™˜๊ฒฝ์—์„œ๋„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

python3.13 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
python -m pip install -e '.[dev]'
alembic upgrade head
uvicorn app.main:create_app --factory --reload

ํ”„๋กœ์ ํŠธ์˜ ์ง€์› ๋ฒ”์œ„๋Š” Python >=3.13,<3.14๋‹ค. ์‹œ์Šคํ…œ ๊ธฐ๋ณธ Python์ด ๋‹ค๋ฅธ ๋ฒ„์ „์ด๋ฉด Python 3.13 ์ธํ„ฐํ”„๋ฆฌํ„ฐ๋ฅผ ๋ช…์‹œํ•œ๋‹ค.

๊ด€๋ฆฌ์ž ๊ณ„์ • ๋งŒ๋“ค๊ธฐ

๊ณต๊ฐœ ๊ฐ€์ž…์€ ํ•ญ์ƒ USER๋ฅผ ๋งŒ๋“ ๋‹ค. ADMIN์€ CLI์—์„œ๋งŒ ์ƒ์„ฑํ•œ๋‹ค. ๋ช…๋ น์€ ํ„ฐ๋ฏธ๋„์—์„œ ๋น„๋ฐ€๋ฒˆํ˜ธ๋ฅผ ๋Œ€ํ™”์‹์œผ๋กœ ์š”์ฒญํ•˜๋ฉฐ ์ž…๋ ฅ๊ฐ’์„ ํ™”๋ฉด์— ํ‘œ์‹œํ•˜์ง€ ์•Š๋Š”๋‹ค.

uv ํ™˜๊ฒฝ:

uv run python -m app.cli create-admin \
  --email admin@example.com \
  --display-name Admin

Makefile ๋‹จ์ถ• ๋ช…๋ น:

make create-admin EMAIL=admin@example.com DISPLAY_NAME=Admin

Compose ํ™˜๊ฒฝ:

docker compose run --rm api uv run python -m app.cli create-admin \
  --email admin@example.com \
  --display-name Admin

๋จผ์ € ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์ด ์ ์šฉ๋˜์–ด ์žˆ์–ด์•ผ ํ•œ๋‹ค. ๊ธฐ์กด ์ด๋ฉ”์ผ๊ณผ ์ค‘๋ณต๋˜๊ฑฐ๋‚˜ ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ 12์ž ๋ฏธ๋งŒ์ด๋ฉด ์ƒ์„ฑ๋˜์ง€ ์•Š๋Š”๋‹ค.

API ๊ฒฝ๋กœ

๋ชจ๋“  ๊ธฐ๋Šฅ API๋Š” /api/v1 ์•„๋ž˜์— ์žˆ๋‹ค. ์ƒํ’ˆ ์ฝ๊ธฐ๋Š” ๊ณต๊ฐœ์ด๋ฉฐ, ์ƒํ’ˆ ์“ฐ๊ธฐ๋Š” Authorization: Bearer <access-token> ํ—ค๋”์™€ ADMIN ์—ญํ• ์ด ํ•„์š”ํ•˜๋‹ค.

๋ฉ”์„œ๋“œ ๊ฒฝ๋กœ ์ธ์ฆ ์—ญํ• 
POST /api/v1/auth/register ์—†์Œ USER ๊ฐ€์ž…
POST /api/v1/auth/login ์—†์Œ Access, Refresh Token ๋ฐœ๊ธ‰
POST /api/v1/auth/refresh Refresh Token ๋ณธ๋ฌธ Token ํšŒ์ „
POST /api/v1/auth/logout Refresh Token ๋ณธ๋ฌธ ํ•ด๋‹น Token ํ๊ธฐ
POST /api/v1/auth/logout-all Bearer ์‚ฌ์šฉ์ž Token ์ „์ฒด ํ๊ธฐ
GET /api/v1/users/me Bearer ๋‚ด ์ •๋ณด ์กฐํšŒ
PATCH /api/v1/users/me Bearer ํ‘œ์‹œ ์ด๋ฆ„ ๋ณ€๊ฒฝ
POST /api/v1/users/me/password Bearer ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ณ€๊ฒฝ
GET /api/v1/products ์—†์Œ ํ™œ์„ฑ ์ƒํ’ˆ ๋ชฉ๋ก
GET /api/v1/products/{product_id} ์—†์Œ ํ™œ์„ฑ ์ƒํ’ˆ ์ƒ์„ธ
POST /api/v1/products Bearer ADMIN ์ƒ์„ฑ
PATCH /api/v1/products/{product_id} Bearer ADMIN ์ˆ˜์ •
DELETE /api/v1/products/{product_id} Bearer ADMIN ์†Œํ”„ํŠธ ์‚ญ์ œ

์ƒํ’ˆ ๋ชฉ๋ก์€ page, page_size, query, sort, order ์ฟผ๋ฆฌ๋ฅผ ์ง€์›ํ•œ๋‹ค. page๋Š” ์ตœ๋Œ€ 10,000, page_size๋Š” ์ตœ๋Œ€ 100์ด๋ฉฐ ์ด ๋ฒ”์œ„๋ฅผ ๋„˜์œผ๋ฉด 422 ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค. sort๋Š” created_at, name, price_in_minor_units, sku ์ค‘ ํ•˜๋‚˜๋‹ค.

๊ฐ€์ž…๊ณผ ๋กœ๊ทธ์ธ ์˜ˆ์‹œ

curl -s http://localhost:8000/api/v1/auth/register \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","password":"correct-horse-123","display_name":"Learner"}'

curl -s http://localhost:8000/api/v1/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","password":"correct-horse-123"}'

์„ฑ๊ณต๊ณผ ์‹คํŒจ ๊ณ„์•ฝ

์„ฑ๊ณต ์‘๋‹ต์€ ๋ผ์šฐํŠธ๊ฐ€ ์„ ์–ธํ•œ ApiResponse[T] ํ˜•์‹์ด๋‹ค.

{
  "success": true,
  "data": {
    "status": "ok"
  },
  "meta": {
    "timestamp": "2026-07-24T00:00:00Z",
    "path": "/health/live",
    "trace_id": "6d55b497-e365-4a2f-9981-cb5d8ae81f52"
  }
}

์‹คํŒจ ์‘๋‹ต์€ RFC 9457 Problem Details์ด๋ฉฐ Content-Type์€ application/problem+json์ด๋‹ค. Pydantic ๊ฒ€์ฆ ์˜ค๋ฅ˜์—๋Š” ํ•„๋“œ๋ณ„ errors๊ฐ€ ํฌํ•จ๋œ๋‹ค. x-request-id ํ—ค๋”, ์‘๋‹ต์˜ trace_id, ์„œ๋ฒ„ ๋กœ๊ทธ์˜ ์ถ”์  ID๋ฅผ ์ด์šฉํ•ด ๊ฐ™์€ ์š”์ฒญ์„ ์ฐพ์„ ์ˆ˜ ์žˆ๋‹ค.

์ธ์ฆ ์„ค๊ณ„์™€ ๋กœ๊ทธ์•„์›ƒ ์ฃผ์˜์‚ฌํ•ญ

Access Token์€ ๊ธฐ๋ณธ 15๋ถ„ ์œ ํšจํ•œ JWT๋‹ค. Refresh Token์€ 256๋น„ํŠธ ๋ถˆํˆฌ๋ช… ๋‚œ์ˆ˜์ด๋ฉฐ ๊ธฐ๋ณธ 30์ผ ๋™์•ˆ ์œ ํšจํ•˜๋‹ค. ์„œ๋ฒ„๋Š” Refresh Token ์›๋ฌธ์ด ์•„๋‹ˆ๋ผ SHA-256 ํ•ด์‹œ๋งŒ ์ €์žฅํ•œ๋‹ค.

์ •์ƒ์ ์ธ refresh ์š”์ฒญ์€ ํ˜„์žฌ ํ† ํฐ ํ–‰์„ ์ž ๊ทธ๊ณ  ์‚ฌ์šฉ ์ฒ˜๋ฆฌํ•œ ๋’ค ๊ฐ™์€ token family์˜ ์ƒˆ Refresh Token์„ ๋ฐœ๊ธ‰ํ•œ๋‹ค. ์ด๋ฏธ ์‚ฌ์šฉํ•œ Refresh Token์ด ๋‹ค์‹œ ์ œ์ถœ๋˜๋ฉด ํƒˆ์ทจ ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋‹ค๊ณ  ๋ณด๊ณ  ๊ฐ™์€ family ์ „์ฒด๋ฅผ ํ๊ธฐํ•œ๋‹ค.

logout์€ ์ œ์ถœํ•œ Refresh Token์„ ํ๊ธฐํ•˜๊ณ , logout-all์€ ํ•ด๋‹น ์‚ฌ์šฉ์ž์˜ ๋ชจ๋“  Refresh Token์„ ํ๊ธฐํ•œ๋‹ค. ์ด ํ”„๋กœ์ ํŠธ์—๋Š” Access Token denylist๊ฐ€ ์—†๋‹ค. Access Token์€ ๋กœ๊ทธ์•„์›ƒ ํ›„์—๋„ ์ตœ๋Œ€ 15๋ถ„ ๋™์•ˆ ์œ ํšจํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ค‘์š”ํ•œ ์„œ๋น„์Šค์—์„œ๋Š” ๋” ์งง์€ ์ˆ˜๋ช…, ์„œ๋ฒ„ ์ธก ์„ธ์…˜, denylist ๊ฐ™์€ ์„ ํƒ์ง€๋ฅผ ์œ„ํ—˜ ๋ชจ๋ธ์— ๋งž๊ฒŒ ๊ฒ€ํ† ํ•œ๋‹ค.

Refresh Token ํšŒ์ „์€ ์—ฌ๋Ÿฌ ๋ธŒ๋ผ์šฐ์ € ํƒญ์ด๋‚˜ ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ฐ™์€ ํ† ํฐ์œผ๋กœ ๋™์‹œ์— refreshํ•  ๋•Œ ํ•œ ์š”์ฒญ๋งŒ ์„ฑ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ค. ํด๋ผ์ด์–ธํŠธ๋Š” ํ•œ ๊ณ„์ •์˜ refresh ์š”์ฒญ์„ ์ง๋ ฌํ™”ํ•˜๊ณ  ์ƒˆ ํ† ํฐ์„ ์›์ž์ ์œผ๋กœ ์ €์žฅํ•ด์•ผ ํ•œ๋‹ค. ์‚ฌ์šฉ๋œ ์ด์ „ ํ† ํฐ์„ ์žฌ์ „์†กํ•˜๋ฉด ๊ณ„์—ด ์žฌ์‚ฌ์šฉ ํƒ์ง€๊ฐ€ ์ž‘๋™ํ•œ๋‹ค.

ํ”„๋ก ํŠธ์—”๋“œ์—์„œ OpenAPI ์‚ฌ์šฉํ•˜๊ธฐ

์‹คํ–‰ ์ค‘์ธ ์„œ๋ฒ„์˜ /openapi.json์„ ์ง์ ‘ ๋ณผ ์ˆ˜ ์žˆ๊ณ , ์ €์žฅ์†Œ์˜ ๊ฒฐ์ •์ ์ธ ๊ณ„์•ฝ ํŒŒ์ผ๋„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

uv run python scripts/export_openapi.py
git diff -- openapi/openapi.json

์ถœ๋ ฅ์€ openapi/openapi.json์ด๋‹ค. API ๋ณ€๊ฒฝ๊ณผ ํ•จ๊ป˜ ์ด ํŒŒ์ผ์˜ diff๋ฅผ ๊ฒ€ํ† ํ•˜๊ณ  ์ปค๋ฐ‹ํ•œ๋‹ค.

openapi-typescript

๋‹ค์Œ ๋ช…๋ น์€ ์ด Python ์ €์žฅ์†Œ๊ฐ€ ์•„๋‹ˆ๋ผ ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ์—์„œ ์‹คํ–‰ํ•œ๋‹ค. ํ”„๋ก ํŠธ์—”๋“œ๊ฐ€ ./openapi/openapi.json์— ๋‚ด๋ณด๋‚ธ ๊ณ„์•ฝ ํŒŒ์ผ์„ ๋ณต์‚ฌํ•˜๊ฑฐ๋‚˜ CI์—์„œ ๊ฐ€์ ธ์™”๋‹ค๊ณ  ๊ฐ€์ •ํ•œ๋‹ค. Node.js ํŒจํ‚ค์ง€๋ฅผ ์ด Python ์ €์žฅ์†Œ์— ์ถ”๊ฐ€ํ•  ํ•„์š”๊ฐ€ ์—†๋‹ค.

npx openapi-typescript ./openapi/openapi.json -o src/api/schema.d.ts

์ƒ์„ฑ๋œ ํƒ€์ž…์€ ํ”„๋ก ํŠธ์—”๋“œ HTTP ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๊ฒฝ๋กœ, ์š”์ฒญ ๋ณธ๋ฌธ, ์„ฑ๊ณต ์‘๋‹ต, Problem Details ํƒ€์ž…์„ ๊ณต์œ ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

Orval

ํ”„๋ก ํŠธ์—”๋“œ์˜ orval.config.ts ์˜ˆ์‹œ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™๋‹ค.

import { defineConfig } from "orval";

export default defineConfig({
  api: {
    input: "./openapi/openapi.json",
    output: {
      target: "./src/api/generated.ts",
      client: "fetch",
      clean: true,
    },
  },
});

ํ”„๋ก ํŠธ์—”๋“œ ํ”„๋กœ์ ํŠธ์—์„œ Orval์„ ๊ฐœ๋ฐœ ์˜์กด์„ฑ์œผ๋กœ ์„ค์น˜ํ•œ ๋’ค ์‹คํ–‰ํ•œ๋‹ค.

npx orval --config ./orval.config.ts

์ƒ์„ฑ ์ฝ”๋“œ๊ฐ€ ๋ชจ๋“  ๋Ÿฐํƒ€์ž„ ์ •์ฑ…์„ ๋Œ€์‹ ํ•˜์ง€๋Š” ์•Š๋Š”๋‹ค. ์ธ์ฆ ํ† ํฐ ์ €์žฅ ๋ฐฉ์‹, refresh ์ง๋ ฌํ™”, ์ œํ•œ ์‹œ๊ฐ„, ์žฌ์‹œ๋„, ์˜ค๋ฅ˜ ํ™”๋ฉด ์ •์ฑ…์€ ํ”„๋ก ํŠธ์—”๋“œ์—์„œ ๋ณ„๋„๋กœ ์„ค๊ณ„ํ•œ๋‹ค.

Node.js ๊ฐœ๋ฐœ์ž๋ฅผ ์œ„ํ•œ ํ•ต์‹ฌ ๋Œ€์‘ํ‘œ

V8์€ JavaScript ์‹คํ–‰ ์—”์ง„์ด๋‹ค. V8 ์ž์ฒด๋ฅผ nonblocking์ด๋ผ๊ณ  ๋ถ€๋ฅด๋Š” ๊ฒƒ์€ ๋ถ€์ •ํ™•ํ•˜๋‹ค. Node.js๋Š” V8, libuv, event loop, ์šด์˜์ฒด์ œ I/O polling, ์ œํ•œ๋œ worker pool์„ ๊ฒฐํ•ฉํ•œ๋‹ค.

Python์€ ๋ณธ์งˆ์ ์œผ๋กœ multithread ์›น ์„œ๋ฒ„๊ฐ€ ์•„๋‹ˆ๋‹ค. ์ด ํ”„๋กœ์ ํŠธ์˜ CPython ๊ธฐ๋ณธ ๋นŒ๋“œ๋Š” GIL์„ ์‚ฌ์šฉํ•œ๋‹ค. async def๋Š” asyncio event loop, ๋™๊ธฐ ๊ฒฝ๋กœ๋Š” ํ•„์š”ํ•  ๋•Œ thread pool, ์—ฌ๋Ÿฌ Uvicorn ์ธ์Šคํ„ด์Šค๋Š” ๋ณ„๋„ worker process์—์„œ ์‹คํ–‰๋œ๋‹ค.

Node.js์™€ NestJS Python๊ณผ FastAPI
Promise Coroutine ๋˜๋Š” Task
Promise.all asyncio.gather
setTimeout asyncio.sleep
Node.js event loop asyncio event loop
Express ๋˜๋Š” NestJS FastAPI
Node HTTP server Uvicorn ASGI server
cluster worker Uvicorn worker process
BullMQ Celery, Dramatiq, ARQ
Zod DTO Pydantic model
Prisma SQLAlchemy์™€ Alembic
Guard dependency
Exception Filter exception handler

์ž์„ธํ•œ ์ฐจ์ด๋Š” ๋Ÿฐํƒ€์ž„ ๋น„๊ต ๋ฌธ์„œ์— ์„ค๋ช…ํ–ˆ๋‹ค.

NestJS ๋ชจ๋…ธ๋ ˆํฌ์™€ ์ด ์ €์žฅ์†Œ

nestjs-monorepo-test๋Š” apps/์˜ ์—ฌ๋Ÿฌ ์‹คํ–‰ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜๊ณผ libs/์˜ ๊ณต์œ  ํŒจํ‚ค์ง€๋ฅผ ํฌํ•จํ•œ๋‹ค. ์ด ์ €์žฅ์†Œ๋Š” ํ•˜๋‚˜์˜ ๋ฐฐํฌ ๋‹จ์œ„ ์•ˆ์—์„œ app/modules/auth, users, products๋ฅผ ๋‚˜๋ˆˆ๋‹ค.

NestJS ๊ด€์  ์ด ์ €์žฅ์†Œ
Module ๊ธฐ๋Šฅ ํŒจํ‚ค์ง€์™€ API router ์กฐ๋ฆฝ
Controller FastAPI APIRouter ๊ฒฝ๋กœ ํ•จ์ˆ˜
Provider์™€ Service ๋ช…์‹œ์ ์œผ๋กœ ์ƒ์„ฑํ•œ service์™€ repository
Zod์™€ DTO Pydantic ์š”์ฒญ๊ณผ ์‘๋‹ต model
Prisma SQLAlchemy ORM
Prisma Migrate Alembic
Guard FastAPI dependency
Exception Filter FastAPI exception handler
Interceptor ์‘๋‹ต ๋ณ€ํ™˜ ๋ผ์šฐํŠธ์˜ ๋ช…์‹œ์  response_model
Swagger module FastAPI ์ž๋™ OpenAPI

๋‘ ๊ตฌ์กฐ์˜ ์ด๋ฆ„์„ ๊ธฐ๊ณ„์ ์œผ๋กœ ์ผ๋Œ€์ผ ๋ณต์‚ฌํ•˜๊ธฐ๋ณด๋‹ค ์ฑ…์ž„ ๊ฒฝ๊ณ„๋ฅผ ๋น„๊ตํ•œ๋‹ค.

FastAPI, Django, Flask๋ฅผ ๋น„๊ตํ•  ๋•Œ

Django์™€ Flask๋„ ์›น๊ณผ ๋ชจ๋ฐ”์ผ ์•ฑ์ด ํ•จ๊ป˜ ์“ฐ๋Š” ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ API๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค. Django๋‚˜ Flask๋กœ ๋งŒ๋“  API๋ฅผ ๋ชจ๋ฐ”์ผ ์•ฑ ๋•Œ๋ฌธ์— ๋”ฐ๋กœ ๋‹ค์‹œ ๊ฐœ๋ฐœํ•ด์•ผ ํ•œ๋‹ค๋Š” ์„ค๋ช…์€ ์ •ํ™•ํ•˜์ง€ ์•Š๋‹ค. Django REST Framework ๊ฐ™์€ ์„ ํƒ์ง€๋„ ์žˆ๋‹ค.

FastAPI๋Š” ํƒ€์ž… ํžŒํŠธ์™€ Pydantic ๊ฒ€์ฆ, OpenAPI, Swagger UI, ReDoc, ASGI ์ฒ˜๋ฆฌ๊ฐ€ API ๊ฐœ๋ฐœ์˜ ๊ธฐ๋ณธ ํ๋ฆ„์— ๊ธด๋ฐ€ํ•˜๊ฒŒ ์—ฐ๊ฒฐ๋œ ์ ์ด ํŠน์ง•์ด๋‹ค. Django๋Š” ORM, ๊ด€๋ฆฌ์ž, ํ…œํ”Œ๋ฆฟ, ์ธ์ฆ์„ ํฌํ•จํ•œ ํฐ ์›น ํ”„๋ ˆ์ž„์›Œํฌ์ด๊ณ  Flask๋Š” ์ž‘์€ ์ฝ”์–ด์— ํ™•์žฅ์„ ์กฐํ•ฉํ•œ๋‹ค. ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ํŒ€ ๊ฒฝํ—˜์— ๋งž์ถฐ ์„ ํƒํ•œ๋‹ค.

FastAPI๊ฐ€ ํšจ์œจ์ ์ธ ๋น„๋™๊ธฐ API๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ๊ณผ ๋ชจ๋“  ์ž‘์—…์—์„œ Node.js ๋˜๋Š” Go์™€ ๊ฐ™์€ ์„ฑ๋Šฅ์„ ๋ณด์žฅํ•œ๋‹ค๋Š” ๊ฒƒ์€ ๋‹ค๋ฅด๋‹ค. ์‹ค์ œ ์ฒ˜๋ฆฌ๋Ÿ‰์€ ์ž‘์—… ํŠน์„ฑ, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ฟผ๋ฆฌ์™€ ์ž ๊ธˆ, ์ง๋ ฌํ™”, worker ์ˆ˜, ์—ฐ๊ฒฐ ํ’€, ๋ฐฐํฌ ์„ค์ •์— ๋”ฐ๋ผ ๋‹ฌ๋ผ์ง„๋‹ค. ์‹ค์ œ ์„œ๋น„์Šค ํ˜•ํƒœ๋กœ ์ธก์ •ํ•œ๋‹ค.

๊ฐœ๋ฐœ ๋ช…๋ น

uv sync --extra dev
uv run pytest
uv run ruff check .
uv run ruff format --check .
uv run mypy app tests scripts
uv run --extra dev python scripts/check_docs.py
uv run python scripts/export_openapi.py

Makefile์„ ์‚ฌ์šฉํ•˜๋ฉด ๊ฐ™์€ ์ž‘์—…์„ ์‹คํ–‰ํ•  ์ˆ˜ ์žˆ๋‹ค.

make install
make test
make lint
make format
make typecheck
make docs-check
make openapi
make verify

scripts/check_docs.py๋Š” ๋ชจ๋“  Markdown์„ UTF-8๋กœ ์ฝ๊ณ  ๊ธˆ์ง€๋œ ๊ฐ€์šด๋Žƒ์  ๋ฌธ์ž์™€ ๊นจ์ง„ ๋กœ์ปฌ Markdown ๋งํฌ๋ฅผ ๊ฒ€์‚ฌํ•œ๋‹ค. CommonMark ๋ฌธ๋ฒ• ํ•ด์„์—๋Š” ๊ฐœ๋ฐœ ์˜์กด์„ฑ์ธ markdown-it-py๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์„ uv sync --extra dev ๋˜๋Š” pip install -e '.[dev]'๋กœ ์„ค์น˜ํ•œ ๋’ค ์‹คํ–‰ํ•œ๋‹ค. make docs-check๋Š” ํ•„์š”ํ•œ dev extra๋ฅผ ๋ช…์‹œํ•ด ๊ฐ™์€ ๊ฒ€์‚ฌ๋ฅผ ์ˆ˜ํ–‰ํ•œ๋‹ค.

make verify๋Š” ์ž ๊ธˆ ํŒŒ์ผ, Ruff ๊ฒ€์‚ฌ์™€ ํ˜•์‹, mypy, ๋ฌธ์„œ, ์ „์ฒด pytest, OpenAPI ์Šค๋ƒ…์ƒท์„ ํ•œ ๋ฒˆ์— ๊ฒ€์ฆํ•œ๋‹ค. OpenAPI ๊ฒ€์ฆ์€ ์ž„์‹œ ํŒŒ์ผ๋กœ ๋ช…์„ธ๋ฅผ ๋‚ด๋ณด๋‚ธ ๋’ค ์ถ”์  ์ค‘์ธ openapi/openapi.json๊ณผ ๋ฐ”์ดํŠธ ๋‹จ์œ„๋กœ ๋น„๊ตํ•œ๋‹ค. ๋”ฐ๋ผ์„œ ๊ฒ€์ฆ ๊ณผ์ •์ด ๊ธฐ์กด ์Šค๋ƒ…์ƒท์„ ๋ฎ์–ด์จ ๋ณ€๊ฒฝ์„ ์ˆจ๊ธฐ์ง€ ์•Š๋Š”๋‹ค.

์ง€์†์  ํ†ตํ•ฉ

GitHub Actions๋Š” Python 3.13๊ณผ ๊ณ ์ •๋œ uv 0.11.31 ํ™˜๊ฒฝ์—์„œ ํ’ˆ์งˆ ๊ฒ€์‚ฌ, ๋‹จ์œ„ ํ…Œ์ŠคํŠธ, PostgreSQL 18.4 ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ, HTTP E2E ํ…Œ์ŠคํŠธ, Docker ์ด๋ฏธ์ง€ ๋นŒ๋“œ, OpenAPI drift ๊ฒ€์‚ฌ๋ฅผ ๊ฐ๊ฐ ์‹คํ–‰ํ•œ๋‹ค. ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์™€ E2E ํ…Œ์ŠคํŠธ๋Š” CI ์„œ๋น„์Šค PostgreSQL์— Alembic ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๋จผ์ € ์ ์šฉํ•œ๋‹ค. ํ…Œ์ŠคํŠธ ๋ณธ์ฒด์˜ Testcontainers PostgreSQL์€ ์ž„์˜์˜ ํ˜ธ์ŠคํŠธ ํฌํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ CI ์„œ๋น„์Šค์˜ 5432 ํฌํŠธ์™€ ์ถฉ๋Œํ•˜์ง€ ์•Š๋Š”๋‹ค.

ํ…Œ์ŠคํŠธ ๊ตฌ์„ฑ

  • tests/unit: ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์—†์ด ๋ณด์•ˆ ํ•จ์ˆ˜์™€ ์„œ๋น„์Šค ๊ทœ์น™ ๊ฒ€์ฆ
  • tests/integration: Testcontainers์˜ ์‹ค์ œ PostgreSQL๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜, ์ œ์•ฝ, ์ €์žฅ์†Œ, Refresh Token ํšŒ์ „ ๊ฒ€์ฆ
  • tests/e2e: ASGI HTTP ์š”์ฒญ์œผ๋กœ ์ธ์ฆ, ์‚ฌ์šฉ์ž, ์ƒํ’ˆ, OpenAPI ๊ณ„์•ฝ ๊ฒ€์ฆ
  • tests/operations: Docker Compose์™€ ์ด๋ฏธ์ง€ ์šด์˜ ๊ณ„์•ฝ ๊ฒ€์ฆ
  • tests/documentation: ํ•™์Šต ๋ฌธ์„œ์˜ ํ•„์ˆ˜ ๋‚ด์šฉ, ๋ช…๋ น, ๋งํฌ ๊ฒ€์ฆ

ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ์—๋Š” ์‹คํ–‰ ๊ฐ€๋Šฅํ•œ Docker๊ฐ€ ํ•„์š”ํ•˜๋‹ค.

๊ณต์‹ ์ฐธ๊ณ  ์ž๋ฃŒ

About

FastAPI and PostgreSQL learning server with typed REST contracts

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages