목차

1. 개요
Python 버전: 3.11 이상
FastAPI 버전: 0.116.1 이상
FastAPI에서 무거운 LLM/딥러닝 객체를 불러올 일이 있다.
이것은 큰 비용을 사용하기 때문에,
API를 실행할 때 마다 객체를 생성하게 되면,
큰 부담이 될 수 있으므로 싱글톤 패턴을 이용하도록 하자.
그리고, 자원관리 따위 솔직히 알고싶지 않고.... 난 백엔드 개발자가 아닌데 이거까지 해줘야하냐(퉤), 백엔드 개발자들아 아무나 좀 와서 나대신 해줘라. 데싸니까 나는 이딴거 안함 (튀튀)
2. 개념
2.1. 싱글톤(Singleton) 패턴 개념
싱글톤 패턴은 하나의 클래스에서 단 하나의 인스턴스만 생성하도록 보장하는 객체 생성 패턴이다.
애플리케이션 전체에서 그 객체는 한 번만 만들어지며, 어디에서든 동일한 인스턴스를 공유해 사용한다.
이 패턴의 핵심 목표는 다음 두 가지다.
- 인스턴스 생성 비용 절감
데이터베이스 커넥션 풀, HTTP 클라이언트처럼 생성 비용이 무거운 객체는 계속 새로 만들면 비용이 크다.
싱글톤을 사용하면 최초 한 번만 만들고 이후 요청들은 이미 생성된 객체를 그대로 사용한다. - 애플리케이션 전역 상태의 일관성 유지
공통 설정 값, 공통 캐시, 공통 로그 객체처럼 시스템 전체에서 같은 상태를 유지해야 하는 경우 싱글톤이 유용하다.
2.2. FastAPI에서 사용되는 경우
1. 무거운 객체
이런 객체는 생성할 때 I/O 또는 메모리 비용이 크기 때문에 싱글톤으로 관리하는 것이 가장 안정적이다.
- 데이터베이스 엔진
- HTTP 클라이언트 (httpx.AsyncClient)
- 머신러닝/딥러닝/LLM 모델 로더
- 쿠버네티스 클라이언트 등 외부 리소스 클라이언트
2. 시스템 전역 설정 또는 공통 로직
이런 값들은 여러 라우터와 서비스 계층에서 공유하므로 싱글톤 형태가 자연스럽다.
- 공통 Config
- 인증 키 관리자
- 공통 캐시 매니저
3. 사용방법
3.1. 모듈 레벨 싱글톤
설명: python이 모듈을 한 번만 로드하기 때문에, 사실상 싱글톤
뭔가 허술해 보이는 방식이지만, 잘 동작함
소규모 프로젝트에서 주로 사용 >> 그냥 사용하지 말자.
객체 생성
# app/clients.py
import httpx
client = httpx.AsyncClient()
객체 사용 (자원 이므로 주로 Service에서 사용)
# app/services/user_service.py
from app.clients import client
async def fetch_user(user_id: int):
res = await client.get(f"https://example.com/api/user/{user_id}")
return res.json()
객체 종료 (종료는 main.py > lifespan() 에서 사용)
# main.py
from fastapi import FastAPI
from contextlib import asynccontextmanager
from app.clients import client
@asynccontextmanager
async def lifespan(app: FastAPI):
yield
await client.aclose()
app = FastAPI(lifespan=lifespan)
3.2. lifespan을 이용한 싱글톤
설명: FastAPI 에서 권장하는 방식
시작부분과 close() 부분을 명확히 알 수 있어, close() 가 필요하다면 가장 좋은 방법
즉 리소스 관리는 lifespan을 이용
객체 생성 / 생성 및 종료 함수 정의
async def init_db(app):
app.state.db = create_async_engine(DATABASE_URL)
async def close_db(app):
await app.state.db.dispose()
객체 생성 및 종료
from fastapi import FastAPI
from contextlib import asynccontextmanager
from app.core.database import init_db, close_db
from app.core.redis import init_redis, close_redis
from app.core.http_client import init_http, close_http
@asynccontextmanager
async def lifespan(app: FastAPI):
# create resources
await init_db(app)
await init_redis(app)
await init_http(app)
yield
# clean resources
await close_http(app)
await close_redis(app)
await close_db(app)
app = FastAPI(lifespan=lifespan)
객체 사용
라우터에서 호출
@router.get("/user/{user_id}")
async def user_detail(user_id: int, request: Request):
return await fetch_user(user_id, request)
서비스에서 사용
# services/user_service.py
async def fetch_user(user_id: int, request):
client = request.app.state.client
res = await client.get(f"https://example.com/api/user/{user_id}")
return res.json()
3.3. DI(Dependency Injection) 기반
언제 사용하나?
- 모듈레벨 싱글톤/lifespan 기반 싱글톤에서 객체를 만들고 DI로 불러오면 됨.
장점
- 테스트에서 mock으로 주입 가능
- 라우터 메서드 시그니처를 통해 의존성 명확
- 객체마다 life-cycle이 명확
- 서비스 분리가 쉬워지고 유지보수성이 극적으로 향상됨
객체 생성 / 제공 함수
# clients.py
client = httpx.AsyncClient()
def get_client():
return client
라우터에서 불러오기
@router.get("/test")
async def test(client=Depends(get_client)):
...
종료하기 (마찬가지로 lifespan 에서 진행)
from clients import get_client, client
@asynccontextmanager
async def lifespan(app: FastAPI):
# 서버 시작 시: 이미 clients.py에서 client는 생성됨
yield
# 서버 종료 시: 싱글톤 client 종료
await client.aclose()
3.4. 캐시 데코레이터(@lru_cache)를 이용한 싱글톤
functools.lru_cache는 파이썬 표준 라이브러리에서 제공하는 함수 결과 캐시 데코레이터다.
간단히 말하면
- Close() 가 필요 없다면 이 방법이 가장 효율적
- 같은 인자로 함수를 여러 번 호출해도
- 처음 한 번만 실제로 계산하고
- 이후에는 캐시에 저장된 값을 바로 반환하는 기능
- FastAPI 공식 문서에서도 설정 객체를 싱글톤으로 만들 때 이 패턴을 추천
객체 생성
from functools import lru_cache
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "My App"
debug: bool = False
database_url: str
class Config:
env_file = ".env"
@lru_cache
def get_settings():
return Settings()
라우터에서 의존성으로 사용 (설정 객체같은 것은 close 안해줘도 됨)
from fastapi import Depends, FastAPI
app = FastAPI()
@app.get("/info")
def info(settings: Settings = Depends(get_settings)):
return {
"app_name": settings.app_name,
"debug": settings.debug,
}
3.5. 클래스로 싱글톤 구현
Python의 모듈 캐시 자체가 싱글톤 구조이기 때문에
굳이 클래스로 싱글톤을 만들 필요가 거의 없음
4. 싱글톤 사용 예시
4.1. ChatGPT 클라이언트
ChatGPT API 클라이언트는 FastAPI에서 싱글톤 또는 lifespan 1회 생성이 정답
# app.py
from fastapi import FastAPI
from openai import AsyncOpenAI
openai_client: AsyncOpenAI = None
async def lifespan(app: FastAPI):
global openai_client
openai_client = AsyncOpenAI(api_key="YOUR_KEY")
yield
app = FastAPI(lifespan=lifespan)
Dependency.py 에 정의
# deps.py
from openai import AsyncOpenAI
from .app import openai_client
def get_openai_client() -> AsyncOpenAI:
return openai_client
이후 DI 로 불러오자
@router.post("/ask")
async def ask(
client: AsyncOpenAI = Depends(get_openai_client)
):
return await client.chat.completions.create(...)
4.2. 데이터베이스 엔진
SQLAlchemy
# db.py
from sqlalchemy.ext.asyncio import create_async_engine
engine = create_async_engine(DATABASE_URL, pool_pre_ping=True)
4.3. HTTP 등 외부 클라이언트 (httpx.AsyncClient)
AsyncClient()
AWS boto3, Firebase Admin SDK, Stripe 클라이언트 등
# clients.py
client = httpx.AsyncClient()
4.4. Redis 클라이언트
# redis_client.py
redis = aioredis.from_url(REDIS_URL)
4.5. 설정(Settings) 객체 (환경변수, .env)
@lru_cache 패턴을 쓰자
@lru_cache
def get_settings():
return Settings()
4.6. 인증/보안 관련 객체
- JWT 키 관리
- OAuth2/OpenID Provider 클라이언트
- 토큰 검증 키 캐시
- 인증 토큰
이런 보안 관련 객체들은 앱 전체에서 공유되어야 한다.
# security.py
jwt_manager = JWTManager(secret_key=SECRET_KEY)
4.7. 모델 로더(머신러닝 모델), Embedding Client
딥러닝 모델 로딩 비용은 매우 크기 때문에
서버 시작 시 로드해두고 모든 요청에서 재사용해야 한다.
# ml.py
nlp_model = load_model("model.bin")
4.8. 서비스 계층(Service Layer) 객체
서비스 객체가
- http client
- db session factory
- config
같은 리소스를 내부 멤버로 가지고 있을 경우,
해당 서비스 객체 자체를 싱글톤으로 관리하기도 한다.
class UserService:
def __init__(self, http, redis):
self.http = http
self.redis = redis
user_service = UserService(http_client, redis_client)
'Fastapi' 카테고리의 다른 글
| [FastAPI] 트렌젝션 사용하기 (17) (0) | 2025.08.02 |
|---|---|
| [FastAPI] 객체지향과 FastAPI (16) (0) | 2025.02.25 |
| [FastAPI] FastAPI 속도개선 - 캐시(Cache) (15) (1) | 2025.02.04 |
| [FastAPI] pytest 사용법 (14) (0) | 2025.02.03 |
| [FastAPI] 로그 남기기 (logging) (13) (0) | 2025.02.03 |