FastAPI ๊ธฐ๋ฐ fds_boilerplate
https://github.com/ch0992/fds_boilerplate/
๐ 1. fds_boilerplate ์ค๊ณ ๋ฐฐ๊ฒฝ๊ณผ ๊ตฌ์กฐ ๋ชฉํ
โ ์ ์ด ํ๋ก์ ํธ๋ฅผ ๋ง๋ค์์๊น?
FastAPI๋ ๊ฒฝ๋ ์น ํ๋ ์์ํฌ์์๋ ๋ถ๊ตฌํ๊ณ ๋ค์๊ณผ ๊ฐ์ ์ฅ์ ์ด ์์ด, ์ต๊ทผ ๋ง์ ํ๋ก์ ํธ์์ ํ์ฉ๋๊ณ ์์ต๋๋ค:
- Python ๊ธฐ๋ฐ์ ๊ฐ๋จํ ๋ฌธ๋ฒ
- ๋น๋๊ธฐ ์ฒ๋ฆฌ์ ๊ฐํ๊ณ
- ์๋์ผ๋ก Swagger ๋ฌธ์ํ๊ฐ ์ ๊ณต๋จ
ํ์ง๋ง ํ ๋จ์ ๊ฐ๋ฐ์ ํ๋ค ๋ณด๋ฉด ๋ค์๊ณผ ๊ฐ์ ๋ถํธ์ด ์๊น๋๋ค:
- ๋ชจ๋ API๊ฐ ํ ๊ณณ์ ์ฝํ ์์ด ๋๋ฉ์ธ ๋ถ๋ฆฌ ์ด๋ ค์
- ํ๋ก์ ํธ๋ฅผ ์จ๋ณด๋ฉํ๋ ค๋ฉด ๊ตฌ์กฐ๋ถํฐ ํ์ ํด์ผ ํ๋ ๋น์ฉ์ด ํผ
- ์ค์ ์ด์์ ๋ง๋ ๋ก๊น , ์ธ์ฆ, ๋๋ฒ๊น ํ๊ฒฝ์ด ์์
๋ฐ๋ผ์ ๋ค์๊ณผ ๊ฐ์ ๊ธฐ์ค์ ๊ฐ๊ณ ํ ํ๋ฆฟ์ ๊ตฌ์ฑํ๊ฒ ๋์์ต๋๋ค.
๐งฉ ์ค๊ณ ๋ชฉํ (๊ฐ๋ฐ/์ด์ ๊ด์ )
์์ญ | ๋ชฉํ |
---|---|
๊ฐ๋ฐ ๊ตฌ์กฐ | ๊ณตํต ๋ชจ๋๊ณผ ๋๋ฉ์ธ ๋ถ๋ฆฌ๋ฅผ ํตํด ๋ชจ๋๋ฆฌ์์ฒ๋ผ ๊ฐ๋ฐ, ๋ง์ดํฌ๋ก์๋น์ค์ฒ๋ผ ๋ฐฐํฌ |
์คํ ํ๊ฒฝ | .env ๊ธฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ์ผ๋ก ๊ฐ๋ฐ/์ด์ ๋ถ๋ฆฌ, VSCode ๊ธฐ๋ฐ ๋๋ฒ๊น
์นํ์ ๊ตฌ์ฑ |
ํ ํ์ฅ์ฑ | ์ด๋ณด์๋ ๋น ๋ฅด๊ฒ ์ดํดํ๊ณ ๋ณต๋ถํด์ ์ธ ์ ์๋๋ก ํ์ค ํ๋ฆ ๊ณ ์ |
๐งญ ์ ์ฒด ์์คํ ๊ตฌ์กฐ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ
์ฌ์ฉ์ ์์ฒญ
↓
API Gateway (์ธ์ฆ/๋ผ์ฐํ
)
↓
๋๋ฉ์ธ ์๋น์ค ํธ์ถ (file, data ๋ฑ)
↓
Kafka ์ ์ก / S3 ์
๋ก๋
↓
์๋ต ๋ฐํ + ๋ก๊ทธ ์ ์ก
- ํ๋์ ํ๋ก์ ํธ ์์์ ์ฌ๋ฌ ๋๋ฉ์ธ์ ๋ ๋ฆฝ ์คํ ๊ฐ๋ฅํ๊ฒ ๋ง๋ค์๊ณ
- ๊ฐ ๋๋ฉ์ธ์ FastAPI ์ฑ์ผ๋ก ๊ตฌ์ฑ๋์ด ์์ด ๋จ๋ ํ ์คํธ์ ๊ฐ๋ฐ์ด ๊ฐ๋ฅํฉ๋๋ค.
๐ 2. ํ๋ก์ ํธ ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ์ ์๋น์ค ๋ถ๋ฆฌ ์ ๋ต
๐ ์ ์ฒด ๋๋ ํ ๋ฆฌ ๊ตฌ์กฐ ๊ฐ์
๐ฆ fds_boilerplate
โโโ app
โ โโโ common
โ โโโ domains
โ โโโ gateway
โ โโโ file
โ โโโ data
โ โโโ log
โโโ infra
โโโ scripts
โโโ tests
โโโ .env
โโโ docker-compose.yaml
โโโ requirements.txt
โ ํต์ฌ ๊ตฌ์ฑ ์์ ์ค๋ช
๋๋ ํ ๋ฆฌ | ์ค๋ช |
---|---|
app/common |
์ค์ , ์์ธ ์ฒ๋ฆฌ, ์ธ์ฆ, ๊ณตํต ๋ชจ๋ธ ๋ฑ ๋ชจ๋ ๋๋ฉ์ธ์์ ๊ณตํต ์ฌ์ฉํ๋ ์ฝ๋๋ค |
app/domains/{๋๋ฉ์ธ} |
gateway, file, data, log ๋ฑ ๋๋ฉ์ธ๋ณ ๋ ๋ฆฝ ์๋น์ค๋ก ๋ถ๋ฆฌ๋ FastAPI ์ ํ๋ฆฌ์ผ์ด์ |
infra |
Helm chart, Kubernetes manifest ๋ฑ ์ค์ ๋ฐฐํฌ ์ธํ๋ผ ๊ตฌ์ฑ ๊ด๋ฆฌ ์์ญ |
scripts |
๊ฐ๋ฐ ๋ฐ ์คํ์ ๋๋ ๋ก์ปฌ ์คํ ์คํฌ๋ฆฝํธ๋ค ํฌํจ (์: run_all_fastapi_local.sh ) |
tests |
ํ ์คํธ ์ฝ๋ ๋ชจ์ (๊ธฐ๋ณธ ๊ตฌ์กฐ๋ง ์ ๊ณต๋์ด ์์ผ๋ฉฐ ๋๋ฉ์ธ๋ณ๋ก ์์ฑ ํ์) |
.env |
ํ๊ฒฝ๋ณ ์คํ ๋ณ์๋ฅผ ์ ์ํ๋ ํ์ผ๋ก, ์๋น์ค ๊ฐ ํต์ผ๋ ํฌํธ ๋ฐ ์ค์ ๊ณต์ |
๐ฆ ๋๋ฉ์ธ ๊ตฌ์ฑ์ ์ฒ ํ: "๋ชจ๋๋ฆฌ์์ฒ๋ผ ๊ฐ๋ฐ, MSA์ฒ๋ผ ๋ฐฐํฌ"
- ํ๋์ ์ฝ๋๋ฒ ์ด์ค ์์ ์ฌ๋ฌ FastAPI ์ฑ์ ๋๋,
- ์ค์ ์คํ์
gateway
,file
,data
,log
๊ฐ ๊ฐ๊ธฐ ๋ค๋ฅธ ํฌํธ๋ก ๋ถ๋ฆฌ ์คํ๋๋๋ก ์ค๊ณํ์ต๋๋ค.
์์: ๋๋ฉ์ธ๋ณ ํฌํธ ๊ตฌ์ฑ
gateway → 8000
file → 8001
data → 8002
log → 8003
๊ฐ ๋๋ฉ์ธ์ main.py
๋ฅผ ๊ฐ์ง๊ณ ์์ผ๋ฉฐ,
VSCode์์ launch.json
์ ํตํด ๋จ๋
์คํํ๊ฑฐ๋ ๋ชจ๋ ์ฑ์ ๋ณตํฉ ์คํํ ์ ์์ต๋๋ค.
๐ 3. .env ๊ธฐ๋ฐ ๋ก์ปฌ ์คํ ํ๊ฒฝ ๊ตฌ์ฑ ๋ฐ ์ ์ฒด ์๋น์ค ๊ธฐ๋
โ ๋ชฉํ
.env
ํ์ผ์ ํตํด ์คํ ์ค์ ์ ํต์ผ- ๊ฐ๋ฐ์๊ฐ ๋จ์ผ ๋ช ๋ น์ด๋ก ์ ์ฒด FastAPI ์ฑ์ ์คํํ ์ ์๋๋ก ๊ตฌ์ฑ
- ํฌํธ ์ถฉ๋ ๋ฐฉ์ง, ํ๊ฒฝ๋ณ ์ค์ ๋ถ๋ฆฌ ๋ฑ์ ๊ธฐ๋ณธ ์ ๊ณต
๐ .env ํ์ผ ๊ตฌ์กฐ
# ๊ณตํต ์คํ ๋ณ์
ENV=local
# ๊ฐ ์๋น์ค ํฌํธ
GATEWAY_PORT=8000
FILE_PORT=8001
DATA_PORT=8002
LOG_PORT=8003
# Kafka, MinIO ๋ฑ ์ธ๋ถ ์์กด ๊ตฌ์ฑ
KAFKA_HOST=localhost:9092
MINIO_ENDPOINT=http://localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
- ๋ชจ๋ ์๋น์ค๋ ์ด
.env
ํ์ผ์ ๊ธฐ์ค์ผ๋ก ํ๊ฒฝ์ ๊ฐ์ ธ๊ฐ๋๋ก ๋์ด ์์ต๋๋ค. app/common/config.py
ํ์ผ์ด ์ด๋ฅผpydantic.BaseSettings
๊ธฐ๋ฐ์ผ๋ก ํ์ฑํฉ๋๋ค.
โ๏ธ config.py ์์ (๊ณตํต ํ๊ฒฝ ๋ถ๋ฌ์ค๊ธฐ)
from pydantic import BaseSettings
class Settings(BaseSettings):
env: str = "local"
gateway_port: int = 8000
file_port: int = 8001
data_port: int = 8002
log_port: int = 8003
kafka_host: str = "localhost:9092"
minio_endpoint: str = "http://localhost:9000"
minio_access_key: str = "minioadmin"
minio_secret_key: str = "minioadmin"
class Config:
env_file = ".env"
settings = Settings()
๋ชจ๋ ๋๋ฉ์ธ์์ from app.common.config import settings
๋ง ํ๋ฉด ๋์ผํ ํ๊ฒฝ์ ๊ณต์ ํฉ๋๋ค.
โถ๏ธ ์ ์ฒด FastAPI ์ฑ ์คํ ๋ฐฉ๋ฒ
/scripts
๋๋ ํ ๋ฆฌ ๋ด run_all_fastapi_local.sh
์คํ ์คํฌ๋ฆฝํธ๋ฅผ ์ฌ์ฉํฉ๋๋ค.
์์
$ cd scripts
$ sh run_all_fastapi_local.sh
- ๊ฐ ์๋น์ค๊ฐ ๋ฐฑ๊ทธ๋ผ์ด๋๋ก ์คํ๋๋ฉฐ
scripts/*.log
ํ์ผ๋ก ๋ก๊ทธ๊ฐ ์ ์ฅ๋ฉ๋๋ค. - ์คํ์ด ์๋ฃ๋๋ฉด ์๋์ ๊ฐ์ด ํ์ธ ๊ฐ๋ฅํฉ๋๋ค:
$ lsof -i :8000
$ lsof -i :8001
...
๐งช ๋จ์ผ ์๋น์ค๋ง ์คํํ๊ณ ์ถ๋ค๋ฉด?
๊ฐ ๋๋ฉ์ธ์๋ main.py
๊ฐ ์กด์ฌํ๋ฏ๋ก ์๋์ฒ๋ผ ์คํ ๊ฐ๋ฅํฉ๋๋ค:
$ uvicorn app.domains.file.main:app --reload --port 8001
ํน์ launch.json
์ ์ค์ ๋ ํ๊ฒฝ์ ํตํด VSCode ๋๋ฒ๊น
๋ ๊ฐ๋ฅํฉ๋๋ค (5ํธ์์ ์์ธ ์ค๋ช
).
๐ ์ ๋ฆฌ
.env
๊ธฐ๋ฐ ํ๊ฒฝ ๊ตฌ์ฑ์ ํตํด ์๋น์ค ๊ฐ ํต์ผ๋ ์ค์ ์ ๊ณต- FastAPI ์ฑ์ ํ ๋ฒ์ ์คํํ๊ฑฐ๋ ๊ฐ๋ณ ์คํ ๊ฐ๋ฅ
- ๊ฐ ๋๋ฉ์ธ์ ๊ณ ์ ํฌํธ๋ฅผ ์ฌ์ฉํ์ฌ ๋ ๋ฆฝ ๋์
config.py
๊ธฐ๋ฐ์ผ๋ก ๋ชจ๋ ์๋น์ค์์ ์ค์ ๊ฐ ๊ณต์ ๊ฐ๋ฅ
๐ 4. Swagger UI ๊ตฌ์ฑ๊ณผ ๊ณตํต FastAPI ์ธ์คํด์ค ์ ์ฉ ๋ฐฉ๋ฒ
โ ๋ชฉํ
- ๋ชจ๋ ๋๋ฉ์ธ API ์คํ์ gateway์์ ํตํฉํ์ฌ Swagger ๋ฌธ์๋ก ์๋ ์ ๊ณต
- ๊ฐ ๋๋ฉ์ธ์ ์์ฒด์ ์ผ๋ก FastAPI ์ธ์คํด์ค๋ฅผ ๊ตฌ์ฑํ์ง๋ง, Swagger ๋ฌธ์๋ gateway์์๋ง ์ ๊ทผ ๊ฐ๋ฅ
๐งฑ ์ค์ FastAPI ์ฑ ๊ตฌ์ฑ ๋ฐฉ์
๊ฐ ๋๋ฉ์ธ(file, data, log ๋ฑ)์ ๋๋ฉ์ธ๋ณ main.py
์์ FastAPI ์ธ์คํด์ค๋ฅผ ์ง์ ์์ฑํ๊ณ ,
๋ผ์ฐํฐ·๋ฏธ๋ค์จ์ด·ํธ๋ ์ด์ฑ·์์ธ์ฒ๋ฆฌ ๋ฑ์ ํตํฉ ์ ์ฉํฉ๋๋ค.
# ์์: app/domains/file/main.py
from fastapi import FastAPI
from dotenv import load_dotenv
from app.domains.file.api.routes import router as file_router
from app.common.config import settings
from app.domains.log.services.common.tracing import init_tracer
from app.domains.log.services.common.sentry import init_sentry
from app.domains.log.services.common.middleware import install_exception_handlers, TraceLoggingMiddleware
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
import os, logging
# ๋ก๊ทธ ๋ฐ .env ์ค์ ์๋ต
app = FastAPI(
title="File Service",
description="File upload/download microservice",
docs_url=None, # ๋๋ฉ์ธ ์๋น์ค์์๋ Swagger UI ๋นํ์ฑํ
redoc_url=None,
openapi_url=None
)
# Tracing, Sentry, ๋ฏธ๋ค์จ์ด, ๋ผ์ฐํฐ ๋ฑ๋ก ์๋ต
- ๊ฐ ๋๋ฉ์ธ ์๋น์ค๋
docs_url
,redoc_url
,openapi_url
์None
์ผ๋ก ์ค์ ํ์ฌ ์์ฒด Swagger UI๋ฅผ ๋นํ์ฑํํฉ๋๋ค.
๐๏ธ Gateway์์ Swagger ๋ฌธ์ ํตํฉ ์ ๊ณต
gateway ์๋น์ค(app/domains/gateway/main.py
)์์ ๋ชจ๋ ๋๋ฉ์ธ์ ๋ผ์ฐํฐ๋ฅผ ํตํฉ ๋ฑ๋กํ๊ณ ,
Swagger UI(/gateway/docs
)๋ฅผ ๋จ์ผ ์๋ํฌ์ธํธ๋ก ์ ๊ณตํฉ๋๋ค.
gateway์ FastAPI ์ธ์คํด์ค๋ Swagger UI๋ฅผ ํ์ฑํํ๊ณ ,
๊ฐ ๋๋ฉ์ธ๋ณ ๋ผ์ฐํฐ๋ฅผ include_router()
๋ก ํตํฉํฉ๋๋ค.
# ์์: app/domains/gateway/main.py
from fastapi import FastAPI
from app.domains.file.api.routes import router as file_router
from app.domains.data.api.routes import router as data_router
# ... ๊ธฐํ ๋๋ฉ์ธ ๋ผ์ฐํฐ import
app = FastAPI(
title="FDS API Gateway",
description="All domain APIs integrated",
docs_url="/gateway/docs",
redoc_url="/gateway/redoc",
openapi_url="/gateway/openapi.json"
)
# ๊ฐ ๋๋ฉ์ธ ๋ผ์ฐํฐ๋ฅผ prefix์ ํจ๊ป ๋ฑ๋ก
app.include_router(file_router, prefix="/file")
app.include_router(data_router, prefix="/data")
# ... ๊ธฐํ ๋๋ฉ์ธ ๋ผ์ฐํฐ ๋ฑ๋ก
๐ API ๋ฌธ์ ํ์ธ ๋ฐฉ๋ฒ
์ค์ง gateway ์๋น์ค์์ /gateway/docs
๋๋ /gateway/redoc
๊ฒฝ๋ก๋ฅผ ํตํด ๋ชจ๋ ๋๋ฉ์ธ API ๋ฌธ์๋ฅผ ํตํฉ ํ์ธํ ์ ์์ต๋๋ค.
์์:
http://localhost:8000/gateway/docs #Swagger
http://localhost:8000/gateway/redoc #API ๋ฌธ์
โ ๊ตฌ์ฑ ์ ๋ต ์์ฝ
- ๊ฐ ๋๋ฉ์ธ ์๋น์ค๋ ์์ฒด FastAPI ์ธ์คํด์ค๋ฅผ ์ง์ ์ ์ํ๋, Swagger UI๋ ๋นํ์ฑํ
- gateway ์๋น์ค์์๋ง Swagger ๋ฌธ์๋ฅผ ํตํฉ ์ ๊ณต
- ๋ฏธ๋ค์จ์ด, ํธ๋ ์ด์ฑ, ์์ธ์ฒ๋ฆฌ, ๋ผ์ฐํฐ ๋ฑ๋ก์ ๊ฐ ๋๋ฉ์ธ ์ง์ ์ ์์ ์ผ๊ด ์ฒ๋ฆฌ
- ๋๋ฉ์ธ๋ณ ๋ผ์ฐํฐ๋ gateway์ ํตํฉ ๋ฑ๋ก
๐ ์ ๋ฆฌ
- ๋๋ฉ์ธ๋ณ ์๋น์ค๋ ๋ ๋ฆฝ์ ์ผ๋ก ๋์ํ์ง๋ง, API ๋ฌธ์(Swagger)๋ gateway์์๋ง ํตํฉ ์ ๊ณต
- gateway์
/gateway/docs
์์ ์ ์ฒด API ์คํ์ ํ ๋ฒ์ ํ์ธ - ๊ฐ ๋๋ฉ์ธ ์๋น์ค๋ ์์ฒด์ ์ผ๋ก ๋ฏธ๋ค์จ์ด, ๋ก๊น , ํธ๋ ์ด์ฑ, ์์ธ์ฒ๋ฆฌ ๋ฑ์ ๊ด๋ฆฌ
๐ 5. ๋ผ์ฐํ ๊ณผ ์ธ์ฆ ํ๋ฆ: Gateway ์ค์ฌ ์ค๊ณ ์์น๊ณผ ์ธ์ฆ ๋ชจ๋ ์ฐ๊ณ
โ ๋ชฉํ
- Gateway์์ ๋ชจ๋ ์ธ๋ถ API ์์ฒญ์ ์์ ํ๊ณ ์ธ์ฆ ํ ๊ฐ ๋๋ฉ์ธ ์๋น์ค๋ก ๋ผ์ฐํ
- ์ธ์ฆ ๋ก์ง์ ๊ณตํต ๋ชจ๋๋ก ๋ถ๋ฆฌํ์ฌ ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ๊ฒ ์ค๊ณ
๐งญ ์ ์ฒด ํ๋ฆ ์์ฝ
ํด๋ผ์ด์ธํธ ์์ฒญ
↓
Gateway (JWT ๊ฒ์ฆ ์ํ)
↓
๋ผ์ฐํ
๋ ๋๋ฉ์ธ ์๋น์ค (์: file)
↓
์๋น์ค ๋ก์ง ์ํ ํ ์๋ต ๋ฐํ
- ์ธ์ฆ์ Gateway์์๋ง ์ฒ๋ฆฌํ๋ฉฐ, ๋๋ฉ์ธ ์๋น์ค๋ ์ธ์ฆ๋ ์์ฒญ๋ง ์ฒ๋ฆฌ
- JWT ํ ํฐ์ ์ ํจ์ฑ ๊ฒ์ฌ๋ Gateway ๋ด ์ธ์ฆ ๋ชจ๋์ด ๋ด๋น
๐งฉ ์ธ์ฆ ์ฒ๋ฆฌ ๊ตฌ์กฐ (gateway ๋ด๋ถ)
์์: /imgplt/auths
ํธ์ถ ์
# app/domains/gateway/api/routes/auth.py
from fastapi import APIRouter, Header, HTTPException, status
router = APIRouter()
@router.get("/imgplt/auths")
def check_auth_token(accessToken: str = Header(...)):
if accessToken != "VALID_TOKEN": # ์์์ฉ ๋ก์ง
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
return {"message": "์ธ์ฆ ์ฑ๊ณต"}
- ์ค์ ๊ตฌํ์์๋ accessToken์ workspace ์ธ์ฆ ์๋ฒ๋ก ์์ํ์ฌ ๊ฒ์ฆํ ์ ์์
- local/mock ๋ชจ๋๋ก๋ ์ค์ ๊ฐ๋ฅํ๋๋ก
.env
์AUTH_MODE
๋ฅผ ๊ตฌ๋ถ
โ๏ธ ์ธ์ฆ ๋ชจ๋ ์ค์ ๋ฐฉ์ (AUTH_MODE
)
AUTH_MODE=local # local / remote
AUTH_LOCAL_TOKEN=dev-token
AUTH_SERVER_URL=https://workspace.example.com/auth/verify
local
๋ชจ๋: ์ฌ์ ์ ์๋ ํ ํฐ(AUTH_LOCAL_TOKEN
)๊ณผ ๋น๊ตremote
๋ชจ๋: ์ค์ ์ธ์ฆ ์๋ฒ์ HTTP ์์ฒญ์ผ๋ก ์์
๐ ๋ผ์ฐํ ๊ตฌ์กฐ์ ํต์ฌ
- Gateway๋ ๋๋ฉ์ธ๋ณ ๋ผ์ฐํฐ๋ฅผ ๋ค์๊ณผ ๊ฐ์ด prefix๋ฅผ ๋ถ์ฌ ๋ฑ๋กํฉ๋๋ค:
app.include_router(file_router, prefix="/file")
app.include_router(data_router, prefix="/data")
- ๋ชจ๋ ๊ฒฝ๋ก๋ gateway์
/gateway
ํ์์ ์์นํ๊ฒ ๋ฉ๋๋ค:
/gateway/file/upload
/gateway/data/insert
๐ ์ ๋ฆฌ
- Gateway๋ ์ธ์ฆ๊ณผ ๋ผ์ฐํ ์ ์ ๋ดํ๋ฉฐ, ๋๋ฉ์ธ ์๋น์ค๋ ๋น์ฆ๋์ค ๋ก์ง์๋ง ์ง์ค
- ์ธ์ฆ์ ๊ณตํต ๋ก์ง์ผ๋ก ๊ตฌํ๋๋ฉฐ local/remote ๋ชจ๋ ๋ถ๊ธฐ ๊ฐ๋ฅ
- ๋๋ฉ์ธ ๋ผ์ฐํฐ๋ Gateway์ ํตํฉ๋์ด
/gateway/{domain}/
๊ฒฝ๋ก๋ก ํต์ผ
๐ 6. ํ์ผ ์ ๋ก๋ API ๊ตฌ์กฐ ํด์ค: S3 + Kafka๊น์ง์ ๋น์ฆ๋์ค ํ๋ฆ
โ ๋ชฉํ
- ํด๋ผ์ด์ธํธ๊ฐ ํ์ผ์ ์ ๋ก๋ํ๋ฉด, ์ด๋ฅผ S3์ ์ ์ฅํ๊ณ ๋ฉํ๋ฐ์ดํฐ๋ Kafka๋ก ์ ์ก
- ์ ์ฒด ํ๋ฆ์ Gateway → File ๋๋ฉ์ธ → MinIO + Kafka๋ก ์ด์ด์ง
๐งญ ์ ์ฒด ๋น์ฆ๋์ค ํ๋ฆ๋
Client Request
↓
Gateway (/imgplt/upload)
↓
AuthModule.verify(accessToken)
↓
FileUploadInterface.upload_file()
↓
FileUploadService.upload_file()
↓
UploadHandlerInterface.handle_upload()
↓
UploadHandlerService.handle_upload()
↓
S3Client.upload() → KafkaProducer.send()
↓ ↓
S3 ์ ์ฅ ๋ฉํ๋ฐ์ดํฐ ์ ์ก
๐ ์ฃผ์ ์ฝ๋ ๊ณ์ธต ๊ตฌ์ฑ
app/domains/file/
โโโ api/routes/upload.py # ์๋ํฌ์ธํธ ์ ์
โโโ services/interfaces/ # ์ถ์ ์ธํฐํ์ด์ค ๊ณ์ธต
โโโ services/impl/ # ์ค์ ๊ตฌํ์ฒด (๋น์ฆ๋์ค ๋ก์ง)
โโโ clients/s3_client.py # MinIO ์
๋ก๋ ์ฒ๋ฆฌ
โโโ clients/kafka_producer.py # Kafka ๋ฉ์์ง ์ ์ก ์ฒ๋ฆฌ
- API ์์ฒญ ์ฒ๋ฆฌ:
upload.py
- ํต์ฌ ๋ก์ง ์ฒ๋ฆฌ:
UploadHandlerService
- ํ์ผ ์ ์ฅ ๋ฐ Kafka ์ ์ก: ๊ฐ๊ฐ ์ ์ฉ ํด๋ผ์ด์ธํธ๋ก ์์
๐ง ์ค์ API ์ฌ์ฉ ์์
์์ฒญ ํ์
curl -X POST http://localhost:8000/gateway/imgplt/upload \
-H "accessToken: dev-token" \
-F "file=@sample.jpg" \
-F "meta={\"table\": \"camera01\", \"ts\": \"2024-01-01 12:00:00\"}" \
-H "Content-Type: multipart/form-data"
์๋ต ์์
{
"message": "Upload successful",
"s3_key": "camera01/20240101/sample.jpg",
"meta": {
"table": "camera01",
"ts": "2024-01-01 12:00:00"
}
}
๐งฉ ์ค๊ณ ํฌ์ธํธ
- ๋น๋๊ธฐ ํธ์ถ: ๋ด๋ถ์ ์ผ๋ก
async def
๊ธฐ๋ฐ์ผ๋ก ์ค๊ณ๋์ด ๋์ ๋์์ฑ ์ง์ - ์์กด์ฑ ๋ถ๋ฆฌ: Kafka, S3 ํด๋ผ์ด์ธํธ๋ ๊ตฌํ์ฒด๋ง ๋ฐ๊ฟ๋ ๋์ ๊ฐ๋ฅํ๊ฒ ์ถ์ํ
- ๋๋ฉ์ธ ์ฑ ์ ๋ถ๋ฆฌ: Gateway๋ ์ธ์ฆ ๋ฐ ๋ผ์ฐํ ๋ง, File ๋๋ฉ์ธ์ ์ ๋ก๋ ์ ๋ด
๐ ์ ๋ฆฌ
- ํด๋ผ์ด์ธํธ๋
/gateway/imgplt/upload
์ ํ์ผ๊ณผ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ ์ก - Gateway๋ ์ธ์ฆ์ ํ์ธํ๊ณ File ๋๋ฉ์ธ์ ์์ฒญ ์์
- ํ์ผ์ S3(MinIO)์ ์ ์ฅ, ๋ฉํ๋ฐ์ดํฐ๋ Kafka ํ ํฝ์ผ๋ก ์ ์ก
- ๊ณ์ธต๋ณ๋ก ์ญํ ์ด ๋๋์ด ์์ด ํ ์คํธ, ์ ์ง๋ณด์, ๊ต์ฒด๊ฐ ์ฉ์ด
7. ๋ก๊น ๋ฐ ํธ๋ ์ด์ฑ ๊ตฌ์ฑ: loguru + Sentry ์ค์ฌ์ ์์ธ์ฒ๋ฆฌ ๊ตฌ์กฐ
โ ๋ชฉํ
- ๊ฐ ์๋น์ค์์ ๋ฐ์ํ๋ ๋ชจ๋ ์์ธ๋ฅผ Sentry๋ก ์ ์กํ์ฌ ์ฅ์ ์ถ์ ๊ฐ๋ฅ
- ๋ชจ๋ ๋ก๊ทธ๋ loguru ๊ธฐ๋ฐ logger๋ก ์ผ์๋ณ ํ์ผ(app-YYYYMMDD.log) + ์ฝ์์ ๊ธฐ๋ก
- ์๋ฌ ์๋ฆผ์ Sentry SDK๋ฅผ ํตํด ์กฐ๊ฑด๋ถ(์ด์/์คํ ์ด์ง ํ๊ฒฝ)๋ก๋ง ์ ์ก
- OpenTelemetry(OTEL)๋ ์ ํ ์ฌํญ์ด๋ฉฐ, ๊ธฐ๋ณธ์ Sentry ์ค์ฌ์ผ๋ก ์ด์
- ์๋น์ค๋ณ ๋ก๊ทธ ์ ์ก์ fire-and-forget ๋ฐฉ์(๋น๋๊ธฐ, ์คํจํด๋ ๋น์ฆ๋์ค ์ํฅ ็ก)
๐งญ ์์ธ์ฒ๋ฆฌ ๋ฐ ๋ก๊น ํ๋ฆ
FastAPI ์ฑ ์์
↓
loguru ๊ธฐ๋ฐ TraceLoggingMiddleware ๋ฑ๋ก (์์ฒญ/์๋ต ๋ก๊น
)
↓
์์ธ ๋ฐ์ ์ install_exception_handlers ๋๋ global_exception_handler ๋์
↓
- loguru logger๋ก ์ฝ์ + ์ผ์๋ณ ํ์ผ ๋ก๊ทธ ์ถ๋ ฅ
- ์ด์/์คํ
์ด์ง ํ๊ฒฝ์ด๋ฉด Sentry์ ์์ธ ์ ์ก
โ๏ธ ์์ธ ์ฒ๋ฆฌ ๋ฐฉ์
1) ๋ชจ๋ ์์ธ → Sentry ๊ธฐ๋ก ์ฌ๋ถ ๋ฐ loguru ๋ก๊น
from app.common.logging import logger
import sentry_sdk
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
if USE_SENTRY and SENTRY_DSN:
sentry_sdk.capture_exception(exc)
logger.error(f"[Global Exception] {request.method} {request.url} - {exc}", exc_info=True)
# ... (์ ์ ํ ์๋ต ๋ฐํ)
- global handler์์ ๋ชจ๋ ์์ธ ํฌ์ฐฉ
- ์ด์/์คํ ์ด์ง ํ๊ฒฝ์ด๋ฉด Sentry๋ก ์ ์ก, ํญ์ loguru๋ก ํ์ผ+์ฝ์ ๊ธฐ๋ก
2) ์ปค์คํ ์์ธ ๋ฐ ์์คํ ์์ธ
- HTTPException, CustomException, ์์คํ ์์ธ ๋ชจ๋ ๋์ผํ๊ฒ ์ฒ๋ฆฌ
- loguru logger์ Sentry SDK ๋ณํ ์ฌ์ฉ, trace/context ์๋ ์บก์ฒ
3) fire-and-forget ๋ก๊ทธ ์ ์ก
- ๊ฐ ์๋น์ค๋ send_log_async๋ก ๋ก๊ทธ๋ฅผ ๋น๋๊ธฐ ์ ์ก
- ์คํจํ๋๋ผ๋ ์๋น์ค ๋ก์ง๊ณผ ๋ฌด๊ดํ๊ฒ ์ฒ๋ฆฌ๋จ
- ์ค์ ๋ก๊ทธ ์๋น์ค๋ ์ผ์๋ณ ํ์ผ(total_log/app-YYYYMMDD.log)์ ์ง๊ณ
๐ฆ ํ๊ฒฝ๋ณ ์ค์ ํฌ์ธํธ (.env, config.py, logging.py)
- .env ๋ฐ config.py ๊ธฐ์ค:
if ENV in ["production", "stage"]:
self.SENTRY_DSN = os.getenv("SENTRY_DSN")
self.USE_SENTRY = os.getenv("USE_SENTRY", "false").lower() == "true"
else:
self.SENTRY_DSN = None
self.USE_SENTRY = False
- logging.py์์ loguru ์ค์ :
from loguru import logger
from datetime import datetime
LOG_PATH = f"logs/app-{datetime.now().strftime('%Y%m%d')}.log"
logger.add(LOG_PATH, rotation="10 MB", retention="7 days", level="INFO")
- ์ด์/์คํ ์ด์ง๋ง Sentry ํ์ฑํ, ๊ฐ๋ฐ/ํ ์คํธ๋ ๋ก์ปฌ ๋ก๊ทธ๋ง ๊ธฐ๋ก
- loguru logger๋ ๋ชจ๋ ํ๊ฒฝ์์ ์ฌ์ฉ, Python ํ์ค logging ๋ฏธ์ฌ์ฉ
- OTEL์ ํ์ํ ๊ฒฝ์ฐ์๋ง ํ์ฑํ
๐ ๋ก๊ทธ/์์ธ ์ฒ๋ฆฌ ํฌ์ธํธ ์์ฝ
- loguru ๊ธฐ๋ฐ logger๋ก ๋ชจ๋ ๋ก๊ทธ ๊ธฐ๋ก (ํ์ผ + ์ฝ์)
- TraceLoggingMiddleware: ์์ฒญ/์๋ต ์ถ์ ์ฉ ๋ฏธ๋ค์จ์ด
- install_exception_handlers ๋๋ @app.exception_handler: ์์ธ ์ํฉ์์ Sentry ์ ์ก ๋ด๋น
- FastAPI์ ๊ธฐ๋ณธ ์์ธ ๋ฐ ๋ฐํ์ ์ค๋ฅ ๋ชจ๋ ํตํฉ ์ฒ๋ฆฌ ๊ฐ๋ฅ
- ์๋น์ค ๊ฐ ๋ก๊ทธ ์ ์ก์ send_log_async ๊ธฐ๋ฐ ๋น๋๊ธฐ ์ฒ๋ฆฌ
๐ ์ ๋ฆฌ
- ๋ชจ๋ ์์ธ๋ ๊ณตํต ํธ๋ค๋ฌ์์ ์์ง๋๊ณ , ์กฐ๊ฑด ์ถฉ์กฑ ์ Sentry๋ก ์ ์ก
- Sentry ์ค์ ์ .env์ config.py์ ๋ฐ๋ผ ๋ถ๊ธฐ๋๋ฉฐ, ์ด์ ํ๊ฒฝ ์ค์ฌ์ผ๋ก ์ฌ์ฉ
- loguru ๊ธฐ๋ฐ ๋ก๊ทธ ์์คํ ์ผ๋ก ์ผ์๋ณ ํ์ผ + ์ฝ์ ์ถ๋ ฅ
- ๋ก๊ทธ ์ ์ก์ ๋น๋๊ธฐ(fire-and-forget) ๋ฐฉ์์ผ๋ก ์ด์ ์๋น์ค์ ์์ ํ ๋ถ๋ฆฌ๋์ด ๋์
- OpenTelemetry๋ ์ ํ ์ฌํญ์ด๋ฉฐ ๊ธฐ๋ณธ ์์ธ ์ถ์ ์ Sentry ์ค์ฌ์ผ๋ก ๊ตฌํ