work

FastAPI ๊ธฐ๋ฐ˜ fds_boilerplate

ygtoken 2025. 5. 13. 17:46
728x90

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 ์ค‘์‹ฌ์œผ๋กœ ๊ตฌํ˜„
728x90