[고찰] NestJS vs. FastAPI : 싱글톤 패턴으로 작성된 클래스의 export

Ooleem·2025년 12월 11일

개인프로젝트

목록 보기
1/1

요즘 크래프톤 정글 최종 프로젝트에서 내가 개발했던 AI 모의면접 기능만 배포하려고 다시 개발중인데, 백엔드를 NestJS에서 FastAPI로 변경해서 개발하는 과정에서 얻은 지식을 적어본다.

궁금증을 느끼게 된 계기

  • FastAPI에서 환경변수 설정을 담당하는 config.py는 보통 다음과 같이 작성한다.
# config.py
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DB_HOST: str
    DB_PORT: int = 5432
    SECRET_KEY: str

    class Config:
        env_file = ".env"

settings = Settings()

그리고 이제 다른 파일, 예를 들면 app.py에서

from fastapi import FastAPI
from config import settings

app = FastAPI()

@app.get("/debug")
def debug():
    return {
        "db_host": settings.DB_HOST,
        "secret": settings.SECRET_KEY
    }

이렇게 가져와서 사용하게 된다.
그런데 여기서, import 'settings' 대신 import 'Settings'를 쓰면 안 되나 하는 생각이 갑자기 들었다.
결국, settings = Settings() 이 부분이 왜 필요한지 궁금했다.

클래스를 직접 가져오면 안 되는 이유

Settings를 쓸 경우 그 줄이 읽힐 때마다 Settings 객체를 다시 생성하면서, 환경변수 전체를 다시 로딩한다. 앱 내에의 여러 곳에서 Settings()를 호출하는 것은 당연히 매우 비효율적이고, 불필요한 오버헤드가 발생한다.
그래서 Settings 클래스의 인스턴스로써 settings를 딱 한 번만 생성한 뒤(settings = Settings()), 그 인스턴스를 다른 곳에서 import하여 사용하게 된다.
그러면 앱 전체에서 전역적으로 단일 settings 인스턴스를 유지한다. 이것이 싱글턴을 쓰는 이유이기도 하다.

어? 그런데 예전에 NestJS로 개발했을 때는..

'저런 과정 없었던 거 같은데..? 인스턴스를 굳이 만들 필요가 없지 않았나..?'
분명 그 때는 export class ~~~ 라고 정의만 해 놓고 다른 곳에서 import했던 걸로 기억했다.
내 기억이 확실한지, 그리고 맞다면 NestJS와 FastAPI에 무슨 차이가 있는지 궁금했다.

NestJS에서는 내부 DI 컨테이너가 인스턴스를 관리해준다

NestJS에서는 보통 다음과 같이 싱글톤 패턴을 작성한다.

// users.service.ts
@Injectable()
export class UsersService {
  // ...
}

그리고 다른 곳에서 다음과 같이 주입하여 사용하게 된다.

// users.controller.ts
@Controller('users')
export class UsersController {
  constructor(
    private readonly usersService: UsersService,  // 👈 여기
  ) {}
}

그냥 클래스를 정의하고, 다른 파일에서 import해서 써먹기만 하면 됐던 것이다.
UsersService = new UsersService() 이런 작업 없이 말이다.

이 작업을 NestJS가 대신 해 준다고 생각하면 된다.
NestJS가 내부 DI 컨테이너에서 UsersService를 한 번만 생성하고, 그 인스턴스를 각각의 Controller/다른 Service에 주입해 주는 것이다.

(FastAPI의 DI는 클래스 기반이 아니라 함수 기반이라고 한다(Depends()). 이건 나중에 더 자세히 알아봐야겠다.)

FastAPI에서 유사하게 흉내내는 방법

lru_cache를 이용해서, 처음 호출된 이후로는 캐시된 같은 인스턴스를 리턴하는 방법이 있다.

from functools import lru_cache
from pydantic_settings import BaseSettings

class Settings(BaseSettings):
    DB_HOST: str
    DB_PORT: int = 5432

    class Config:
        env_file = ".env"

@lru_cache
def get_settings():
    # 이 함수가 처음 호출될 때만 Settings()를 만들고
    # 그 이후로는 캐시된 같은 인스턴스를 리턴
    return Settings()

그리고 라우터 쪽에서는, 앞서 말했던 Depends를 이용하여 다음과 같이 사용한다.

from fastapi import Depends

@app.get("/items")
def read_items(settings: Settings = Depends(get_settings)):
    return {"db_host": settings.DB_HOST}

하지만 굳이 이러느니.. 그냥 인스턴스 하나 만드는 게 낫지 않을까 싶다.

결론

사실 NestJS의 내부 DI 관리 컨테이너에 대해서는 예전에 들은 적이 있지만, 이번에 확실히 무슨 역할을 하는지 알게 되었다.
이런 식으로 다른 언어와 프레임워크로 마이그레이션하면서 배울 수 있는 것들이 많을 것 같다.
개발하면서 하나씩 이렇게 정리하다 보면 좋은 공부가 될 것 같다.

profile
개발 / 성장 노트

0개의 댓글