환경변수 이름을 잘못 적었는데 앱이 정상적으로 뜬 문제
문제 발생
새 환경에 배포했더니 앱은 정상적으로 떴는데, 첫 요청에서 DB 연결이 실패했습니다.
connect ECONNREFUSED 127.0.0.1:3306DATABASE_HOST 환경변수 이름을 DB_HOST로 잘못 적어둔 것이었습니다. process.env.DATABASE_HOST가 undefined였고, 그게 조용히 기본값으로 흘렀습니다.
원인 분석
문제는 실패 시점입니다. 설정이 잘못됐다는 사실이 부팅이 아니라 요청 처리 중에 드러났습니다. 그 사이 헬스체크는 통과했고, 배포는 성공으로 기록됐습니다.
NestJS 공식 문서가 이걸 표준 관행으로 정리합니다 — 필수 환경변수가 제공되지 않았거나 검증 규칙을 만족하지 않으면 애플리케이션 시작 중에 예외를 던지는 것이 표준 관행입니다.
이유도 분명합니다. 설정 오류를 요청 처리 중이 아니라 시작 시점에 잡으면 런타임 실패를 막고, 모든 의존성이 제대로 설정되었음을 보장하며, 환경변수가 개발과 다를 수 있는 운영 환경에서 디버깅이 훨씬 쉬워집니다.
해결 방안
- 스키마 검증을 겁니다. 이게 핵심입니다.
import { z } from "zod";
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: z.object({
NODE_ENV: z.enum(["development", "production"]).default("development"),
PORT: z.coerce.number().default(3000),
DATABASE_HOST: z.string().min(1),
DATABASE_PASSWORD: z.string().min(1),
}),
}),
],
})
export class AppModule {}이제 DATABASE_HOST가 없으면 부팅이 실패합니다. 잘못된 배포가 트래픽을 받지 못합니다.
-
isGlobal: true로 반복 import를 없앱니다. 문서의 안내대로 루트 모듈에서 한 번 로드하면 다른 모듈에서ConfigModule을 다시 가져오지 않아도 됩니다. -
ConfigService.get()으로 읽습니다.process.env를 직접 읽지 않습니다 — 검증을 우회하게 되고, 타입도 항상string | undefined입니다.
const host = this.configService.get<string>("DATABASE_HOST");
const port = this.configService.get<number>("database.port", 3306);infer: true를 쓰면 인터페이스 정의에서 타입이 자동 추론되어, 없는 속성을 컴파일 시점에 잡습니다.
- 설정을 namespace로 묶습니다.
registerAs()로 도메인별로 나누면 점 표기로 접근할 수 있고,ConfigType으로 타입까지 붙습니다.
export default registerAs("database", () => ({
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT ?? 3306),
}));- 파일 경로는
envFilePath로 지정합니다. 배열을 주면 앞에 있는 파일이 우선합니다.
envFilePath: [".env.development.local", ".env.development"]-
컨테이너에서는
ignoreEnvFile: true가 맞을 수 있습니다..env파일 없이 런타임 환경변수만 쓰는 배포에서는 파일 파싱을 아예 끕니다. -
.env를 커밋하지 않습니다..env.example에는 이름만 남기고 값은 placeholder로 둡니다. 검증 스키마가 있으면 예제 파일이 최신인지도 자연스럽게 드러납니다 — 스키마에 있는데 예제에 없는 키는 그대로 부팅 실패로 나타납니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.