검증 예외
요새 마와셀 백엔드 쪽 개발을 하느라 이것저것 바쁘다.
앱도 리뉴얼을 해야 하는데 일단 웹쪽 연동해서 처리해야 하는 부분이 있어서 웹을 먼저 끝내고 앱을 끝내기로 했다.
백엔드던 프론트엔드던 개발하면서 예외 처리가 항상 중요한데,
원래 전달인자 검증을 자체적으로 만들어 쓰고 있었다.
근데 일일이 그걸 하려니 너무 비효율적인 것 같아서, 한번에 처리하는 방향으로 만들게 되었다.
Nest.Js에서 대중적으로 가장 많이 사용하는 Class-Validator를 사용했다.
동작은 잘 했는데, 프론트 쪽에서 받을 때 응답 코드가 내가 원하는 형태로 떨어지지 않았다.
그래서 이 부분을 좀 내 입맛에 바꾸고 싶었다.
How to?
먼저 검증 실패 시 발생하는 예외는 아래와 같은 코드로 구현되어 있다.
export class ValidationError extends Error {
constructor(
public readonly error: {
statusCode: number
className: string
methodName: string
errorCode: string
message: string
errorTag?: string[]
originError?: any
},
name: string
) {
super(error.message)
this.name = name
}
}
이건 그냥 자신의 프로젝트 취향에 맞게 작성하면 될 것 같다.
그리고 PipeTransform를 상속받는 객체를 하나 만들어준다.
import {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
ValidationPipeOptions,
} from "@nestjs/common"
import { validate } from "class-validator"
import { plainToInstance } from "class-transformer"
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
constructor(options?: ValidationPipeOptions) {}
async transform(value: any, { metatype }: ArgumentMetadata) {
if (!metatype || !this.toValidate(metatype)) {
return value
}
const object = plainToInstance(metatype, value)
const errors = await validate(object)
if (errors.length > 0) {
throw new ValidationError(
{
statusCode: 500,
className: "ValidationPipeError",
methodName: "transform",
errorCode: "validation",
message: `검증 에러가 발생하였습니다.\n${errors
.map(item => JSON.stringify(item.constraints))
.join("\n")}`,
errorTag: ["validation", "error", ""],
originError: errors,
},
"ValidationError"
)
}
return value
}
/**
*
* @param metatype
* @returns
*/
private toValidate(metatype: Function): boolean {
const types: Function[] = [String, Boolean, Number, Array, Object]
return !types.includes(metatype)
}
}
이렇게 작성하면 아래와 같은 DTO가 전달되었을 때, 검증에 문제가 생길 경우
throw new ValidationError부분의 예외가 실행된다.
export class AdminUpdateDto extends BaseDto {
@IsString()
id: string
@IsIn(Object.values(DATA_STATUS_ENUM))
status: DataStatus
}
그리고 이 에러를 잡을 ExceptionFilter를 상속받는 필터를 하나 만들어준다.
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpStatus,
} from "@nestjs/common"
// 25.01.10 by elfinlas
// [Exception] 검증 관련 에러 처리를 위한 공통 헨들러
@Catch(ValidationError)
export class ValidationErrorFilter implements ExceptionFilter {
catch(exception: ValidationError, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const response = ctx.getResponse()
const status = exception.error.statusCode
? 500
: HttpStatus.INTERNAL_SERVER_ERROR
response.status(status).json({
statusCode: status,
...exception.error,
})
}
}
그리고 이 필터의 경우 컨트롤러에 하나씩 넣어주는 것보다, 전역으로 잡아주는게 좋다.
main.ts 내의 bootstrap함수 적당한 곳에 아래와 같이 선언해준다.
async function bootstrap() {
//...
app.useGlobalFilters(new ValidationErrorFilter())
}
정리
근데 이런 예외의 경우 프론트에 바로 노출하는 것 보다는 프록시 등으로 먼저 받은 다음, 가공하는게 좋다.
위처럼 하면 검증 관련 에러를 자신의 입맛에 편하게 바꿀 수 있다.