본문 바로가기
개발 💻/NestJS

class-validator 의 @IsBoolean 데코레이터와 enableImplicitConversion 설정된 Transform 의 문제

by 감사로봇 2024. 2. 29.

개요

본 개요는 문제를 만나게 된 과정을 설명하고 있으므로, 이미 문제를 겪으신 분들은 생략하셔도 좋습니다.

 

NestJS에서는 애플리케이션에 필요한 설정(Configuration)들을 위해 @nestjs/config 패키지를 제공합니다. 환경변수 파일(대표적으로 .env 파일)에 기재된 환경변수(Environment variables)들은 위 패키지가 제공하는 ConfigModule 로 주입되며 애플리케이션 곳곳에서 이용될 수 있게 됩니다.

이 때, 파일에 기재되고 애플리케이션으로 불러온 환경변수가, 필수인데도 입력되지 않았거나 잘못된 포맷으로 입력되었는지 확인하지 않는다면 애플리케이션은 정상적으로 동작할 수 없습니다. 이러한 문제를 해결하기 위해 Joi 와 같은 패키지가 공식 문서를 통해 추천되고 있습니다. 해당 패키지는 ConfigModule.forRoot 모듈 주입에서 validationSchema 옵션으로 전달하여, 불러오는 환경변수의 타입이나 포맷, 값의 범위 등을 검사할 수 있게 합니다.

 

저는 예전부터 이 방식을 사용해 왔는데 Joi 패키지에 대한 의존성이 있다는 느낌이 들어 추가적인 패키지를 이용하는 대신, DTO 검사 등을 위해 이미 사용하고 있는 검증(Validation) 패키지인 class-validator 및 class-transformer 패키지를 기반으로 해당 환경변수 객체를 검사하는 방식으로 변경하고자 하였습니다. 해당 방식 또한 공식 문서에서 추천하고 있는 Custom Validation 방식입니다.

해당 방식은 특정 환경변수 그룹(도메인)을 Class로 만들고 class-validator 및 class-transformer 에서 제공하는 데코레이터들을 응용하여 환경변수들에 적용될 포맷을 지정합니다. 그리고 class-validator 에서 제공하는 validateSync 함수를 통해 해당 Class를 검사하여 올바른 환경변수가 입력되었는지 확인할 수 있게 합니다.

이 방식을 기반으로, 저는 @ConfigValidation 이라고 하는 클래스 데코레이터를 구현하여, validateSync 함수를 가지고 있는 검사함수를 Class 자체에 대입될 수 있도록 적용하였습니다. 그러면 Class가 선언 또는 생성될 때 클래스 데코레이터가 호출되며, 해당 클래스 데코레이터가 확장한 생성자(Constructor) 함수를 실행합니다.

 

결론적으로 @ConfigValidation 클래스 데코레이터가 확장한 생성자는 process.env 값을 해당 Class 를 기반으로 인스턴스화합니다. 그리고 validateSync 함수를 이용하여, 인스턴스 내부 멤버변수(환경변수)들이 올바른 포맷을 가지고 있는지 검사합니다. 검사 결과, 에러가 포함되었다면 해당 에러(validateSync 함수는 검증 결과 발생한 에러들을 ValidationError[] 배열로 반환합니다.)를 발생시키며, 에러가 발생하지 않으면 통과합니다. (생성자 함수이므로, 반환하는 값을 지정하면 process.env 에서 추출되어 생성된 해당 인스턴스를 반환할 수 있습니다.)

그 과정에서...

 

문제

데이터베이스의 환경변수들을 가지고 검사하는 DatabaseConfigValidation Class를 구현하고자 합니다.

데이터베이스를 위한 환경변수들을 아래와 같이 정의하였습니다.

데이터베이스 환경변수

데이터베이스의 환경변수 중 DB_SYNCDB_LOGGING 환경변수들은 Boolean으로써 true 또는 false 값을 가집니다.

 

DatabaseConfigValidation Class는 아래와 같이 정의될 수 있습니다.

데이터베이스 환경변수 Class

각 환경변수들이 존재해야 하며, 필요한 포맷들을 검사하도록 설정하였습니다. 이 때, DB_SYNCDB_LOGGING 환경변수들은 boolean 값을 가지기 때문에, @IsBoolean 데코레이터가 이용되었습니다.

 

해당 Class에 구현한 @ConfigValidation 데코레이터를 붙이도록 합니다. @ConfigValidation 데코레이터의 코드는 다음과 같습니다.

@ConfigValidation 데코레이터

 

해당 클래스 데코레이터는 클래스의 생성자를 확장(Extend)하여, 클래스 생성을 통해 실행될 수 있습니다. 개요에서 설명한 것과 같이 다음과 같은 로직을 가집니다.

  1. class-transformer 패키지가 지원하는 plainToInstance 함수는 두번째 인자에 대입되는 값을 첫번째 인자에 대입되는 클래스 형태로 인스턴스화 해줍니다. 이 과정에서 class-transformer 가 제공하는 Transform(값에 맞는 타입 변경)가 동반될 수 있습니다.
  2. class-validator 패키지가 지원하는 validateSync 함수는 해당 인스턴스에 포함된 각종 검증 데코레이터를 통해 각 멤버변수가 올바른 포맷을 가지고 있는지 검사합니다.
  3. 에러가 있는 경우, 에러를 발생시키며, 그렇지 않은 경우 생성자 함수의 반환값으로 대입된 인스턴스를 반환합니다.

 

위의 과정에서 plainToInstance 함수는 enableImplicitConversion 이라는 옵션이 대입되어 있습니다.

enableImplicitConversion 옵션

이 옵션을 true 로 설정하는 경우, class-transformer 기능에 의해 해당 값이 가질 수 있는 대상 값을 자동으로(암시적으로) 형 변환해줍니다. 이것을 사용하는 이유는 환경변수로 대입된 값들을 원하는 변수의 타입으로 자동 형 변환하기 위함입니다.

 

DB_PORT 환경변수는 숫자 타입을 가집니다.

앞서 설정한 DatabaseConfigValidation 클래스의 DB_PORT 변수는 데이터베이스가 가지는 포트번호를 나타내며, 숫자 타입으로 다루고자 합니다.

그러나 환경변수 파일에서 불러오는 모든 값들은 기본적으로 문자열(String) 타입을 가집니다. 따라서 별도의 문자열 - 숫자 타입 변환이 필요한데, 이를 자동으로 해주는 것이 class-transformer 의 역할입니다.

이를 통해, 환경변수에서 초기 작성되었던 값(예를 들어, DB_PORT=8000)은 문자열 "8000" 으로 대입되나, plainToInstance 함수를 통과함에 따라 숫자 8000 으로 변형되어 이용할 수 있게 됩니다.

 

그런데 이와 똑같은 과정에서 @IsBoolean 데코레이터와 boolean 타입을 가진 DB_LOGGING 환경변수는 계속해서 true 값만을 가지고 있는 문제를 발견하였습니다.

DB_LOGGING 변수의 데코레이터들

 

DatabaseConfigValidation Class에 @ConfigValidation 데코레이터를 붙인 뒤, 생성 및 로그를 출력하여 확인해 보았습니다.

DatabaseConfigValidation Class
대입되는 환경변수들
실행하는 코드

 

그 결과 출력된 값은 환경변수에서 설정한 false 값이 아닌, true 값이 나옵니다.

코드 실행 결과

 

이것은 @IsBooleanboolean 타입으로 설정된 DB_LOGGING 변수에 false 값이 대입되었지만, plainToInstance 값과 그 설정된 enableImplicitConversion 옵션으로 변경된 부울 값은 true 로 나온다는 의미입니다.

즉, enableImplicitConversion 옵션은 암시적 형 변환을 수행하면서, 대입된 변수가 문자열(String) 타입이고, 비어있지 않을 때, Boolean으로의 변환(Transform)을 시도한다면 이것들을 모두 true 로 변환한다는 것을 알 수 있습니다. (직관적으로 해석하면, 문자열이 존재하므로 해당 문자열은 '존재한다'로 인식하여 true 값이 되는 것입니다. 따라서 빈 값인 경우에는 false 로 변환됩니다.)

 

문제 해결

서두가 길었지만, 결론적으로는 class-transformer 의 자동 형 변환에서의 문제가 발생한 것이기 때문에 직접 형 변환을 수행해 주는 것으로 문제를 해결하였습니다.

class-transformer 의 @Transform 데코레이터는 사용 시 해당 멤버변수에 대한 강제 형 변환을 수행합니다. 이 데코레이터를 활용하여, 대입된 값을 실제 원하는 Boolean으로 변경될 수 있도록 하였습니다.

@Transform 데코레이터

 

@Transform 데코레이터에 입력되는 파라미터 값을 보면 다음과 같은 인터페이스 TransformFnParams 를 가지고 있습니다.

TransformFnParams 인터페이스

 

얼추 유추해 보기로는, 해당 파라미터로 대입되는 value 값이 해당 멤버변수(데코레이터가 적용된 변수)의 값일 것으로 예상됩니다. 데코레이터를 이용하여 해당 값을 출력해 봅니다.

@Transform 데코레이터의 적용
실행 결과

그러나 value 값은 true 로 나오고 있습니다. 이는 이미 앞선 @ConfigValidation 데코레이터에 의해 형 변환이 이루어진 이후라는 뜻입니다.

 

실제 DB_LOGGING 에 대입될 다른 값을 찾기 위해, 이번에는 obj 값을 확인해 보았더니, plainToInstance 값에 대입되었던 process.env 값이 전부 출력됩니다. (스크린샷은 생략하겠습니다.) 그러면 이제 실제 DB_LOGGING 값에 대한 접근을 수행해야 하므로, 이에 대한 키값을 알 수 있는 key 값을 이용하면 됩니다.

 

결론적으로, 다음과 같은 @Transform 데코레이터 적용을 통해, 강제 형 변환을 적용하였습니다.

@Transform 을 기반으로, boolean 타입으로의 강제 형 변환

 

이제 출력을 확인해 보면, 환경변수 파일에 기재하였던 false 값이 그대로 출력되는 것을 확인할 수 있습니다. 이제 @ConfigValidation 데코레이터를 사용할 수 있게 되었습니다.

코드 실행 결과

 

결론

발생했던 문제와 해결 방법을 정리하면 아래와 같습니다.

  1. class-transformer 의 plainToInstance 함수에서 enableImplicitConversion 옵션을 활성화하면, 암시적 형 변환이 일어납니다.
  2. plainToInstance 함수에서, 입력된 문자열(String)을 부울(Boolean) 값으로 변환하게 되면, 해당 문자열 값을 읽는 것이 아니라, 문자열의 존재(빈 값이 아닌지)에 따라 true 또는 false 로 자동 형 변환을 수행합니다.
  3. 환경변수를 검사하는 Class 및 데코레이터를 구현하여 class-validator 가 제공하는 validateSync 함수를 이용하려고 하였으나, 위 문제로 인해 실제 부울 값이 반영되지 않는 문제가 있었습니다.
  4. 이 문제를 해결하기 위해, 검사에서 사용될 변수를 다시 형 변환해주기 위하여 class-transformer 의 @Transform 데코레이터를 이용하였습니다.
  5. @Transform 데코레이터에서 입력된 원본 값으로 접근하여, 해당 값이 문자열 "true" 와 같은지 검사함으로써, 실제 입력된 환경변수가 제대로 반영될 수 있도록 하였습니다.

 

위 문제에 대한 해결 방법은 직관적으로 생각해서 적용한 방식이므로, 더 좋은 방법이 있을 수 있습니다. 특히, 매번 @IsBoolean 데코레이터를 사용하는 경우 (입력 문자열 값으로부터 부울 값으로의 형 변환이 필요한 경우), @Transform 데코레이터를 같이 붙여서 사용해야 한다는 단점이 있습니다. 이 부분을 해결하려면, 새로운 데코레이터로 정의해주는 것이 좋습니다. 기존의 @IsBoolean@Transform 데코레이터들을 하나의 데코레이터로 묶거나, 필요한 여러 가지 로직이나 자세한 옵션들을 동반하여 구현할 수 있겠습니다.