Skip to content

NestJS Providers와 의존성 주입(DI)

개요

컨트롤러는 HTTP 요청을 받는 일만 하고, 실제 비즈니스 로직은 밖으로 빼는 게 NestJS의 기본 사고방식이에요. 그 로직을 담당하는 서비스, 저장소, 팩토리, 헬퍼 같은 것들을 통틀어 provider라고 부릅니다. provider의 핵심 성질은 의존성으로 주입될 수 있다는 것인데, 이 덕분에 객체들이 서로 관계를 맺으면서도 구현을 갈아 끼우기 쉬워져요. 컨트롤러가 HTTP를 처리하고 provider에 복잡한 일을 위임하는 구조죠.

핵심 개념

@Injectable로 IoC 컨테이너에 등록

provider는 기본적으로 @Injectable() 데코레이터를 단 클래스예요. 이 데코레이터가 클래스에 메타데이터를 붙여서, Nest의 IoC 컨테이너가 이 클래스를 관리할 수 있음을 알려줍니다. 컨테이너는 앱 시작 시 의존성을 해석해 provider를 생성하고, 앱이 끝나면 함께 정리해요. provider의 생성 시점을 스코프(scope)라고 하는데, 기본적으로 앱 생명주기와 맞춰집니다.

생성자 주입으로 해석

주입은 생성자에서 이뤄져요.

@Controller('cats')
export class CatsController {
  constructor(private catsService: CatsService) {}
}

여기서 TypeScript의 타입 주석 CatsService가 실제로 해석의 근거가 됩니다. 컴파일 타임에 생성자 파라미터 타입이 메타데이터로 남고, 컨테이너가 그 메타데이터를 읽어 어떤 provider를 넣을지 결정하죠. 관례적으로 타입으로 주입하지만, 특정 토큰에 @Inject()를 써서 이름으로 주입할 수도 있어요.

커스텀 provider

내장 IoC 컨테이너는 흔한 생성자 주입보다 더 많은 것을 지원합니다. provider를 값, 클래스, 동기·비동기 팩토리 등 여러 방식으로 정의할 수 있어요. 예를 들어 @Optional()을 함께 쓰면 주입할 대상이 없어도 오류 없이 넘어가는 선택적 provider를 만들 수 있습니다.

@Injectable()
export class HttpService<T> {
  constructor(
    @Optional() @Inject('HTTP_OPTIONS') private httpClient: T,
  ) {}
}

모듈에 등록해야 주입 가능

provider를 정의했다고 끝나는 게 아니에요. 모듈 파일에서 @Module()providers 배열에 등록해야 컨테이너가 의존성을 해석할 수 있습니다. 등록하기 전까지는 컨트롤러가 주입받을 대상이 없는 상태죠.

@Module({
  controllers: [CatsController],
  providers: [CatsService],
})
export class AppModule {}

만약 부트스트랩 과정에서 provider를 직접 꺼내 써야 한다면 Standalone Application처럼 별도 진입점을 활용하기도 해요.

실제 적용 (데이터스케쳐스 관점)

웹빌더 백엔드에서 provider는 표현 계층 아래의 응용·도메인 로직을 담당해요.

  • 서비스로 비즈니스 로직 분리 — LLM 호출, 큐 작업, 검증 같은 로직을 @Injectable() 서비스로 떼어 두고 컨트롤러는 요청 위임만 해요.
  • 구현 갈아 끼우기 — 같은 인터페이스 뒤에 여러 구현을 두고 DI로 바꿔 끼워서 테스트나 벤더 전환을 쉽게 해요.
  • 팩토리 provider 활용 — 환경에 따라 달라지는 설정이나 연결을 팩토리 provider로 만들어 유연하게 구성합니다.

더 알아보기