개요
본 개요는 문제를 만나게 된 과정을 설명하고 있으므로, 이미 문제를 겪으신 분들은 생략하셔도 좋습니다.
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_SYNC` 와 `DB_LOGGING` 환경변수들은 Boolean으로써 `true` 또는 `false` 값을 가집니다.
`DatabaseConfigValidation` Class는 아래와 같이 정의될 수 있습니다.
각 환경변수들이 존재해야 하며, 필요한 포맷들을 검사하도록 설정하였습니다. 이 때, `DB_SYNC` 와 `DB_LOGGING` 환경변수들은 `boolean` 값을 가지기 때문에, `@IsBoolean` 데코레이터가 이용되었습니다.
해당 Class에 구현한 `@ConfigValidation` 데코레이터를 붙이도록 합니다. `@ConfigValidation` 데코레이터의 코드는 다음과 같습니다.
해당 클래스 데코레이터는 클래스의 생성자를 확장(Extend)하여, 클래스 생성을 통해 실행될 수 있습니다. 개요에서 설명한 것과 같이 다음과 같은 로직을 가집니다.
- class-transformer 패키지가 지원하는 `plainToInstance` 함수는 두번째 인자에 대입되는 값을 첫번째 인자에 대입되는 클래스 형태로 인스턴스화 해줍니다. 이 과정에서 class-transformer 가 제공하는 Transform(값에 맞는 타입 변경)가 동반될 수 있습니다.
- class-validator 패키지가 지원하는 `validateSync` 함수는 해당 인스턴스에 포함된 각종 검증 데코레이터를 통해 각 멤버변수가 올바른 포맷을 가지고 있는지 검사합니다.
- 에러가 있는 경우, 에러를 발생시키며, 그렇지 않은 경우 생성자 함수의 반환값으로 대입된 인스턴스를 반환합니다.
위의 과정에서 `plainToInstance` 함수는 `enableImplicitConversion` 이라는 옵션이 대입되어 있습니다.
이 옵션을 `true` 로 설정하는 경우, class-transformer 기능에 의해 해당 값이 가질 수 있는 대상 값을 자동으로(암시적으로) 형 변환해줍니다. 이것을 사용하는 이유는 환경변수로 대입된 값들을 원하는 변수의 타입으로 자동 형 변환하기 위함입니다.
앞서 설정한 `DatabaseConfigValidation` 클래스의 `DB_PORT` 변수는 데이터베이스가 가지는 포트번호를 나타내며, 숫자 타입으로 다루고자 합니다.
그러나 환경변수 파일에서 불러오는 모든 값들은 기본적으로 문자열(String) 타입을 가집니다. 따라서 별도의 문자열 - 숫자 타입 변환이 필요한데, 이를 자동으로 해주는 것이 class-transformer 의 역할입니다.
이를 통해, 환경변수에서 초기 작성되었던 값(예를 들어, `DB_PORT=8000`)은 문자열 `"8000"` 으로 대입되나, `plainToInstance` 함수를 통과함에 따라 숫자 `8000` 으로 변형되어 이용할 수 있게 됩니다.
그런데 이와 똑같은 과정에서 `@IsBoolean` 데코레이터와 `boolean` 타입을 가진 `DB_LOGGING` 환경변수는 계속해서 `true` 값만을 가지고 있는 문제를 발견하였습니다.
`DatabaseConfigValidation` Class에 `@ConfigValidation` 데코레이터를 붙인 뒤, 생성 및 로그를 출력하여 확인해 보았습니다.
그 결과 출력된 값은 환경변수에서 설정한 `false` 값이 아닌, `true` 값이 나옵니다.
이것은 `@IsBoolean` 및 `boolean` 타입으로 설정된 `DB_LOGGING` 변수에 `false` 값이 대입되었지만, `plainToInstance` 값과 그 설정된 `enableImplicitConversion` 옵션으로 변경된 부울 값은 `true` 로 나온다는 의미입니다.
즉, `enableImplicitConversion` 옵션은 암시적 형 변환을 수행하면서, 대입된 변수가 문자열(String) 타입이고, 비어있지 않을 때, Boolean으로의 변환(Transform)을 시도한다면 이것들을 모두 `true` 로 변환한다는 것을 알 수 있습니다. (직관적으로 해석하면, 문자열이 존재하므로 해당 문자열은 '존재한다'로 인식하여 `true` 값이 되는 것입니다. 따라서 빈 값인 경우에는 `false` 로 변환됩니다.)
문제 해결
서두가 길었지만, 결론적으로는 class-transformer 의 자동 형 변환에서의 문제가 발생한 것이기 때문에 직접 형 변환을 수행해 주는 것으로 문제를 해결하였습니다.
class-transformer 의 `@Transform` 데코레이터는 사용 시 해당 멤버변수에 대한 강제 형 변환을 수행합니다. 이 데코레이터를 활용하여, 대입된 값을 실제 원하는 Boolean으로 변경될 수 있도록 하였습니다.
`@Transform` 데코레이터에 입력되는 파라미터 값을 보면 다음과 같은 인터페이스 `TransformFnParams` 를 가지고 있습니다.
얼추 유추해 보기로는, 해당 파라미터로 대입되는 `value` 값이 해당 멤버변수(데코레이터가 적용된 변수)의 값일 것으로 예상됩니다. 데코레이터를 이용하여 해당 값을 출력해 봅니다.
그러나 `value` 값은 `true` 로 나오고 있습니다. 이는 이미 앞선 `@ConfigValidation` 데코레이터에 의해 형 변환이 이루어진 이후라는 뜻입니다.
실제 `DB_LOGGING` 에 대입될 다른 값을 찾기 위해, 이번에는 `obj` 값을 확인해 보았더니, `plainToInstance` 값에 대입되었던 `process.env` 값이 전부 출력됩니다. (스크린샷은 생략하겠습니다.) 그러면 이제 실제 `DB_LOGGING` 값에 대한 접근을 수행해야 하므로, 이에 대한 키값을 알 수 있는 `key` 값을 이용하면 됩니다.
결론적으로, 다음과 같은 `@Transform` 데코레이터 적용을 통해, 강제 형 변환을 적용하였습니다.
이제 출력을 확인해 보면, 환경변수 파일에 기재하였던 `false` 값이 그대로 출력되는 것을 확인할 수 있습니다. 이제 `@ConfigValidation` 데코레이터를 사용할 수 있게 되었습니다.
결론
발생했던 문제와 해결 방법을 정리하면 아래와 같습니다.
- class-transformer 의 `plainToInstance` 함수에서 `enableImplicitConversion` 옵션을 활성화하면, 암시적 형 변환이 일어납니다.
- `plainToInstance` 함수에서, 입력된 문자열(String)을 부울(Boolean) 값으로 변환하게 되면, 해당 문자열 값을 읽는 것이 아니라, 문자열의 존재(빈 값이 아닌지)에 따라 `true` 또는 `false` 로 자동 형 변환을 수행합니다.
- 환경변수를 검사하는 Class 및 데코레이터를 구현하여 class-validator 가 제공하는 `validateSync` 함수를 이용하려고 하였으나, 위 문제로 인해 실제 부울 값이 반영되지 않는 문제가 있었습니다.
- 이 문제를 해결하기 위해, 검사에서 사용될 변수를 다시 형 변환해주기 위하여 class-transformer 의 `@Transform` 데코레이터를 이용하였습니다.
- `@Transform` 데코레이터에서 입력된 원본 값으로 접근하여, 해당 값이 문자열 `"true"` 와 같은지 검사함으로써, 실제 입력된 환경변수가 제대로 반영될 수 있도록 하였습니다.
위 문제에 대한 해결 방법은 직관적으로 생각해서 적용한 방식이므로, 더 좋은 방법이 있을 수 있습니다. 특히, 매번 `@IsBoolean` 데코레이터를 사용하는 경우 (입력 문자열 값으로부터 부울 값으로의 형 변환이 필요한 경우), `@Transform` 데코레이터를 같이 붙여서 사용해야 한다는 단점이 있습니다. 이 부분을 해결하려면, 새로운 데코레이터로 정의해주는 것이 좋습니다. 기존의 `@IsBoolean` 과 `@Transform` 데코레이터들을 하나의 데코레이터로 묶거나, 필요한 여러 가지 로직이나 자세한 옵션들을 동반하여 구현할 수 있겠습니다.
'개발자 💻 > NestJS' 카테고리의 다른 글
[Swagger] BasicAuth 문서 접근 보안 (0) | 2025.01.08 |
---|---|
[초보자의 눈으로 보는 NestJS] 6. DTO와 Validation (0) | 2024.05.31 |
[초보자의 눈으로 보는 NestJS] 5. 유저 서비스의 구현 (0) | 2024.02.24 |
[초보자의 눈으로 보는 NestJS] 4. 유저 서비스의 정의와 의존성 주입 (1) | 2023.11.13 |
[초보자의 눈으로 보는 NestJS] 3+. API 테스트를 위한 Postman (0) | 2023.07.19 |