요즘 크래프톤 정글 최종 프로젝트에서 내가 개발했던 AI 모의면접 기능만 배포하려고 다시 개발중인데, 백엔드를 NestJS에서 FastAPI로 변경해서 개발하는 과정에서 얻은 지식을 적어본다.
# 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 인스턴스를 유지한다. 이것이 싱글턴을 쓰는 이유이기도 하다.
'저런 과정 없었던 거 같은데..? 인스턴스를 굳이 만들 필요가 없지 않았나..?'
분명 그 때는 export class ~~~ 라고 정의만 해 놓고 다른 곳에서 import했던 걸로 기억했다.
내 기억이 확실한지, 그리고 맞다면 NestJS와 FastAPI에 무슨 차이가 있는지 궁금했다.
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()). 이건 나중에 더 자세히 알아봐야겠다.)
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 관리 컨테이너에 대해서는 예전에 들은 적이 있지만, 이번에 확실히 무슨 역할을 하는지 알게 되었다.
이런 식으로 다른 언어와 프레임워크로 마이그레이션하면서 배울 수 있는 것들이 많을 것 같다.
개발하면서 하나씩 이렇게 정리하다 보면 좋은 공부가 될 것 같다.