Nest.js Circular dependency

나주엽·2024년 7월 2일

nest

목록 보기
3/3
post-thumbnail

순환 종속성이란?

A circular dependency occurs when two classes depend on each other.

순환 종속성은 두 클래스가 각자에 의존할 때 발생한다.

Nest.js 에서는 모듈과 모듈사이 혹은 프로바이더와 프로바이더 사이에서 발생할 수 있다.

문제

ProductModule 그리고 OrderModule 이 있다.

ProductModule 내에는 ProductService 가, OrderModule 내에는 OrderService 가 있다.

// product.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Product]),
    OrderModule, // ← 문제 발생 지점
  ],
  providers: [ProductService],
  controllers: [ProductController],
  exports: [ProductService],
})
export class ProductModule {}

// order.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Order]),
    Product Module, // ← 문제 발생 지점
  ],
  providers: [OrderService],
  controllers: [OrderController],
  exports: [OrderService],
})
export class OrderModule {}

무엇이 문제인가?

ProductService 의 구매 로직은 아래와 같다.

@Injectable
export class ProductService {
  constructor(
    private orderService: OrderService,
    @InjectRepository(Product) private productRepository: Repository<Product>,
  ) {}
  
	buyProduct = async (buyer: User, productId: number) => {
		const product = await this.productRepository.findOne({
      where: { id: productId },
      relations: ['seller'],
    })

    // ...

    const newOrder = await this.orderService.createOrder(...)
    // ProductService 내에서 OrderService 를 사용해야 한다.
		
		// ...
	} 
}

구매자가 어떤 제품을 구매하면, 해당 제품의 상태를 예약중 으로 변경하고, 새로운 주문을 만들어야 한다.

따라서 OrderService 를 ProductService 내에서 사용해야 한다.

@Injectable()
export class OrderService {
  constructor(
	  private productService: ProductService,
    @InjectRepository(Order) private orderRepository: Repository<Order>,
  ) {}
  
  approveOrder = async (orderId: number) => {
    // ...
    
    await this.productService.updateOrder(...)
    
    // ...
  }
}

판매자가 위에서 생성된 어떤 주문을 승인하는 상황이 발생한다고 가정하자.

OrderService 내의 AcceptOrder 가 호출될 것이다.

주문을 승인하면 해당 주문의 상태를 대기중 에서 승인됨 으로 변경하고, 해당 제품의 상태를 판매완료 로 변경해야 하므로, OrderService 내에서 ProductService 를 사용하게 된다.

→ 이렇게 순환 종속성이 발생하게 되었다.

`[Nest] 64392 - ERROR [ExceptionHandler] Nest cannot create the OrderModule instance.
The module at index [1] of the OrderModule "imports" array is undefined.

Potential causes:

해결하기

forwardRef

위의 에러와 공식 문서에 제시된 방법이다.

전방 참조, 선행 참조라는 이 유틸리티 기능을 이용해 아직 정의되지 않은 class 를 참조할 수 있게 한다.

나는 현재 OrderModule 내의 OrderService
그리고 ProductModule 내의 ProductService 구조이므로

Module forward reference 항목 또한 적용해야 한다.

이를 적용해 순환 종속성 문제를 해결해보자.

// product.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Product]),
    forwardRef(() => OrderModule),
  ],
  providers: [ProductService],
  controllers: [ProductController],
  exports: [ProductService],
})
export class ProductModule {}

// product.service.ts
@Injectable
export class ProductService {
  constructor(
    @Inject(forwardRef(() => OrderService))
    private orderService: OrderService,
    @InjectRepository(Product) private productRepository: Repository<Product>,
  ) {}
	
	// ...
}
// order.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Order]),
    forwardRef(() => ProductModule),
  ],
  providers: [OrderService],
  controllers: [OrderController],
  exports: [OrderService],
})
export class OrderModule {}

// order.service.ts
@Injectable()
export class OrderService {
  constructor(
    @Inject(forwardRef(() => ProductService))
	  private productService: ProductService,
    @InjectRepository(Order) private orderRepository: Repository<Order>,
  ) {}

	// ...
}

위와 같이 작성하면 Circular Dependency 문제를 해결할 수 있다.

ModuleRef

공식문서에 추가로 ModuleRef 를 이용해 DI container 로 부터 직접 프로바이더 인스턴스를 받아오는 방법이 소개되어 있다.

이 방법도 적용해보자.

// product.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Product]),
    // forwardRef(() => OrderModule),
  ],
  providers: [ProductService],
  controllers: [ProductController],
  // exports: [ProductService],
})
export class ProductModule {}

// product.service.ts
@Injectable
export class ProductService implements OnMoudleInit {
	private orderService: OrderService
	
  constructor(
	  private moduleRef: ModuleRef,
    @InjectRepository(Product) private productRepository: Repository<Product>,
  ) {}
  
  onMoudleInit() {
	  this.orderService = this.moduleRef.get(OrderService, { strict: false });
	}
	
	// ...
}
// order.module.ts
@Module({
  imports: [
    TypeOrmModule.forFeature([Order]),
    // forwardRef(() => ProductModule),
  ],
  providers: [OrderService],
  controllers: [OrderController],
  // exports: [OrderService],
})
export class OrderModule {}

// order.service.ts
@Injectable
export class OrderService implements OnMoudleInit {
	private productService: productService
	
  constructor(
	  private moduleRef: ModuleRef,
    @InjectRepository(Order) private productRepository: Repository<Order>,
  ) {}
  
  onMoudleInit() {
	  this.productService = this.moduleRef.get(ProductService, { strict: false });
	}
	
	// ...
}

코드를 이와 같이 수정해도 순환 종속성 문제를 해결할 수 있다.

위의 경우 두 프로바이더가 다른 모듈 내에 존재하기 때문에 moduleRef.get() 의 두 번째 인자로 { strict: false } 를 전달해 주어야 합니다.

공통 모듈

Nest 는 하위 모듈을 합쳐서 새로운 모듈을 생성한다. Product 와 Order 모듈을 함께 사용해서 새로운 CommonModule (공통 모듈)을 만들어서 사용할 수 있다.

// common.module.ts
@Module({
  imports: [
	  OrderModule,
	  ProductModule
  ],
  providers: [CommonService],
  controllers: [],
})
export class OrderModule {}

// common.service.ts
@Injectable
export class CommonService{
  constructor(
	  private orderService: OrderService
		private productService: ProductService
  ) {}
  
	// ...
}

결론 (진짜 문제)

forwardRef 와 ModuleRef 의 경우는 간단하고 빠르게 적용할 수 있지만, 결국 코드 복잡도를 높히게 되어 유지보수가 어려워진다고 한다. Nest.js 문서에는 하나의 테크닉으로 소개되고 있다.
리팩토링을 통해 공통 모듈을 작성하고 순환 종속성 문제를 해결하는 방식이 장기적으로는 더 좋은 방식이라고 한다.

나의 진짜 문제는 buyProduct 가 ProductService 내부에 있던 것이다.
하나의 관심사, 하나의 도메인을 기준으로 모듈을 구성하는 것이 맞다.
처음에는 구매는 제품 구매이니 ProductService 내에서 구현했지만, 실제 구매하는 행위는 주문을 생성하는 행위이다.
따라서, OrderModule 내에서 ProductModule 를 사용하는 것이 맞았을 거라는 생각이 들었다.

아니면, TypeORM 도 모듈을 참조한 것이니, OrderService 에서 직접 orderRepo 를 사용하면,
여러 모듈을 상위 모듈에서 사용한 위 이미지와 같은 구조라는 생각도 하게되었다.

어떤 방식으로 이를 해결해야 하나 오래 고민했다. 더 나은 방식을 사용해보려고 노력했다.
진짜 결론은, 설계가 더 중요하다. 구현보다는 설계에 더 투자해보자…

참고

0개의 댓글