본문으로 건너뛰기
김신건의 로그

[NestJS] Providers & Dependency Injection

· 수정 · 📖 약 2분 · 622자/단어 #nestjs #providers #dependency-injection #di
NestJS Providers, NestJS Dependency Injection, NestJS DI, @Injectable decorator, @Inject decorator, useValue useFactory useClass, Provider Scope, REQUEST scope, NestJS 의존성 주입

정의

NestJS Provider@Injectable() 데코레이터가 붙은 클래스 (또는 값/factory) 로, DI container 에 등록되어 다른 클래스에 주입됩니다. Service, Repository, Factory, Helper 등이 모두 provider 입니다. Constructor injection 이 기본.

왜 DI 인가

수동 초기화 없이 필요한 의존성을 주입:

// DI 없이
class UsersController {
  private service = new UsersService(new UsersRepo(new PrismaClient()));
}

// DI
@Controller('users')
class UsersController {
  constructor(private service: UsersService) {}
}
  • Testability: mock 주입 쉬움
  • 가독성: 의존성 명시
  • 재사용: singleton 자동
  • loose coupling: 인터페이스 기반

Injectable Service

import { Injectable } from '@nestjs/common';

@Injectable()
export class UsersService {
  constructor(
    private readonly repo: UsersRepository,
    private readonly logger: LoggerService,
  ) {}

  async findAll() {
    this.logger.log('finding all users');
    return this.repo.findAll();
  }
}

Module 에 등록:

@Module({
  providers: [UsersService, UsersRepository, LoggerService],
  exports: [UsersService],
})
export class UsersModule {}

기본 등록 형태

providers: [UsersService]

는 축약형:

providers: [
  {
    provide: UsersService,     // 토큰 (클래스 이름)
    useClass: UsersService,    // 실제 클래스
  },
]

Custom Provider

useClass - 다른 클래스로 대체

providers: [
  {
    provide: LoggerService,
    useClass: process.env.NODE_ENV === 'production'
      ? ProductionLogger
      : DevLogger,
  },
]

Injectable 은 LoggerService 지만 실제로는 env 에 따라 다른 구현.

useValue - 고정 값

const mockUsersService = {
  findAll: () => [{id: 1, name: 'kim'}],
};

providers: [
  {
    provide: UsersService,
    useValue: mockUsersService,
  },
]

테스트 mock 이나 상수 config 에 유용.

useFactory - Factory 함수

providers: [
  {
    provide: 'DATABASE_CONNECTION',
    useFactory: async (config: ConfigService) => {
      const client = new PrismaClient({
        datasources: {db: {url: config.get('DATABASE_URL')}}
      });
      await client.$connect();
      return client;
    },
    inject: [ConfigService],   // factory 인자
  },
]

Async 도 OK. inject 배열 순서 == factory 인자 순서.

useExisting - alias

providers: [
  LoggerService,
  {
    provide: 'ALIAS_LOGGER',
    useExisting: LoggerService,
  },
]

Non-Class Token

Class 가 아닌 값 (문자열, symbol) 을 토큰으로:

export const DATABASE_CONNECTION = 'DATABASE_CONNECTION';

providers: [
  {
    provide: DATABASE_CONNECTION,
    useFactory: () => new PrismaClient(),
  },
]

// 주입 시
@Injectable()
export class UsersRepo {
  constructor(
    @Inject(DATABASE_CONNECTION) private db: PrismaClient,
  ) {}
}

TypeScript 는 어떤 타입인지 몰라 @Inject() 명시. Symbol 도 가능:

export const DATABASE_CONNECTION = Symbol('DATABASE_CONNECTION');

Class 를 그대로 토큰으로 쓰면 @Inject() 필요 없음 (TypeScript 가 메타데이터 저장).

Provider Scope

기본은 Singleton (앱 한 개). 다른 scope:

import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.DEFAULT })      // singleton (default)
@Injectable({ scope: Scope.REQUEST })      // 요청마다 새 instance
@Injectable({ scope: Scope.TRANSIENT })    // 주입마다 새 instance
export class MyService {}

REQUEST scope

각 HTTP 요청마다 새 instance. Request context 접근 가능.

import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';

@Injectable({ scope: Scope.REQUEST })
export class RequestContextService {
  constructor(@Inject(REQUEST) private request: Request) {}

  getUserId(): string {
    return this.request.user.id;
  }
}

주의: REQUEST scope provider 를 주입받는 상위 provider 도 자동으로 REQUEST scope. 성능 오버헤드 (요청마다 인스턴스 생성 + injection graph 재구성). 꼭 필요할 때만.

TRANSIENT

주입될 때마다 새 instance. Consumer 별 독립 상태.

@Injectable({ scope: Scope.TRANSIENT })
export class Counter {
  private count = 0;
  increment() { return ++this.count; }
}

// UserService.counter 와 OrderService.counter 는 다른 인스턴스

Optional Injection

import { Optional } from '@nestjs/common';

@Injectable()
export class MyService {
  constructor(
    @Optional() @Inject('OPTIONAL_TOKEN') private optional?: string,
  ) {}
}

주입 못 하면 undefined.

Async Provider

DB 연결 등 초기화가 async 인 경우:

{
  provide: 'DATABASE',
  useFactory: async () => {
    const conn = await connectToDb();
    return conn;
  },
}

AppModule 이 초기화 완료된 후 앱 시작.

Circular Dependency

Provider A -> B -> A -> forwardRef:

@Injectable()
export class AService {
  constructor(
    @Inject(forwardRef(() => BService))
    private bService: BService,
  ) {}
}

@Injectable()
export class BService {
  constructor(
    @Inject(forwardRef(() => AService))
    private aService: AService,
  ) {}
}

Refactoring 가 나은 편이 대부분.

Repository 패턴

// users.repository.ts
@Injectable()
export class UsersRepository {
  constructor(private prisma: PrismaService) {}

  async findAll() {
    return this.prisma.user.findMany();
  }

  async findOne(id: string) {
    return this.prisma.user.findUnique({where: {id}});
  }

  async create(data: CreateUserDto) {
    return this.prisma.user.create({data});
  }
}

Service 가 repo 를 주입:

@Injectable()
export class UsersService {
  constructor(private repo: UsersRepository) {}

  async findAll() {
    return this.repo.findAll();
  }
}

Multi-Provider (여러 값 배열)

여러 인스턴스를 하나의 토큰 배열로:

export const HANDLER = 'HANDLER';

providers: [
  {provide: HANDLER, useClass: HandlerA, multi: true} as any,   // Nest 는 표준 multi 없음
  {provide: HANDLER, useClass: HandlerB, multi: true} as any,
]

NestJS 는 표준 multi-provider 없음 (Angular 에는 있음). 대신 배열 provider:

providers: [
  HandlerA,
  HandlerB,
  {
    provide: 'HANDLERS',
    useFactory: (a: HandlerA, b: HandlerB) => [a, b],
    inject: [HandlerA, HandlerB],
  },
]

Lifecycle Hook

Provider 도 module 처럼:

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.connect();
  }

  async onModuleDestroy() {
    await this.disconnect();
  }
}

실전 패턴

Config injection

@Injectable()
export class ApiClient {
  private baseUrl: string;

  constructor(private config: ConfigService) {
    this.baseUrl = this.config.get('API_URL');
  }
}

여러 구현 선택

@Module({
  providers: [
    {
      provide: 'PAYMENT_GATEWAY',
      useClass: process.env.PAYMENT === 'stripe' ? StripeGateway : PayPalGateway,
    },
  ],
  exports: ['PAYMENT_GATEWAY'],
})
export class PaymentModule {}

Strategy Pattern

interface EmailSender {
  send(to: string, subject: string, body: string): Promise<void>;
}

@Injectable() class SendGridSender implements EmailSender { ... }
@Injectable() class SmtpSender implements EmailSender { ... }

@Module({
  providers: [
    {
      provide: 'EMAIL_SENDER',
      useClass: SendGridSender,
    },
  ],
  exports: ['EMAIL_SENDER'],
})
export class EmailModule {}

Testing DI

const module = await Test.createTestingModule({
  providers: [
    UsersService,
    {
      provide: UsersRepository,
      useValue: {
        findAll: jest.fn().mockResolvedValue([{id: 1, name: 'kim'}]),
      },
    },
  ],
}).compile();

const service = module.get<UsersService>(UsersService);

자세한 것은 NestJS Testing 참조.

함정

WARNING

REQUEST scope 의 성능 비용. 매 요청 인스턴스 생성 + graph 재구성. Global logger, tracing 에는 다른 방식 (AsyncLocalStorage) 검토.

CAUTION

@Inject() 누락. String/Symbol 토큰 사용 시 @Inject() 명시 필수. Class 는 자동.

WARNING

Circular dependency 는 refactor 신호. forwardRef 는 응급.

IMPORTANT

useFactoryinject 순서. inject 배열 순서 == factory 인자 순서.

CAUTION

Module 에 provider 안 등록하면 UNKNOWN_DEPENDENCIES 오류. 오류 메시지의 module 이름 확인.

관련 위키

이 글의 용어 (8개)
[FastAPI] Dependency Injectionfastapi
정의 FastAPI Dependency Injection 은 로 함수를 endpoint 에 자동 주입하는 시스템입니다. DB 세션, 인증 사용자, 설정, 서비스 계층 등을 end…
[Framework] NestJSframeworks
정의 NestJS 는 Kamil Myśliwiec 이 2017년 발표한 Node.js 서버측 프레임워크 입니다. TypeScript first, Angular 에서 영감받은 모듈…
[NestJS] Config (Environment, Validation)frameworks
정의 NestJS Config 는 패키지로 환경변수를 관리합니다. 파일 로딩, TypeScript type-safe access, 검증 (Joi/Zod), 여러 환경 (dev/s…
[NestJS] Controllersframeworks
정의 NestJS Controller 는 데코레이터가 붙은 클래스로, HTTP 요청을 라우팅하고 응답을 반환합니다. Method 별로 , , , , 데코레이터를 붙여 URL + …
[NestJS] Database (TypeORM, Prisma, Mongoose, Drizzle)frameworks
정의 NestJS Database 통합은 여러 ORM/ODM 을 module 로 감싸 DI + 트랜잭션 + 마이그레이션을 관용화합니다. 주요 선택지: TypeORM (성숙), P…
[NestJS] Modulesframeworks
정의 NestJS Module 은 데코레이터가 붙은 클래스로, 관련된 controller, provider, import/export 를 하나로 묶는 조직 단위입니다. Angul…
[NestJS] Testing (Jest, Supertest, e2e)frameworks
정의 NestJS Testing 은 Jest (기본) + Supertest 조합으로 unit test 와 e2e test 를 지원합니다. 의 로 module 을 격리된 형태로 만…
[TypeScript] Decoratorstypescript
정의 Decorator 는 class, method, accessor, property, parameter 에 부착되는 함수로 그 대상을 검사/수정할 수 있습니다. TypeScr…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기