NestJS에서 Joi 검증과 Config 객체를 하나로 합치기

인한별
인한별
조회 수25

일반적으로 NestJS에서 ConfigModule과 Joi를 함께 사용하면 다음과 같은 중복 작업이 발생합니다.

  • app.config.ts: 클래스 내부에 타입과 필드 정의
  • validation-schema.ts: Joi를 이용해 동일한 필드들에 대한 검증 로직 작성


이 방식은 환경 변수가 추가될 때마다 두 파일을 모두 수정해야 하며, 실수로 한 곳을 누락할 위험이 있습니다.

이를 해결하기 위해 데코레이터 하나로 필드 정의와 검증을 동시에 처리하는 구조를 설계했습니다.


핵심 아키텍처

  • Metadata Reflection: 클래스 필드에 @JoiValidate 데코레이터를 달아 Joi 스키마 정보를 메타데이터로 저장합니다.
  • Dynamic Schema Building: 모듈 초기화 단계에서 클래스에 저장된 메타데이터를 읽어와 하나의 Joi.object() 스키마를 동적으로 생성합니다.
  • Auto Mapping: 클래스 생성자에서 ConfigService를 통해 검증된 값을 자신의 필드에 자동으로 할당합니다.


커스텀 데코레이터 및 상수 (joi-validate.decorator.ts)

Reflect-metadata를 활용해 각 프로퍼티가 가져야 할 검증 규칙을 클래스 레벨의 맵에 저장합니다.

// joi-validate.decorator.ts
export function JoiValidate(schema: Joi.Schema) {
    return (target: object, propertyKey: string) => {
        // 기존에 저장된 스키마 맵을 가져오거나 새로 생성
        const schemas = (Reflect.getMetadata(JOI_SCHEMAS_KEY, target) as object) || {};
        schemas[propertyKey] = schema;
        // 메타데이터에 다시 저장
        Reflect.defineMetadata(JOI_SCHEMAS_KEY, schemas, target);
    };
}


공급원 (app-config.ts)

이제 이 파일 하나만 관리하면 됩니다. 타입 정의와 검증 규칙이 한곳에 모여 있습니다.

@Injectable()
export class AppConfig {
    // 서버 포트
    @JoiValidate(Joi.number().required())
    readonly PORT: number;

    constructor(private configService: ConfigService) {
        // 생성자에서 ConfigService의 값을 클래스 필드에 자동 바인딩
        const keys = Object.keys(this);
        keys.forEach(key => {
            (this as Record<string, unknown>)[key] = this.configService.get<unknown>(key);
        });
    }
}

생성자 로직을 통해 config.PORT처럼 직관적으로 환경 변수에 접근할 수 있으며, 타입 안전성도 보장됩니다.


동적 스키마 주입 (app-config.module.ts)

ConfigModule.forRoot 설정 시, 클래스 메타데이터로부터 스키마를 추출해 주입합니다.

ConfigModule.forRoot({
    validationSchema: (() => {
        // AppConfig 클래스 프로토타입에 저장된 모든 Joi 스키마를 객체로 변환
        const schemas = (Reflect.getMetadata(JOI_SCHEMAS_KEY, AppConfig.prototype) as object) || {};
        return Joi.object(schemas).options({ convert: true });
    })(),
}),


이 구조의 장점

  • 유지보수성 향상: 새로운 환경 변수를 추가할 때 AppConfig 클래스에 필드와 데코레이터만 추가하면 끝납니다.
  • 가독성: 각 변수가 어떤 제약 조건(필수 여부, 타입 등)을 가지는지 코드 상에서 직관적으로 확인할 수 있습니다.
  • 타입 안정성: 클래스 필드에 정의된 타입을 그대로 사용하므로 IDE의 자동 완성 기능을 100% 활용할 수 있습니다.

댓글 0

댓글은 회원만 작성할 수 있습니다.

로그인하고 댓글 달기
댓글을 불러오는 중입니다...