프로젝트 상담

INSIGHTS · Technology

NestJS 모듈 순환 의존성, 실무에서 발견·해결한 구조 개선 사례

BLICT · 2026. 9. 30. · 수정 2026. 9. 30.

실제 프로젝트에서 모듈 간 순환 의존성으로 인한 장애 발생 과정을 분석하고, 단계별 점검 및 의존성 분리 리팩토링 기준을 제시합니다.

NestJS 모듈 순환 의존성, 실무 장애 발생 과정

NestJS 기반 프로젝트에서 모듈 간 순환 의존성(Circular Dependency)은 개발 초기에는 쉽게 감지되지 않습니다. 의존성 주입이 잘 추상화되어 있고, NestJS가 순환 참조를 어느 정도 허용하기 때문입니다. 하지만 실제 서비스 트래픽이 증가하거나, 일부 모듈에 추가 기능이 도입되는 순간 순환 의존성이 치명적인 장애를 일으키는 경우가 있습니다.

실제 사례에서 장애는 다음과 같이 시작됐습니다. 특정 서비스 로직에서 두 모듈이 서로의 Provider를 필요로 하는데, 각 모듈에서 상대 모듈을 직접 imports로 선언하고 있었습니다. 개발 단계에서는 문제없이 작동했지만, 신규 Provider 추가 후 런타임에 NestJS DI 컨테이너가 의존성 그래프를 완성하지 못하면서, 앱이 부팅 단계에서 멈추는 현상이 발생했습니다. 초기에는 단순 코드 오타나 설정 오류로 오인해 원인 파악에만 수시간이 소요되었습니다.

장애를 유발하는 핵심 지점은, 서비스 간 경계가 불명확할 때 순환 의존성이 필연적으로 발생한다는 점입니다. 특히 도메인 모델이 복잡해질수록, 한 모듈이 다른 모듈의 비즈니스 로직을 직접 호출하는 패턴이 반복되면 순환 참조는 점차 심화됩니다.

순환 의존성 감지와 진단 절차

NestJS는 순환 의존성을 감지하면 경고 로그를 남기거나, 상황에 따라 앱 실행을 중단합니다. 하지만 실무에서는 경고 로그가 애매하게 출력되거나, 특정 Provider에서만 오류가 나타나는 등 진단이 쉽지 않습니다. 장애 초기에 무엇을 확인해야 할지 우선순위를 잡는 것이 핵심입니다.

첫 단계는 NestJS의 의존성 그래프를 시각화하는 것입니다. 공식적으로는 제공되지 않지만, 오픈소스 도구(nest-dependency-graph 등)를 활용하면 각 모듈 간 의존 관계를 그래프로 확인할 수 있습니다. 이 그래프에서 순환 고리가 발견되는 지점을 집중적으로 살펴야 합니다.

다음은 각 모듈의 imports와 providers 선언을 코드 레벨에서 점검하는 단계입니다. 실수로 상대 모듈 전체를 임포트하고 있지는 않은지, 특정 서비스만 필요할 때 전체 모듈을 의존성으로 끌어오고 있지는 않은지 살펴봅니다. 가장 흔한 패턴은 A모듈이 B모듈을, B모듈이 다시 A모듈을 imports로 선언하는 구조입니다.

// 순환 의존성 예시
@Module({
  imports: [BModule],
  providers: [AService],
})
export class AModule {}

@Module({
  imports: [AModule],
  providers: [BService],
})
export class BModule {}

위와 같은 패턴이 발견되면, 이후 리팩토링의 기준점이 됩니다.

순환 의존성의 구조적 원인 분석

실무에서 순환 의존성이 발생하는 구조적 원인은 대부분 모듈 경계 설계의 실패에 있습니다. 단순히 forwardRef()로 순환 참조를 우회하는 것은 일시적인 처방에 불과합니다. 실제로 장애가 발생한 사례에서는 다음과 같은 구조적 문제가 있었습니다.

첫 번째는 도메인 서비스가 서로의 내부 로직에 과도하게 의존하는 경우입니다. 예를 들어, 유저(User)와 주문(Order) 도메인이 있는데, 유저 서비스가 주문 상태를 직접 변경하고, 주문 서비스가 유저 상태를 직접 조작하는 구조였습니다. 이런 경우 각 도메인이 서로의 변경 내역에 민감해지고, 자연스럽게 순환 참조가 발생합니다.

두 번째는 공통 유틸리티 또는 헬퍼 서비스가 각 도메인 모듈에 중복 정의되어 있고, 이 유틸리티가 서로를 참조하는 패턴입니다. 예를 들어, Logger, Notification, FileUploader 등 범용 서비스를 각 도메인에 별도로 두고, 필요에 따라 서로 가져다 쓰는 경우입니다. 결국 공통 기능임에도 모듈 간 경계가 명확히 분리되지 않아 순환 의존성이 심화됩니다.

이러한 구조적 문제는 단순히 코드 일부를 수정해서 해결할 수 없습니다. 모듈 경계, 도메인 의존성, 공통 기능 분리 등 전반적인 설계 방향을 점검해야 합니다.

실무 리팩토링: 의존성 분리와 기준

장애를 근본적으로 해결하기 위해서는 순환 의존성을 야기하는 의존성 구조를 재설계해야 합니다. 실무에서는 다음과 같은 리팩토링 기준을 적용합니다.

첫째, 도메인 간 직접 의존을 피하고, 인터페이스(Interface)나 이벤트 발행(Event Emitter)로 간접 호출하도록 변경합니다. 예를 들어, 주문 서비스에서 유저 정보를 직접 참조하지 않고, 유저 서비스가 제공하는 인터페이스를 통해 필요한 정보만 얻도록 설계합니다.

둘째, 공통 서비스(유틸리티, 도우미)는 별도의 Shared 모듈로 분리합니다. 모든 도메인 모듈에서 필요한 공통 기능은 Shared 모듈에만 정의하고, 각 도메인 모듈에서는 Shared 모듈만 의존하도록 구조를 단순화합니다.

셋째, 불가피하게 일부 서비스만을 참조해야 할 때는 전체 모듈이 아니라 특정 Provider만 exports/imports 하도록 코드를 재구성합니다. 예를 들어, A모듈이 B모듈의 특정 서비스만 필요하다면, B모듈에서 해당 서비스만 exports로 내보내고, A모듈에서는 전체가 아닌 그 서비스만 imports로 받아옵니다.

넷째, 강한 결합이 필요한 경우에는 NestJS의 forwardRef()를 일시적으로 사용할 수 있으나, 이 방식은 임시 처방임을 명확히 인식하고 빠른 시일 내에 구조 개선을 계획해야 합니다.

마지막으로, 리팩토링 후에는 반드시 의존성 그래프를 재점검하고, 각 모듈 간 경계가 명확하게 분리됐는지 확인하는 절차가 필요합니다.

NestJS 모듈 순환 의존성, 점검·개선 체크리스트

  • 서비스 간 직접 참조가 아닌, 인터페이스 또는 이벤트 기반 호출 설계 여부 점검
  • 공통 유틸리티·헬퍼 서비스는 Shared 모듈로 통합·분리되어 있는지 확인
  • 모듈 간 imports는 전체 모듈 단위가 아닌, 필요한 Provider만 exports·imports하는지 점검
  • 순환 참조 발생 시 forwardRef() 사용은 일시적 조치임을 명확히 표시하고, 구조 개선 계획 수립
  • 리팩토링 후 의존성 그래프를 시각화하여, 순환 고리가 완전히 해소됐는지 마지막으로 점검

이 체크리스트를 바탕으로, NestJS 실무 개발에서 모듈 순환 의존성으로 인한 장애를 선제적으로 차단하고, 구조적으로 견고한 모듈 설계를 실현할 수 있습니다.

관련 글

NestJS 모듈 순환 의존성, 실무에서 발견·해결한 구조 개선 사례 | BLICT