이번 글에서는 NestJS에서 TypeORM을 연동하고, 환경변수를 안전하게 관리하기 위해 @nestjs/configJoi를 활용하여 유효성 검증을 추가하는 방법까지 실습해본 내용을 정리해보겠습니다.

1. 기본적인 TypeORM 연동 방법

처음에는 가장 단순한 방식으로 TypeORM을 연동해보았습니다. .env 파일에 정의된 환경변수를 process.env를 통해 직접 불러오고, 이를 TypeOrmModule.forRoot()에 주입하는 구조입니다.
TypeOrmModule.forRoot({
  type: process.env.DB_TYPE as 'postgres',
  host: process.env.DB_HOST,
  port: parseInt(process.env.DB_PORT as string),
  username: process.env.DB_USERNAME,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_DATABASE,
  entities: [],
  synchronize: true, // 개발 환경에서만 true
}),

이 설정만으로도 기본적인 DB 연결은 가능합니다. synchronize: true 옵션 덕분에 애플리케이션 실행 시 DB 테이블이 자동으로 생성되어, 개발 초기에는 매우 편리하게 사용할 수 있습니다.
하지만 이 방식에는 명확한 단점이 있습니다. 바로 환경변수에 대한 유효성 검증이 전혀 이루어지지 않는다는 점입니다. 예를 들어 .env 파일에서 포트 번호를 문자열이 아닌 잘못된 값으로 입력하거나, 필수 값 중 하나가 누락되어 있어도 NestJS는 애플리케이션을 실행하려고 시도합니다. 이로 인해 런타임에서 예기치 못한 오류가 발생할 수 있습니다.

2. 환경변수 유효성 검증을 위한 Joi 도입

이러한 문제를 해결하기 위해 NestJS에서는 @nestjs/config 모듈을 사용해 환경변수를 로드하고, Joi를 통해 유효성 검증을 수행할 수 있도록 지원합니다.
우선 ConfigModule을 전역으로 등록하고, validationSchema 옵션에 Joi.object()를 지정하여 각 환경변수의 타입, 허용 값, 필수 여부 등을 명시합니다.
ConfigModule.forRoot({
  isGlobal: true,
  validationSchema: Joi.object({
    ENV: Joi.string().valid('dev', 'prod').required(),
    DB_TYPE: Joi.string().valid('postgres').required(),
    DB_HOST: Joi.string().required(),
    DB_PORT: Joi.number().required(),
    DB_USERNAME: Joi.string().required(),
    DB_PASSWORD: Joi.string().required(),
    DB_DATABASE: Joi.string().required(),
  }),
}),

💡 왜 Joi를 써야 할까?

  • 유효하지 않은 값이 애플리케이션에 반영되는 것을 방지할 수 있습니다.
  • 실행 전에 오류를 사전에 인지할 수 있어, 런타임 에러를 줄일 수 있습니다.
  • 팀 프로젝트에서 .env 형식이나 값에 대한 일관성을 강제할 수 있습니다.
예를 들어 DB_PORT가 숫자가 아니거나 DB_TYPE이 허용된 값(postgres)이 아닐 경우, NestJS는 서버를 실행시키지 않고 에러를 발생시킵니다. 이는 실무에서 굉장히 중요한 안정성 확보 수단이 됩니다.

🔧 3. TypeORM과 ConfigService를 함께 사용하는 방법

환경변수를 안전하게 불러오기 위해서는 ConfigService를 통해 검증된 값을 가져와야 합니다. 이를 위해 TypeOrmModule.forRootAsync()를 사용하여, 비동기 방식으로 설정을 주입받도록 구조를 변경합니다.
TypeOrmModule.forRootAsync({
  useFactory: (configService: ConfigService) => ({
    type: configService.get<string>('DB_TYPE') as 'postgres',
    host: configService.get<string>('DB_HOST'),
    port: configService.get<number>('DB_PORT'),
    username: configService.get<string>('DB_USERNAME'),
    password: configService.get<string>('DB_PASSWORD'),
    database: configService.get<string>('DB_DATABASE'),
    entities: [],
    synchronize: true, // 개발 시에만 true
  }),
  inject: [ConfigService],
}),

여기서 중요한 포인트는 다음과 같습니다:
| 항목 | 설명 | | -------------- | ------------------------------------------------------------------ | | useFactory | 비동기 설정 함수로, DI를 통해 ConfigService를 주입받아 값을 설정 | | inject | useFactory에 사용할 의존성을 명시 | | forRootAsync | 비동기 설정이 필요할 때 사용하는 TypeORM 초기화 메서드 |
이 패턴은 추후 프로덕션 배포 환경에서 RDS, AWS Secrets Manager, 환경별 설정 분리 등을 도입할 때도 확장성과 안정성을 보장해줍니다.

예시 .env 파일

ENV=dev
DB_TYPE=postgres
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=yourpassword
DB_DATABASE=mydb


정리하며

이번 실습에서는 NestJS에서 TypeORM을 연동하는 가장 기본적인 방법부터, 환경변수의 유효성 검증을 추가한 안전한 설정 방식까지 함께 적용해보았습니다.
  • process.env를 직접 사용하는 방식은 간단하지만, 안정성이 떨어진다
  • @nestjs/config + Joi 조합을 사용하면, 환경변수를 사전에 검증할 수 있어 실무에서도 매우 유용하다
  • TypeOrmModule.forRootAsync를 사용하면, DI 기반으로 환경변수 설정을 안전하게 주입할 수 있다
NestJS의 강점 중 하나는 이러한 구성요소들을 매우 일관성 있게 구조화할 수 있다는 점입니다. 앞으로 실무 프로젝트에서도 이러한 구조를 바탕으로 환경 설정을 안전하게 유지할 수 있을 것입니다.
이후에는 entities에 실제 엔티티 클래스를 추가하거나, 환경별 설정 파일(.env.dev, .env.prod)을 분리하여 ConfigModule에서 동적으로 선택하는 방식도 적용해볼 수 있습니다.