NestJS ConfigModule 환경 설정

NestJS ConfigModule이란?

애플리케이션의 환경 변수, 시크릿, 설정값을 체계적으로 관리하는 것은 12-Factor App의 핵심 원칙이다. NestJS의 @nestjs/config 패키지는 .env 파일 로딩, 환경별 설정 분리, 타입 안전 검증, 네임스페이스 분리를 하나의 모듈로 통합한다.

기본 설정과 .env 로딩

// 설치
// npm i @nestjs/config

// app.module.ts
import { ConfigModule } from '@nestjs/config';

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,           // 전역 모듈 (모든 모듈에서 주입 가능)
      envFilePath: [
        `.env.${process.env.NODE_ENV}`,  // .env.production 우선
        '.env',                           // 폴백
      ],
      cache: true,              // 환경 변수 캐싱 (성능 최적화)
      expandVariables: true,    // ${VAR} 참조 확장
    }),
  ],
})
export class AppModule {}

// .env 파일 예시
// DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
// REDIS_URL=redis://localhost:6379
// JWT_SECRET=super-secret-key
// API_BASE_URL=http://localhost:${PORT}/api  ← expandVariables

Zod 기반 환경 변수 검증

환경 변수 누락이나 잘못된 타입은 런타임 장애의 주범이다. 서버 시작 시 검증하면 배포 후 장애를 원천 차단할 수 있다. ConfigModule의 validate 옵션에 Zod 스키마를 연결하는 패턴이 가장 견고하다.

// env.validation.ts
import { z } from 'zod';

export const envSchema = z.object({
  // 서버
  NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),

  // 데이터베이스
  DATABASE_URL: z.string().url().startsWith('postgresql://'),
  DATABASE_POOL_MIN: z.coerce.number().int().min(1).default(2),
  DATABASE_POOL_MAX: z.coerce.number().int().min(1).default(10),

  // Redis
  REDIS_URL: z.string().url().startsWith('redis://'),

  // JWT
  JWT_SECRET: z.string().min(32, 'JWT_SECRET은 32자 이상이어야 합니다'),
  JWT_EXPIRES_IN: z.string().default('1h'),

  // 외부 API
  PAYMENT_API_URL: z.string().url(),
  PAYMENT_API_KEY: z.string().min(1),

  // 선택 (개발 환경에서만)
  DEBUG_SQL: z.coerce.boolean().default(false),
});

export type Env = z.infer<typeof envSchema>;

// validate 함수
export function validateEnv(config: Record<string, unknown>) {
  const result = envSchema.safeParse(config);

  if (!result.success) {
    const errors = result.error.issues
      .map(i => `  ${i.path.join('.')}: ${i.message}`)
      .join('n');
    throw new Error(`환경 변수 검증 실패:n${errors}`);
  }

  return result.data;
}

// app.module.ts에서 연결
ConfigModule.forRoot({
  isGlobal: true,
  validate: validateEnv,
})

NestJS Zod 스키마 검증 심화에서 다룬 Zod 패턴을 환경 변수 검증에도 동일하게 적용할 수 있다.

네임스페이스 설정: registerAs

설정값이 많아지면 플랫 구조로는 관리가 어렵다. registerAs로 논리적 네임스페이스를 나누면 각 모듈이 자신의 설정만 주입받을 수 있다.

// config/database.config.ts
import { registerAs } from '@nestjs/config';

export const databaseConfig = registerAs('database', () => ({
  url: process.env.DATABASE_URL,
  pool: {
    min: parseInt(process.env.DATABASE_POOL_MIN ?? '2'),
    max: parseInt(process.env.DATABASE_POOL_MAX ?? '10'),
  },
  debug: process.env.DEBUG_SQL === 'true',
}));

// config/redis.config.ts
export const redisConfig = registerAs('redis', () => ({
  url: process.env.REDIS_URL,
  ttl: parseInt(process.env.REDIS_DEFAULT_TTL ?? '3600'),
}));

// config/jwt.config.ts
export const jwtConfig = registerAs('jwt', () => ({
  secret: process.env.JWT_SECRET,
  expiresIn: process.env.JWT_EXPIRES_IN ?? '1h',
  refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN ?? '7d',
}));

// app.module.ts — 네임스페이스 등록
ConfigModule.forRoot({
  isGlobal: true,
  load: [databaseConfig, redisConfig, jwtConfig],
  validate: validateEnv,
})

타입 안전 설정 주입

네임스페이스 설정을 서비스에 주입할 때는 ConfigType@Inject를 조합하면 완전한 타입 안전성을 확보할 수 있다.

import { ConfigType } from '@nestjs/config';
import { jwtConfig } from './config/jwt.config';

@Injectable()
export class AuthService {
  constructor(
    // 타입 안전 주입: config.secret, config.expiresIn 자동완성
    @Inject(jwtConfig.KEY)
    private readonly config: ConfigType<typeof jwtConfig>,
  ) {}

  generateToken(userId: string): string {
    return this.jwtService.sign(
      { sub: userId },
      {
        secret: this.config.secret,         // ✅ 타입 안전
        expiresIn: this.config.expiresIn,   // ✅ 자동완성
      },
    );
  }
}

// 대안: ConfigService 직접 사용 (타입 안전성 낮음)
@Injectable()
export class LegacyService {
  constructor(private configService: ConfigService) {}

  getPort(): number {
    // get<T> 제네릭으로 타입 힌트
    return this.configService.get<number>('PORT', 3000);
  }

  getDatabaseUrl(): string {
    // 네임스페이스 접근
    return this.configService.get<string>('database.url')!;
  }
}

환경별 설정 파일 분리

개발·스테이징·프로덕션 환경마다 다른 설정이 필요할 때, 설정 팩토리 함수에서 환경을 분기하는 패턴이 깔끔하다.

// config/app.config.ts
export const appConfig = registerAs('app', () => {
  const env = process.env.NODE_ENV ?? 'development';

  const baseConfig = {
    name: 'MyApp',
    env,
    port: parseInt(process.env.PORT ?? '3000'),
  };

  const envConfigs = {
    development: {
      cors: { origin: '*' },
      logging: { level: 'debug', prettyPrint: true },
      swagger: { enabled: true },
    },
    production: {
      cors: { origin: ['https://app.example.com'] },
      logging: { level: 'info', prettyPrint: false },
      swagger: { enabled: false },
    },
    test: {
      cors: { origin: '*' },
      logging: { level: 'warn', prettyPrint: false },
      swagger: { enabled: false },
    },
  };

  return { ...baseConfig, ...envConfigs[env] };
});

비동기 설정과 외부 소스

Vault, AWS Secrets Manager, 외부 설정 서버에서 비동기로 설정을 가져오는 경우 forRootAsync를 사용한다.

// AWS Secrets Manager에서 시크릿 로딩
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

export const secretsConfig = registerAs('secrets', async () => {
  if (process.env.NODE_ENV === 'development') {
    return {
      dbPassword: process.env.DB_PASSWORD,
      apiKey: process.env.API_KEY,
    };
  }

  const client = new SecretsManagerClient({ region: 'ap-northeast-2' });
  const command = new GetSecretValueCommand({
    SecretId: 'prod/myapp/secrets',
  });

  const response = await client.send(command);
  return JSON.parse(response.SecretString!);
});

// Module에서 비동기 로딩
ConfigModule.forRoot({
  isGlobal: true,
  load: [databaseConfig, secretsConfig],  // async factory 자동 처리
})

커스텀 ConfigModule 패턴

라이브러리나 공유 모듈을 만들 때, 설정을 외부에서 주입받는 패턴은 NestJS 생태계의 표준이다. NestJS Dynamic Module 설계에서 다룬 forRoot/forRootAsync 패턴과 결합하면 재사용 가능한 설정 모듈을 만들 수 있다.

// 재사용 가능한 알림 모듈
export interface NotificationModuleOptions {
  slack: { webhookUrl: string; channel: string };
  email: { from: string; smtpHost: string };
}

@Module({})
export class NotificationModule {
  static forRootAsync(options: {
    imports?: any[];
    useFactory: (...args: any[]) => NotificationModuleOptions | Promise<NotificationModuleOptions>;
    inject?: any[];
  }): DynamicModule {
    return {
      module: NotificationModule,
      imports: options.imports ?? [],
      providers: [
        {
          provide: 'NOTIFICATION_OPTIONS',
          useFactory: options.useFactory,
          inject: options.inject ?? [],
        },
        NotificationService,
      ],
      exports: [NotificationService],
    };
  }
}

// 사용처: ConfigService에서 설정 주입
NotificationModule.forRootAsync({
  useFactory: (config: ConfigService) => ({
    slack: {
      webhookUrl: config.getOrThrow('SLACK_WEBHOOK_URL'),
      channel: config.getOrThrow('SLACK_CHANNEL'),
    },
    email: {
      from: config.getOrThrow('EMAIL_FROM'),
      smtpHost: config.getOrThrow('SMTP_HOST'),
    },
  }),
  inject: [ConfigService],
})

설정 변경 감지와 Hot Reload

// 개발 환경에서 .env 변경 감지
// (프로덕션에서는 재시작이 원칙)
import { watch } from 'fs';

@Injectable()
export class ConfigWatcher implements OnModuleInit {
  private readonly logger = new Logger(ConfigWatcher.name);

  onModuleInit() {
    if (process.env.NODE_ENV !== 'development') return;

    watch('.env', (eventType) => {
      if (eventType === 'change') {
        this.logger.warn('.env 파일 변경 감지 — 재시작 필요');
        // 또는 dotenv.config()로 동적 리로드
      }
    });
  }
}

// 설정값 검증 헬스체크
// 배포 후 필수 설정이 올바른지 확인
@Injectable()
export class ConfigHealthIndicator extends HealthIndicator {
  constructor(private config: ConfigService) { super(); }

  check(): HealthIndicatorResult {
    const required = ['DATABASE_URL', 'REDIS_URL', 'JWT_SECRET'];
    const missing = required.filter(k => !this.config.get(k));

    if (missing.length > 0) {
      return this.getStatus('config', false, { missing });
    }
    return this.getStatus('config', true);
  }
}

마무리

NestJS ConfigModule은 단순한 .env 로더를 넘어, Zod 검증·네임스페이스 분리·타입 안전 주입·외부 시크릿 연동까지 제공하는 완전한 설정 관리 솔루션이다. 서버 시작 시 환경 변수를 엄격하게 검증하고, registerAs로 모듈별 설정을 격리하는 것이 프로덕션 품질의 기본이다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux