Post

NestJS 의존성 주입에서 Symbol 활용하기

  1. NestJS 의존성 주입(DI)에 대해 알아본다.
  2. NestJS DI 방식과 Symbol 을 사용해야 하는 이유에 대해 알아본다.

의존성 주입이란?

의존성 주입은 클래스나 모듈이 필요한 의존성을 직접 생성하지 않고 외부에서 주입받는 디자인 패턴입니다.
이를 통해 코드의 결합도를 낮추고, 테스트와 유지보수를 쉽게 할 수 있습니다.

injection scope

NestJS::injection-scopes

Using singleton scope is recommended for most use cases.
Sharing providers across consumers and across requests means that an instance can be cached and its initialization occurs only once, during application startup.

ScopeDescription
DEFAULT제공자의 단일 인스턴스는 전체 애플리케이션에 걸쳐 공유됩니다.(Singleton 스코프)
인스턴스 수명은 애플리케이션 lifecycle 직접 연결됩니다.
애플리케이션이 부트스트랩되면 모든 싱글톤 provider가 인스턴스화됩니다.
REQUEST공급자의 새 인스턴스는 request 마다 전용으로 생성되며, request 처리를 완료한 후에는 인스턴스가 garbage-collected 됩니다.
TRANSIENTTransient(임시) provider 는 consumer 간에 공유되지 않습니다. Transient provider를 주입하는 각각의 consumer는 새로운 전용 인스턴스를 받게 됩니다.

종속성 주입하기

@Injectable() 을 이용한 종속성 주입 예시

1
2
3
// user.repository.ts
@Injectable()
export class UserRepository extends Repository<User> {}
1
2
3
4
5
6
7
8
9
// user.service.ts
import {Injectable} from '@nestjs/common';

@Injectable()
export class UserService {
  constructor(
    private readonly UserRepository: UserRepository
  ) {}
}

이때 @Injectable() 키워드를 사용할 경우 NestJS의 IoC 컨테이너에 의해 의존성이 주입/관리됩니다.

The @Injectable() decorator attaches metadata, which declares that Service is a class that can be managed by the Nest IoC container. NestJS::providers#services

1
2
3
4
5
6
7
8
9
10
11
// user.module.ts
import { Module } from '@nestjs/common';
import { UserService } from './user.service';
import { UserController } from './user.controller';

@Module({
  imports: [],
  controllers: [],
  providers: [UserService, UserRepository],
})
export class UserModule {}

UserService 에서 UserRepository 를 사용하고 있기 때문에, provider 에 UserRepository 를 불러오지 않으면 에러가 발생합니다.

Symbol 을 통해 의존성 주입 예시

Symbol 은 고유한 값을 정의합니다. geeksforgeeks::explain-the-symbol-type-in-typescript

1
2
3
4
5
6
7
8
9
10
// user.repository.ts

/* 
* The Symbol.for() static method searches for existing symbols in a runtime-wide symbol registry with the given key and returns it if found. 
* Otherwise a new symbol gets created in the global symbol registry with this key.
*/
export const USER_REPOSITORY = Symbol.for('USER_REPOSITORY');

@Injectable()
export class UserRepository extends Repository<User> {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// user-repository.module.ts   
import { Module } from '@nestjs/common';
import { USER_REPOSITORY, UserRepository } from './user.repository';

@Module({
  providers: [
    {
      provide: USER_REPOSITORY,
      useClass: UserRepository,
    },
  ],
  imports: [],
  exports: [USER_REPOSITORY],
})
export class UserRepositoryModule {}

USER_REPOSITORY 키워드를 Symbol 로 선언하고 프로바이더로서 등록합니다.

사실 굳이 UserRepositoryModule 모듈로 분리할 필요는 없고, UserModule 에서 바로 프로바이더로 등록해도 무방합니다.(의 예시처럼..)
하지만 나중에 의존성이 꼬이는 것을 방지하려면 모듈을 세세하게 나누는게 좋습니다.

1
2
3
4
5
6
7
8
// user.module.ts
    
@Module({
  imports: [UserRepositoryModule],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}
1
2
3
4
5
6
7
8
// user.service.ts
@Injectable()
export class UserService {
  constructor(
    @Inject(USER_REPOSITORY)
    private readonly UserRepository: UserRepository
  ){}
}

서비스에서 @Inject(USER_REPOSITORY) 데코레이터를 사용하여 UserRepository를 주입받습니다.
이때 심볼이 가리키는 값의 타입과, 데코레이터 이하의 변수의 타입이 일치해야 합니다.

@Inject() 을 사용해야 하는 이유

UserRepositoryModule 에서 프로바이더가 클래스 타입이 아닌 커스텀 토큰(심볼)으로 등록되었기 때문입니다.
github::nest/inject.decorator.ts

1
2
3
4
/**
 * @param token  lookup key for the provider to be injected (assigned to the constructor parameter).
 */
export declare function Inject<T = any>(token?: T): PropertyDecorator & ParameterDecorator;

왜 Symbol을 사용해야 하는가?

  1. 네임스페이스 충돌 방지:
    여러 모듈에서 동일한 이름의 프로바이더를 사용할 때 심볼을 사용하면 충돌을 방지할 수 있습니다.
    또, 라이브러리 내의 예약어와 겹치는 경우에 발생하는 에러를 방지할 수 있습니다.
  2. 유연성:
    동일한 인터페이스나 타입을 구현하는 여러 프로바이더를 필요에 따라 교체하거나 다르게 설정할 수 있습니다.
    단순히 클래스 이름 변경에도 유연하게 대응할 수 있습니다.

마무리

진짜 오랫동안 묵혀둔 글인데 마무리를 늦게 하게 되었다.. 정신차리자!ㅠㅠ

This post is licensed under CC BY 4.0 by the author.

Trending Tags