모듈 참조 (Module Reference) — ModuleRef로 프로바이더를 동적으로 다루기

모듈 참조 (Module Reference) — ModuleRef로 프로바이더를 동적으로 다루기

Nest는 ModuleRef 클래스를 제공해서 내부 프로바이더 목록을 탐색하고, 주입 토큰을 조회 키로 사용해 어떤 프로바이더에든 참조를 얻을 수 있게 해줘요. ModuleRef 클래스는 또한 정적 프로바이더와 스코프 프로바이더를 모두 동적으로 인스턴스화하는 방법도 제공해요. 보통 의존성 주입은 프레임워크가 알아서 처리하지만, 런타임에 프로바이더를 찾거나 조건에 따라 다른 클래스를 동적으로 생성해야 할 때가 있어요. 그럴 때 ModuleRef가 필요한 거죠. 이번엔 ModuleRef를 주입하고 활용하는 핵심 방법을 차근히 살펴볼게요. 🗂️

출처: 공식문서 - Module reference

본문

ModuleRef는 일반적인 방식으로 클래스에 주입할 수 있어요.

@@filename(cats.service)
@Injectable()
export class CatsService {
  constructor(private moduleRef: ModuleRef) {}
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
  constructor(moduleRef) {
    this.moduleRef = moduleRef;
  }
}

💡 ModuleRef 클래스는 @nestjs/core 패키지에서 import 해요.

인스턴스 검색(Retrieving instances)

ModuleRef 인스턴스(이하 모듈 참조라고 부를게요)는 get() 메서드를 가져요. 기본적으로 이 메서드는 주입 토큰/클래스 이름을 사용해 현재 모듈에 등록·인스턴스화된 프로바이더, 컨트롤러, 또는 인젝터블(예: 가드, 인터셉터 등)을 반환해요. 인스턴스를 찾지 못하면 예외가 발생해요.

@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
  private service: Service;
  constructor(private moduleRef: ModuleRef) {}

  onModuleInit() {
    this.service = this.moduleRef.get(Service);
  }
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
  constructor(moduleRef) {
    this.moduleRef = moduleRef;
  }

  onModuleInit() {
    this.service = this.moduleRef.get(Service);
  }
}

⚠️ get() 메서드로는 스코프 프로바이더(일시적 또는 요청 스코프)를 검색할 수 없어요. 대신 아래에 설명된 기법을 사용해야 해요. 스코프를 제어하는 방법은 여기에서 배울 수 있어요.

전역 컨텍스트에서 프로바이더를 검색하려면(예: 다른 모듈에 주입된 프로바이더), get()의 두 번째 인자로 {{ '{' }} strict: false {{ '}' }} 옵션을 넘겨요.

this.moduleRef.get(Service, { strict: false });

스코프 프로바이더 해결(Resolving scoped providers)

스코프 프로바이더(일시적 또는 요청 스코프)를 동적으로 해결하려면 resolve() 메서드를 사용하고 프로바이더의 주입 토큰을 인자로 전달해요.

@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
  private transientService: TransientService;
  constructor(private moduleRef: ModuleRef) {}

  async onModuleInit() {
    this.transientService = await this.moduleRef.resolve(TransientService);
  }
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
  constructor(moduleRef) {
    this.moduleRef = moduleRef;
  }

  async onModuleInit() {
    this.transientService = await this.moduleRef.resolve(TransientService);
  }
}

resolve() 메서드는 프로바이더의 고유 인스턴스를 반환하며, 이 인스턴스는 고유한 DI 컨테이너 하위 트리에서 만들어져요. 각 하위 트리는 고유한 컨텍스트 식별자(context identifier) 를 가져요. 따라서 이 메서드를 두 번 이상 호출해 인스턴스 참조를 비교하면 같지 않다는 걸 볼 수 있어요.

@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
  constructor(private moduleRef: ModuleRef) {}

  async onModuleInit() {
    const transientServices = await Promise.all([
      this.moduleRef.resolve(TransientService),
      this.moduleRef.resolve(TransientService),
    ]);
    console.log(transientServices[0] === transientServices[1]); // false
  }
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
  constructor(moduleRef) {
    this.moduleRef = moduleRef;
  }

  async onModuleInit() {
    const transientServices = await Promise.all([
      this.moduleRef.resolve(TransientService),
      this.moduleRef.resolve(TransientService),
    ]);
    console.log(transientServices[0] === transientServices[1]); // false
  }
}

여러 resolve() 호출에 걸쳐 단일 인스턴스를 생성하고 같은 생성 DI 컨테이너 하위 트리를 공유하도록 하려면 resolve() 메서드에 컨텍스트 식별자를 전달하면 돼요. ContextIdFactory 클래스를 사용해 컨텍스트 식별자를 생성해요. 이 클래스는 적절한 고유 식별자를 반환하는 create() 메서드를 제공해요.

@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
  constructor(private moduleRef: ModuleRef) {}

  async onModuleInit() {
    const contextId = ContextIdFactory.create();
    const transientServices = await Promise.all([
      this.moduleRef.resolve(TransientService, contextId),
      this.moduleRef.resolve(TransientService, contextId),
    ]);
    console.log(transientServices[0] === transientServices[1]); // true
  }
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
  constructor(moduleRef) {
    this.moduleRef = moduleRef;
  }

  async onModuleInit() {
    const contextId = ContextIdFactory.create();
    const transientServices = await Promise.all([
      this.moduleRef.resolve(TransientService, contextId),
      this.moduleRef.resolve(TransientService, contextId),
    ]);
    console.log(transientServices[0] === transientServices[1]); // true
  }
}

💡 ContextIdFactory 클래스는 @nestjs/core 패키지에서 import 해요.

REQUEST 프로바이더 등록(Registering REQUEST provider)

ContextIdFactory.create()로 수동 생성한 컨텍스트 식별자는 Nest 의존성 주입 시스템에 의해 인스턴스화·관리되지 않는 DI 하위 트리를 나타내므로, 그 안에서 REQUEST 프로바이더는 undefined예요.

수동으로 만든 DI 하위 트리에 커스텀 REQUEST 객체를 등록하려면 ModuleRef#registerRequestByContextId() 메서드를 다음과 같이 사용해요.

const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId(/* YOUR_REQUEST_OBJECT */, contextId);

현재 하위 트리 가져오기(Getting current sub-tree)

때때로 요청 컨텍스트 안에서 요청 스코프 프로바이더의 인스턴스를 해결하고 싶을 수 있어요. CatsService가 요청 스코프이고, 요청 스코프 프로바이더로 표시된 CatsRepository 인스턴스도 해결하고 싶다고 해볼게요. 같은 DI 컨테이너 하위 트리를 공유하려면 (위에서처럼 ContextIdFactory.create() 함수로) 새 식별자를 생성하는 대신 현재 컨텍스트 식별자를 얻어야 해요. 현재 컨텍스트 식별자를 얻으려면 먼저 @Inject() 데코레이터를 사용해 요청 객체를 주입해요.

@@filename(cats.service)
@Injectable()
export class CatsService {
  constructor(
    @Inject(REQUEST) private request: Record<string, unknown>,
  ) {}
}
@@switch
@Injectable()
@Dependencies(REQUEST)
export class CatsService {
  constructor(request) {
    this.request = request;
  }
}

💡 요청 프로바이더에 대해 더 알아보려면 여기를 참고하세요.

이제 ContextIdFactory 클래스의 getByRequest() 메서드를 사용해 요청 객체를 기반으로 컨텍스트 id를 만들고, 이걸 resolve() 호출에 전달해요.

const contextId = ContextIdFactory.getByRequest(this.request);
const catsRepository = await this.moduleRef.resolve(CatsRepository, contextId);

커스텀 클래스 동적 인스턴스화(Instantiating custom classes dynamically)

이전에 프로바이더로 등록되지 않은 클래스를 동적으로 인스턴스화하려면 모듈 참조의 create() 메서드를 사용해요.

@@filename(cats.service)
@Injectable()
export class CatsService implements OnModuleInit {
  private catsFactory: CatsFactory;
  constructor(private moduleRef: ModuleRef) {}

  async onModuleInit() {
    this.catsFactory = await this.moduleRef.create(CatsFactory);
  }
}
@@switch
@Injectable()
@Dependencies(ModuleRef)
export class CatsService {
  constructor(moduleRef) {
    this.moduleRef = moduleRef;
  }

  async onModuleInit() {
    this.catsFactory = await this.moduleRef.create(CatsFactory);
  }
}

이 기법을 사용하면 프레임워크 컨테이너 밖에서 다른 클래스를 조건부로 인스턴스화할 수 있어요.

더 알아보기