환경마다 다른 설정을 모듈에 넘기려다 전역 변수를 쓸 뻔한 이야기
문제 발생
저장소 모듈이 환경마다 다른 버킷과 자격증명을 써야 했습니다. 처음에는 모듈 안에서 process.env를 직접 읽었는데, 테스트에서 값을 바꿀 수가 없었습니다.
@Module({
providers: [{ provide: StorageService, useFactory: () => new S3Storage(process.env.BUCKET!) }],
})
export class StorageModule {}원인 분석
모듈이 자기 설정을 스스로 읽으면 그 설정은 바깥에서 바꿀 수 없습니다. 테스트마다 환경변수를 갈아끼우는 방식은 병렬 실행에서 바로 깨집니다.
NestJS는 이 요구를 위해 동적 모듈을 제공합니다. 문서 정의는 이렇습니다 — 동적 모듈은 정적 모듈과 정확히 같은 속성에 module이라는 속성 하나가 더해진, 런타임에 만들어지는 모듈입니다. 그리고 모듈 옵션 객체의 모든 속성은 module을 제외하면 선택적입니다.
이름 규칙도 문서에 정리돼 있습니다.
register— 소비하는 모듈마다 각자의 설정으로 구성할 때forRoot— 애플리케이션 수준에서 한 번 설정하고 여러 곳에서 재사용할 때forFeature—forRoot설정을 쓰되 개별 기능 모듈에 필요한 세부만 조정할 때
설정 자체가 다른 서비스에 의존할 때는 비동기 형태를 씁니다 — 문서가 안내하는 registerAsync/forRootAsync/forFeatureAsync가 useFactory와 inject로 DI를 받습니다.
해결 방안
- 정적 팩토리 메서드로 설정을 받습니다.
@Module({})
export class StorageModule {
static forRoot(options: StorageOptions): DynamicModule {
return {
module: StorageModule,
providers: [
{ provide: STORAGE_OPTIONS, useValue: options },
StorageService,
],
exports: [StorageService],
};
}
}- 설정이 다른 provider에 의존하면 비동기 형태를 씁니다.
StorageModule.forRootAsync({
inject: [ConfigService],
useFactory: (config: ConfigService) => ({ bucket: config.getOrThrow("BUCKET") }),
});-
이름으로 의도를 드러냅니다. 앱 전체에 하나면
forRoot, 소비처마다 다르면register입니다. 관례를 따르면 다음 사람이 문서를 읽지 않고도 사용법을 짐작할 수 있습니다. -
exports를 빠뜨리지 않습니다. 동적으로 만든 provider도 캡슐화 규칙은 같습니다 — 내보내지 않으면 다른 모듈에서 주입할 수 없습니다. -
전역으로 만들 때는 신중합니다.
global: true를 붙이면 어디서나 주입되지만, 의존 관계가 코드에서 사라져 추적이 어려워집니다.ConfigModule처럼 정말 앱 전체가 쓰는 것에만 씁니다. -
테스트에서 값을 바꿔 끼웁니다. 이 구조의 실제 이득이 여기서 나옵니다 —
StorageModule.forRoot({ bucket: "test" })로 환경변수 없이 테스트가 돌아갑니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.