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:
위의 에러와 공식 문서에 제시된 방법이다.
전방 참조, 선행 참조라는 이 유틸리티 기능을 이용해 아직 정의되지 않은 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
를 이용해 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 를 사용하면,
여러 모듈을 상위 모듈에서 사용한 위 이미지와 같은 구조라는 생각도 하게되었다.
어떤 방식으로 이를 해결해야 하나 오래 고민했다. 더 나은 방식을 사용해보려고 노력했다.
진짜 결론은, 설계가 더 중요하다. 구현보다는 설계에 더 투자해보자…