우당탕탕 우리네 개발생활

[Nestjs] index.ts(barrel)는 Circular Dependency를 야기한다 본문

tech

[Nestjs] index.ts(barrel)는 Circular Dependency를 야기한다

미스터카멜레온 2024. 6. 1. 11:34

레거시 코드를 다루며 모듈의 Circular Dependency와 관련된 다소 희귀한(?) 케이스의 문제를 경험했다. 바로 barrel files와 관련된 이슈이다.

Circular Dependency와 관련된 내용은 공식문서를 통해서 이전에 공부했던 적이 있는데 그때는 유심히 보지 않았던 부분이 바로 위 문제의 원인이었다. 관련된 내용은 공식문서에 기재되어 있다.

Barrel

barrel이 뭔데? 해당 사이트에서는 다음과 같이 barrel의 정의에 대해 알려주고 있다.

배럴은 여러 모듈의 내보내기를 하나의 편리한 모듈로 롤업하는 방법입니다. 배럴 자체는 다른 모듈의 선택된 내보내기를 다시 내보내는 모듈 파일입니다.

A barrel is a way to rollup exports from several modules into a single convenient module. The barrel itself is a module file that re-exports selected exports of other modules.

설명에서 유추를 한 사람들도 있겠지만 흔히 사용되는 index.ts를 이용한 롤업방식이 위 barrel의 대표적인 예시에 해당되겠다. 경험상 barrel 패턴을 사용했을 때의 장점은 관련이 있는 모듈들을 한 곳에 묶음으로써 가져오기(import)를 깔끔하게 할 수 있다는 부분이 있었다. 지금까지는 큰 단점을 느끼지 못한 채 사용하고 있었는데 해당 이슈를 겪으면서 검색을 하다 보니 barrel의 단점들을 나타내는 아티클들을 많이 볼 수 있었다. 특히 해당 Github에 정리를 잘 해주셔서 참고를 했다.

Circular dependency와 Barrel의 연관성?

어찌 됐건 barrel에 대해서는 간략하게 알아봤다. 그래서 현재 문제인 Circular dependency와 barrel의 연관성이 뭐길래 문제가 생기는 건지 궁금하다. 우선 nestjs 공식문서는 다음과 같이 얘기하고 있다.

"배럴 파일"/index.ts 파일을 사용하여 묶은 파일들을 가져오는 경우에도 순환 종속성이 발생할 수 있습니다. module/provider 클래스의 경우 배럴 파일을 생략해야 합니다. 예를 들어, 배럴 파일과 동일한 디렉터리 내의 파일을 가져올 때 배럴 파일을 사용해서는 안 됩니다. 즉, cats/cats.controller는 cats/cats.service 파일을 가져오기 위해 cats를 가져오면 안 됩니다.

A circular dependency might also be caused when using "barrel files"/index.ts files to group imports. Barrel files should be omitted when it comes to module/provider classes. For example, barrel files should not be used when importing files within the same directory as the barrel file, i.e. cats/cats.controller should not import cats to import the cats/cats.service file.

 

아쉽지만 정확한 연관성을 찾을 순 없었다. 하지만 다음 코멘트의 내용과 같이 뭔가 barrel로 인해 파생되는 메타데이터의 오염이 Circular Dependency를 야기시키는 버그가 될 수 있겠다고 추측한 채 넘어갈 수밖에 없었다.

환경 재연

앞으로 설명에 사용될 코드 예시는 아래 Github에 있다.

https://github.com/mmmmicha/nestjs-circular-dependency-problem-with-barrel-files-playground

 

GitHub - mmmmicha/nestjs-circular-dependency-problem-with-barrel-files-playground

Contribute to mmmmicha/nestjs-circular-dependency-problem-with-barrel-files-playground development by creating an account on GitHub.

github.com

문제를 겪은 환경을 최대한 재연해보았다.

// package.json
"dependencies": {
    "@nestjs/common": "7.3.2",
    "@nestjs/core": "7.3.2",
    "@nestjs/platform-express": "7.3.2",
}

모듈 관계도

모듈 관계도

위 모듈 관계도를 봤을 때 큰 특이점은 없다. 적어도 양방향 참조가 일어나는 circular dependency의 여지는 없다는 말로 대체한다. (참고로 NestApplication의 오픈 소스를 확인해 보면 NestFactory를 통해 NestApplication이 init 될 때 graph 방식으로 root module(app.module)부터 모든 하위 module 노드들의 메타데이터가 중복되지 않게 하단 노드들부터 자료구조에 저장되고 이를 기반으로 module 내 provider나 controller 등의 요소들이 인스턴스화된다.)

디렉터리 구조

apps/scheduler
├── job
│   ├── index.ts
│   ├── job.module.ts
│   └── job.service.ts
└── src
    ├── app.controller.spec.ts
    ├── app.controller.ts
    ├── app.module.ts
    ├── app.service.ts
    └── main.ts
libs
└── common
    └── src
        ├── caching
        │   ├── caching.module.ts
        │   ├── caching.service.ts
        │   └── index.ts
        ├── common.module.ts
        ├── index.ts
        └── shared
            ├── index.ts
            ├── shared.module.ts
            ├── shared.service.ts
            └── test.handler.ts

위 모듈 관계도의 근간이 되는 파일들의 구조이다. 여기서 주목해야 할 부분은 줄곧 얘기해 온 디렉터리마다 존재하는 index.ts 파일들이다.

문제 해결

에러는 다음과 같이 발생했다.

Error: A circular dependency has been detected. Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()".

 정확히 어떤 배럴 파일이 문제가 되는지 알 수는 없었기에 가장 의심되는 파일인 common/index.ts부터 제거해 보기로 했다. 해당 코드는 아래와 같다.

// common/index.ts
export * from ./shared/index.ts
export * from ./caching/index.ts
export * from ./common.module

 

위 배럴 파일을 제거한 후 다시 프로젝트를 빌드하여 실행해 보았다. 배럴 파일 제거에 의해 1개 파일의 가져오기 정보를 수정해야 했다. 결과는 여전히 같았다.

Error: A circular dependency has been detected. Please, make sure that each side of a bidirectional relationships are decorated with "forwardRef()".

이번엔 shared/index.ts를 제거해 보기로 했다.

// shared/index.ts
export * from './shared.module';
export * from './shared.service';

 

제거 후 이전과 같이 프로젝트를 빌드하여 실행해 보았다. 배럴 파일 제거에 의해 2개 파일의 가져오기 정보를 수정해야 했다. 결과는 성공이었다. 또 다른 케이스로 shared/index.ts는 그대로 유지하면서 caching/index.ts를 삭제해 보기로 했다. 

// caching/index.ts
export * from './caching.service';
export * from './caching.module';

 

배럴 파일 제거에 의해 3개 파일의 가져오기 정보를 수정해야 했다. 결과는 실패였다.

 

마지막으로 common/index.ts에서 shared/index.ts와 관련된 부분만 제거하고 나머지 부분은 살린 채 shared/index.ts 파일만 삭제해 보기로 했다. 배럴 파일 제거에 의해 4개 파일의 가져오기 정보를 수정해야 했다. 결과는 성공이었다.

결론

- common/index.ts 삭제 -> 실패

- common/index.ts, shared/index.ts 삭제 -> 성공

- common/index.ts, caching/index.ts 삭제 -> 실패

- common/index.ts(shared관련된 부분만), shared/index.ts 삭제 -> 성공

 

처음 우선 가장 의심되는 common/index.ts 배럴 파일을 삭제하기를 시작으로 다양한 케이스들을 테스트해 보다가 현재 상황에서는 shared/index.ts가 직접적인 관련이 있다는 사실을 발견했다. 나는 정말 심플하게 환경을 재연해서 테스트를 했었기 때문에 적은 수의 파일들을 수정하게 되었는데, 실제로 이런 일이 방대한 현업 코드에서 발생한다면 아찔할 것이다.(참고로 나는 실제 문제를 해결하기 위해 21개의 파일을 수정했다.)

nestjs 공식문서에서도 문제 발생의 가능성을 직접적으로 언급한 만큼 provider와 controller 그리고 module은 배럴 패턴에 포함하지 않는 편이 좋겠다고 생각한다.

참조

- https://basarat.gitbook.io/typescript/main-1/barrel

- https://github.com/yeonjuan/dev-blog/blob/master/JavaScript/speeding-up-the-javascript-ecosystem-the-barrel-file-debacle.md#%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%97%90%EC%BD%94%EC%8B%9C%EC%8A%A4%ED%85%9C%EC%9D%98-%EC%86%8D%EB%8F%84-%ED%96%A5%EC%83%81---%EB%B0%B0%EB%9F%B4barrel-%ED%8C%8C%EC%9D%BC%EC%9D%98-%EB%8C%80%EC%8B%A4%ED%8C%A8

- https://github.com/nestjs/nest/issues/7290#issuecomment-866568498