Nest.js Circular dependency

나주엽·2024년 7월 2일
0

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 를 사용해야 한다.
		
		// ...
	} 
}

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

따라서 OrderServiceProductService 내에서 사용해야 한다.

@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 는 하위 모듈을 합쳐서 새로운 모듈을 생성한다. ProductOrder 모듈을 함께 사용해서 새로운 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
  ) {}
  
	// ...
}

결론 (진짜 문제)

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

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

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

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

참고

0개의 댓글