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로 모듈별 설정을 격리하는 것이 프로덕션 품질의 기본이다.