모듈 참조 (Module Reference) — ModuleRef로 프로바이더를 동적으로 다루기
모듈 참조 (Module Reference) — ModuleRef로 프로바이더를 동적으로 다루기
Nest는 ModuleRef 클래스를 제공해서 내부 프로바이더 목록을 탐색하고, 주입 토큰을 조회 키로 사용해 어떤 프로바이더에든 참조를 얻을 수 있게 해줘요. ModuleRef 클래스는 또한 정적 프로바이더와 스코프 프로바이더를 모두 동적으로 인스턴스화하는 방법도 제공해요. 보통 의존성 주입은 프레임워크가 알아서 처리하지만, 런타임에 프로바이더를 찾거나 조건에 따라 다른 클래스를 동적으로 생성해야 할 때가 있어요. 그럴 때 ModuleRef가 필요한 거죠. 이번엔 ModuleRef를 주입하고 활용하는 핵심 방법을 차근히 살펴볼게요. 🗂️
본문
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);
}
}
이 기법을 사용하면 프레임워크 컨테이너 밖에서 다른 클래스를 조건부로 인스턴스화할 수 있어요.
더 알아보기
- NestJS 공식 문서 - Module reference
- 주입 스코프(Injection scopes) — 스코프와 지속 프로바이더
- 동적 모듈(Dynamic modules) — 모듈을 동적으로 구성하는 법
- 커스텀 프로바이더(Custom providers) — DI 시스템의 다양한 프로바이더 형태