πŸ“‘ 곡식 λ¬Έμ„œ λ²ˆμ—­/Nest.js

[Nest.js] Fundamentals - Circular dependency, Module reference

dev_writer 2025. 7. 19. 02:05

μˆœν™˜ μ˜μ‘΄μ„±

μˆœν™˜ μ˜μ‘΄μ„±μ€ 두 ν΄λž˜μŠ€κ°€ μ„œλ‘œλ₯Ό ν•„μš”λ‘œ ν•  λ•Œ λ°œμƒν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, 클래슀 Aκ°€ 클래슀 Bλ₯Ό ν•„μš”λ‘œ ν•˜κ³ , 클래슀 B도 클래슀 Aλ₯Ό ν•„μš”λ‘œ ν•˜λŠ” κ²½μš°μž…λ‹ˆλ‹€. Nestμ—μ„œλŠ” λͺ¨λ“ˆ κ°„ λ˜λŠ” ν”„λ‘œλ°”μ΄λ” 간에 μ΄λŸ¬ν•œ μˆœν™˜ μ˜μ‘΄μ„±μ΄ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€.

 

μˆœν™˜ μ˜μ‘΄μ„±μ€ κ°€λŠ₯ν•˜λ©΄ ν”Όν•˜λŠ” 것이 μ’‹μ§€λ§Œ, 항상 κ·Έλ ‡κ²Œ ν•  μˆ˜λŠ” μ—†μŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ 경우λ₯Ό μœ„ν•΄ NestλŠ” 두 κ°€μ§€ λ°©λ²•μœΌλ‘œ ν”„λ‘œλ°”μ΄λ” κ°„μ˜ μˆœν™˜ μ˜μ‘΄μ„±μ„ ν•΄κ²°ν•  수 μžˆλ„λ‘ μ§€μ›ν•©λ‹ˆλ‹€. 이 μž₯μ—μ„œλŠ” forwardRefλ₯Ό μ‚¬μš©ν•˜λŠ” 방법과, DI μ»¨ν…Œμ΄λ„ˆμ—μ„œ ModuleRef 클래슀λ₯Ό 톡해 ν”„λ‘œλ°”μ΄λ” μΈμŠ€ν„΄μŠ€λ₯Ό κ°€μ Έμ˜€λŠ” 방법을 μ„€λͺ…ν•©λ‹ˆλ‹€.

 

λ˜ν•œ, λͺ¨λ“ˆ κ°„μ˜ μˆœν™˜ μ˜μ‘΄μ„±μ„ ν•΄κ²°ν•˜λŠ” 방법도 μ„€λͺ…ν•©λ‹ˆλ‹€.

κ²½κ³ 
μˆœν™˜ μ˜μ‘΄μ„±μ€ index.ts와 같은 배럴 파일 (barrel file)을 μ‚¬μš©ν•˜μ—¬ importλ₯Ό κ·Έλ£Ήν™”ν•  λ•Œλ„ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. 특히 λͺ¨λ“ˆμ΄λ‚˜ ν”„λ‘œλ°”μ΄λ” ν΄λž˜μŠ€μ™€ κ΄€λ ¨ν•΄μ„œλŠ” 배럴 파일 μ‚¬μš©μ„ ν”Όν•΄μ•Ό ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, λ™μΌν•œ 디렉터리 λ‚΄μ—­ νŒŒμΌμ„ κ°€μ Έμ˜¬ λ•ŒλŠ” 배럴 νŒŒμΌμ„ μ‚¬μš©ν•˜μ§€ 말아야 ν•©λ‹ˆλ‹€. 즉, cats/cats.controller μ—μ„œ catsλ₯Ό 톡해 cats/cats.serviceλ₯Ό import ν•˜λŠ” 방식은 μ§€μ–‘ν•΄μ•Ό ν•©λ‹ˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ ν•΄λ‹Ή GitHub 이슈λ₯Ό μ°Έκ³ ν•˜μ„Έμš”.

μ „λ°© μ„ μ–Έ (Forward reference)

μ „λ°© 선언은 아직 μ •μ˜λ˜μ§€ μ•Šμ€ 클래슀λ₯Ό forwardRef() μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•˜μ—¬ μ°Έμ‘°ν•  수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, CatsService와 CommonServiceκ°€ μ„œλ‘œλ₯Ό μ˜μ‘΄ν•˜λŠ” 경우, μ–‘μͺ½ λͺ¨λ‘ @Inject()와 forwardRef()λ₯Ό ν•¨κ»˜ μ‚¬μš©ν•˜μ—¬ μˆœν™˜ μ˜μ‘΄μ„±μ„ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€. κ·Έλ ‡μ§€ μ•ŠμœΌλ©΄ ν•„μˆ˜ 메타데이터가 λͺ¨λ‘ μ€€λΉ„λ˜μ§€ μ•Šμ•„ Nestκ°€ 이듀을 μΈμŠ€ν„΄μŠ€ν™”ν•˜μ§€ λͺ»ν•©λ‹ˆλ‹€. λ‹€μŒμ€ κ·Έ μ˜ˆμ‹œμž…λ‹ˆλ‹€:

@Injectable()
export class CatsService {
  constructor(
    @Inject(forwardRef(() => CommonService))
    private commonService: CommonService,
  ) {}
}
힌트
forwardRef() ν•¨μˆ˜λŠ” @nestjs/common νŒ¨ν‚€μ§€μ—μ„œ import λ©λ‹ˆλ‹€.

 

이제 ν•œμͺ½ 관계λ₯Ό μ²˜λ¦¬ν–ˆμœΌλ―€λ‘œ, CommonServiceμ—μ„œλ„ λ™μΌν•œ λ°©μ‹μœΌλ‘œ μ²˜λ¦¬ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€:

@Injectable()
export class CommonService {
  constructor(
    @Inject(forwardRef(() => CatsService))
    private catsService: CatsService,
  ) {}
}
주의
μΈμŠ€ν„΄μŠ€ν™” μˆœμ„œλŠ” κ²°μ •λ˜μ–΄ μžˆμ§€ μ•ŠμœΌλ―€λ‘œ, μ–΄λ–€ μƒμ„±μžκ°€ λ¨Όμ € ν˜ΈμΆœλ˜λŠ”μ§€μ— μ˜μ‘΄ν•˜λŠ” μ½”λ“œλŠ” μž‘μ„±ν•˜μ§€ 말아야 ν•©λ‹ˆλ‹€. 특히 Scope.REQUEST λ²”μœ„λ₯Ό κ°€μ§„ ν”„λ‘œλ°”μ΄λ”κ°€ μˆœν™˜ μ˜μ‘΄μ„±μ— 포함될 경우, μ˜μ‘΄μ„±μ΄ μ •μ˜λ˜μ§€ μ•Šμ€ μƒνƒœκ°€ 될 수 μžˆμŠ΅λ‹ˆλ‹€. μžμ„Έν•œ λ‚΄μš©μ€ μ—¬κΈ°λ₯Ό μ°Έκ³ ν•˜μ„Έμš”.

ModuleRef λŒ€μ•ˆ

forwardRef()λ₯Ό μ‚¬μš©ν•˜λŠ” λŒ€μ‹ , μ½”λ“œλ₯Ό λ¦¬νŒ©ν„°λ§ ν•˜μ—¬ ModuleRef 클래슀λ₯Ό ν™œμš©ν•΄ μˆœν™˜ κ΄€κ³„μ˜ ν•œμͺ½μ—μ„œ ν”„λ‘œλ°”μ΄λ”λ₯Ό 직접 κ°€μ Έμ˜€λŠ” 방식도 μ‚¬μš©ν•  수 μžˆμŠ΅λ‹ˆλ‹€. ModuleRef μœ ν‹Έλ¦¬ν‹° ν΄λž˜μŠ€μ— λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ μ—¬κΈ°λ₯Ό μ°Έκ³ ν•˜μ„Έμš”.

λͺ¨λ“ˆ μ „λ°© μ„ μ–Έ

λͺ¨λ“ˆ κ°„μ˜ μˆœν™˜ μ˜μ‘΄μ„±μ„ ν•΄κ²°ν•˜λ €λ©΄, μ–‘μͺ½ λͺ¨λ“ˆμ˜ μ—°κ²°μ—μ„œ λ™μΌν•˜κ²Œ forwardRef() μœ ν‹Έλ¦¬ν‹° ν•¨μˆ˜λ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄:

@Module({
  imports: [forwardRef(() => CatsModule)],
})
export class CommonModule {}

 

이제 ν•œμͺ½ λͺ¨λ“ˆμ—μ„œ μ „λ°© 선언을 μ„€μ •ν–ˆμœΌλ―€λ‘œ, CatsModuleμ—μ„œλ„ λ™μΌν•œ λ°©μ‹μœΌλ‘œ μ„€μ •ν•΄ λ³΄κ² μŠ΅λ‹ˆλ‹€:

@Module({
  imports: [forwardRef(() => CommonModule)],
})
export class CatsModule {}

λͺ¨λ“ˆ μ°Έμ‘°

NestλŠ” ModuleRef 클래슀λ₯Ό μ œκ³΅ν•˜μ—¬ λ‚΄λΆ€ ν”„λ‘œλ°”μ΄λ” λͺ©λ‘μ„ νƒμƒ‰ν•˜κ³ , μ£Όμž… 토큰을 ν‚€λ‘œ μ‚¬μš©ν•˜μ—¬ μ›ν•˜λŠ” ν”„λ‘œλ°”μ΄λ”μ˜ μ°Έμ‘°λ₯Ό 얻을 수 μžˆλ„λ‘ ν•©λ‹ˆλ‹€. λ˜ν•œ ModuleRefλŠ” 정적 (static) ν”„λ‘œλ°”μ΄λ”μ™€ λ²”μœ„ (scope)κ°€ μ§€μ •λœ ν”„λ‘œλ°”μ΄λ” λͺ¨λ‘λ₯Ό λ™μ μœΌλ‘œ μΈμŠ€ν„΄μŠ€ν™”ν•˜λŠ” κΈ°λŠ₯도 μ œκ³΅ν•©λ‹ˆλ‹€. ModuleRefλŠ” 일반적인 λ°©μ‹μœΌλ‘œ ν΄λž˜μŠ€μ— μ£Όμž…ν•  수 μžˆμŠ΅λ‹ˆλ‹€:

@Injectable()
export class CatsService {
  constructor(private moduleRef: ModuleRef) {}
}
힌트
ModuleRef ν΄λž˜μŠ€λŠ” @nestjs/core νŒ¨ν‚€μ§€μ—μ„œ import λ©λ‹ˆλ‹€.

μΈμŠ€ν„΄μŠ€ κ°€μ Έμ˜€κΈ°

ModuleRef μΈμŠ€ν„΄μŠ€ (μ΄ν•˜ λͺ¨λ“ˆ μ°Έμ‘°)λŠ” get() λ©”μ„œλ“œλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€. 기본적으둜 이 λ©”μ„œλ“œλŠ” ν˜„μž¬ λͺ¨λ“ˆμ— λ“±λ‘λ˜μ–΄ μΈμŠ€ν„΄μŠ€ν™”λœ ν”„λ‘œλ°”μ΄λ”, 컨트둀러 λ˜λŠ” 인젝터블 (예: κ°€λ“œ, 인터셉터 λ“±)을 μ£Όμž… 토큰 λ˜λŠ” 클래슀 이름을 μ‚¬μš©ν•˜μ—¬ λ°˜ν™˜ν•©λ‹ˆλ‹€. λ§Œμ•½ μΈμŠ€ν„΄μŠ€λ₯Ό μ°Ύμ§€ λͺ»ν•˜λ©΄ μ˜ˆμ™Έκ°€ λ°œμƒν•©λ‹ˆλ‹€.

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

  onModuleInit() {
    this.service = this.moduleRef.get(Service);
  }
}
주의
get() λ©”μ„œλ“œλŠ” λ²”μœ„κ°€ μ§€μ •λœ ν”„λ‘œλ°”μ΄λ” (예: transient λ˜λŠ” request λ²”μœ„)λ₯Ό κ°€μ Έμ˜¬ 수 μ—†μŠ΅λ‹ˆλ‹€. μ΄λŸ¬ν•œ κ²½μš°μ—λŠ” μ•„λž˜μ— μ„€λͺ…λœ 기법을 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€. λ²”μœ„ μ œμ–΄μ— λŒ€ν•œ μžμ„Έν•œ λ‚΄μš©μ€ μ—¬κΈ°λ₯Ό μ°Έκ³ ν•˜μ„Έμš”.

 

μ „μ—­ μ»¨ν…μŠ€νŠΈμ—μ„œ ν”„λ‘œλ°”μ΄λ”λ₯Ό κ°€μ Έμ˜€λ €λ©΄ (예: ν•΄λ‹Ή ν”„λ‘œλ°”μ΄λ”κ°€ λ‹€λ₯Έ λͺ¨λ“ˆμ—μ„œ μ£Όμž…λœ 경우), get() λ©”μ„œλ“œμ˜ 두 번째 인자둜 { strict: false } μ˜΅μ…˜μ„ μ „λ‹¬ν•˜λ©΄ λ©λ‹ˆλ‹€.

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

λ²”μœ„κ°€ μ§€μ •λœ ν”„λ‘œλ°”μ΄λ” ν•΄κ²°ν•˜κΈ°

transient λ˜λŠ” request λ²”μœ„μ˜ ν”„λ‘œλ°”μ΄λ”λ₯Ό λ™μ μœΌλ‘œ ν•΄κ²°ν•˜λ €λ©΄, ν•΄λ‹Ή ν”„λ‘œλ°”μ΄λ”μ˜ μ£Όμž… 토큰을 인자둜 μ „λ‹¬ν•˜μ—¬ resolve() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€.

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

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

 

resolve() λ©”μ„œλ“œλŠ” DI μ»¨ν…Œμ΄λ„ˆμ˜ ν•˜μœ„ νŠΈλ¦¬λ‘œλΆ€ν„° ν”„λ‘œλ°”μ΄λ”μ˜ κ³ μœ ν•œ μΈμŠ€ν„΄μŠ€λ₯Ό λ°˜ν™˜ν•©λ‹ˆλ‹€. 각 ν•˜μœ„ νŠΈλ¦¬λŠ” κ³ μœ ν•œ μ»¨ν…μŠ€νŠΈ μ‹λ³„μžλ₯Ό κ°€μ§€λ―€λ‘œ, 이 λ©”μ„œλ“œλ₯Ό μ—¬λŸ¬ 번 ν˜ΈμΆœν•˜λ©΄ 맀번 μ„œλ‘œ λ‹€λ₯Έ μΈμŠ€ν„΄μŠ€κ°€ μƒμ„±λ˜μ–΄ μΈμŠ€ν„΄μŠ€ μ°Έμ‘°λ₯Ό 비ꡐ할 경우 λ™μΌν•˜μ§€ μ•ŠμŒμ„ μ•Œ 수 μžˆμŠ΅λ‹ˆλ‹€.

@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
  }
}

 

μ—¬λŸ¬ 번의 resolve() ν˜ΈμΆœμ—μ„œλ„ 단일 μΈμŠ€ν„΄μŠ€λ₯Ό μƒμ„±ν•˜κ³ , λ™μΌν•œ DI μ»¨ν…Œμ΄λ„ˆ ν•˜μœ„ 트리λ₯Ό κ³΅μœ ν•˜λ„λ‘ ν•˜λ €λ©΄, resolve() λ©”μ„œλ“œμ— μ»¨ν…μŠ€νŠΈ μ‹λ³„μžλ₯Ό μ „λ‹¬ν•˜λ©΄ λ©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ ContextIdFactory 클래슀λ₯Ό μ‚¬μš©ν•˜μ—¬ μ»¨ν…μŠ€νŠΈ μ‹λ³„μžλ₯Ό 생성할 수 μžˆμŠ΅λ‹ˆλ‹€. 이 ν΄λž˜μŠ€λŠ” μ μ ˆν•œ 고유 μ‹λ³„μžλ₯Ό λ°˜ν™˜ν•˜λŠ” create() λ©”μ„œλ“œλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.

@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
  }
}
힌트
ContextIdFactory ν΄λž˜μŠ€λŠ” @nestjs/core νŒ¨ν‚€μ§€μ—μ„œ import λ©λ‹ˆλ‹€.

REQUEST ν”„λ‘œλ°”μ΄λ” λ“±λ‘ν•˜κΈ°

ContextIdFactory.create()둜 μˆ˜λ™ μƒμ„±ν•œ μ»¨ν…μŠ€νŠΈ μ‹λ³„μžλŠ” Nest의 DI μ‹œμŠ€ν…œμ— μ˜ν•΄ μΈμŠ€ν„΄μŠ€ν™”λ˜κ±°λ‚˜ κ΄€λ¦¬λ˜μ§€ μ•ŠκΈ° λ•Œλ¬Έμ—, ν•΄λ‹Ή DI ν•˜μœ„ 트리 λ‚΄μ—μ„œλŠ” REQUEST ν”„λ‘œλ°”μ΄λ”κ°€ μ •μ˜λ˜μ–΄ μžˆμ§€ μ•ŠμŠ΅λ‹ˆλ‹€.

 

μ΄λŸ¬ν•œ μˆ˜λ™ 생성 DI ν•˜μœ„ νŠΈλ¦¬μ— μ‚¬μš©μž μ •μ˜ REQUEST 객체λ₯Ό λ“±λ‘ν•˜λ €λ©΄, ModuleRef#registerRequestByContextId() λ©”μ„œλ“œλ₯Ό λ‹€μŒκ³Ό 같이 μ‚¬μš©ν•΄μ•Ό ν•©λ‹ˆλ‹€:

const contextId = ContextIdFactory.create();
this.moduleRef.registerRequestByContextId(/* λ‹Ήμ‹ μ˜_REQUEST_객체 */, contextId);

ν˜„μž¬ ν•˜μœ„ 트리 κ°€μ Έμ˜€κΈ°

κ²½μš°μ— 따라 μš”μ²­ μ»¨ν…μŠ€νŠΈ λ‚΄μ—μ„œ request λ²”μœ„μ˜ ν”„λ‘œλ°”μ΄λ” μΈμŠ€ν„΄μŠ€λ₯Ό ν•΄κ²°ν•΄μ•Ό ν•  수 μžˆμŠ΅λ‹ˆλ‹€. 예λ₯Ό λ“€μ–΄, CatsServiceκ°€ request λ²”μœ„λ‘œ μ§€μ •λ˜μ–΄ 있고, λ™μΌν•˜κ²Œ request λ²”μœ„μΈ CatsRepository μΈμŠ€ν„΄μŠ€λ₯Ό ν•΄κ²°ν•˜κ³  μ‹Άλ‹€κ³  κ°€μ •ν•΄ λ΄…μ‹œλ‹€. λ™μΌν•œ DI μ»¨ν…Œμ΄λ„ˆ ν•˜μœ„ 트리λ₯Ό κ³΅μœ ν•˜λ €λ©΄, ContextIdFactory.create()와 같이 μƒˆλ‘œμš΄ μ»¨ν…μŠ€νŠΈ μ‹λ³„μžλ₯Ό μƒμ„±ν•˜λŠ” λŒ€μ‹  ν˜„μž¬ μ»¨ν…μŠ€νŠΈ μ‹λ³„μžλ₯Ό μ–»μ–΄μ•Ό ν•©λ‹ˆλ‹€. 이λ₯Ό μœ„ν•΄ λ¨Όμ € @Inject() λ°μ½”λ ˆμ΄ν„°λ₯Ό μ‚¬μš©ν•˜μ—¬ request 객체λ₯Ό μ£Όμž…ν•΄μ•Ό ν•©λ‹ˆλ‹€.

@Injectable()
export class CatsService {
  constructor(
    @Inject(REQUEST) private request: Record<string, unknown>,
  ) {}
}
힌트
request ν”„λ‘œλ°”μ΄λ”μ— λŒ€ν•œ λ‚΄μš©μ€ μ—¬κΈ°μ—μ„œ 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.

 

이제 ContextIdFactory 클래슀의 getByRequest() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ ν•΄λ‹Ή μš”μ²­ 객체λ₯Ό 기반으둜 μ»¨ν…μŠ€νŠΈ IDλ₯Ό μƒμ„±ν•˜κ³ , 이λ₯Ό resolve() 호좜 μ‹œ μ „λ‹¬ν•˜λ©΄ λ©λ‹ˆλ‹€:

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

μ‚¬μš©μž μ •μ˜ 클래슀 λ™μž‘ μΈμŠ€ν„΄μŠ€ν™”

이전에 ν”„λ‘œλ°”μ΄λ”λ‘œ λ“±λ‘λ˜μ§€ μ•Šμ€ 클래슀λ₯Ό λ™μ μœΌλ‘œ μΈμŠ€ν„΄μŠ€ν™”ν•˜λ €λ©΄, ModuleRef의 create() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜λ©΄ λ©λ‹ˆλ‹€.

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

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

 

이 기법을 μ‚¬μš©ν•˜λ©΄ ν”„λ ˆμž„μ›Œν¬ μ»¨ν…Œμ΄λ„ˆ μ™ΈλΆ€μ—μ„œ 쑰건에 따라 μ„œλ‘œ λ‹€λ₯Έ 클래슀λ₯Ό λ™μ μœΌλ‘œ μΈμŠ€ν„΄μŠ€ν™”ν•  수 μžˆμŠ΅λ‹ˆλ‹€.

Reference