본문으로 건너뛰기
개발 머꼬
개발 노트NestJS
hohyeon.dev21

환경마다 다른 설정을 모듈에 넘기려다 전역 변수를 쓸 뻔한 이야기

  • #Engineering Note
  • #NestJS

문제 발생

저장소 모듈이 환경마다 다른 버킷과 자격증명을 써야 했습니다. 처음에는 모듈 안에서 process.env를 직접 읽었는데, 테스트에서 값을 바꿀 수가 없었습니다.

@Module({
  providers: [{ provide: StorageService, useFactory: () => new S3Storage(process.env.BUCKET!) }],
})
export class StorageModule {}

원인 분석

모듈이 자기 설정을 스스로 읽으면 그 설정은 바깥에서 바꿀 수 없습니다. 테스트마다 환경변수를 갈아끼우는 방식은 병렬 실행에서 바로 깨집니다.

NestJS는 이 요구를 위해 동적 모듈을 제공합니다. 문서 정의는 이렇습니다 — 동적 모듈은 정적 모듈과 정확히 같은 속성에 module이라는 속성 하나가 더해진, 런타임에 만들어지는 모듈입니다. 그리고 모듈 옵션 객체의 모든 속성은 module을 제외하면 선택적입니다.

이름 규칙도 문서에 정리돼 있습니다.

  • register — 소비하는 모듈마다 각자의 설정으로 구성할 때
  • forRoot — 애플리케이션 수준에서 한 번 설정하고 여러 곳에서 재사용할 때
  • forFeatureforRoot 설정을 쓰되 개별 기능 모듈에 필요한 세부만 조정할 때

설정 자체가 다른 서비스에 의존할 때는 비동기 형태를 씁니다 — 문서가 안내하는 registerAsync/forRootAsync/forFeatureAsyncuseFactoryinject로 DI를 받습니다.

해결 방안

  1. 정적 팩토리 메서드로 설정을 받습니다.
@Module({})
export class StorageModule {
  static forRoot(options: StorageOptions): DynamicModule {
    return {
      module: StorageModule,
      providers: [
        { provide: STORAGE_OPTIONS, useValue: options },
        StorageService,
      ],
      exports: [StorageService],
    };
  }
}
  1. 설정이 다른 provider에 의존하면 비동기 형태를 씁니다.
StorageModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({ bucket: config.getOrThrow("BUCKET") }),
});
  1. 이름으로 의도를 드러냅니다. 앱 전체에 하나면 forRoot, 소비처마다 다르면 register입니다. 관례를 따르면 다음 사람이 문서를 읽지 않고도 사용법을 짐작할 수 있습니다.

  2. exports를 빠뜨리지 않습니다. 동적으로 만든 provider도 캡슐화 규칙은 같습니다 — 내보내지 않으면 다른 모듈에서 주입할 수 없습니다.

  3. 전역으로 만들 때는 신중합니다. global: true를 붙이면 어디서나 주입되지만, 의존 관계가 코드에서 사라져 추적이 어려워집니다. ConfigModule처럼 정말 앱 전체가 쓰는 것에만 씁니다.

  4. 테스트에서 값을 바꿔 끼웁니다. 이 구조의 실제 이득이 여기서 나옵니다 — StorageModule.forRoot({ bucket: "test" })로 환경변수 없이 테스트가 돌아갑니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.