Validation - Request body (@IsOptional()의 함정)

저뉼(스님?)·2024년 2월 22일
0

나만의 문제해결

목록 보기
3/3
post-custom-banner

nestjs 10.3.3, class-validator 0.14.1, class-transformer 0.5.1


@IsOptional()

API의 request body 파라미터에서 필드가 필수가 아니라면 보통 서버에서 해당 프로퍼티에 아래처럼 @IsOptional()을 붙인다.

// 카드 결제 API

@IsOptional()
@IsEmail()
customerEmail: string // 결제 결과를 알려줄 때 사용하는 고객의 이메일 주소

이렇게 하여 클라이언트가 서버에게 "고객의 이메일 주소"를 전달하지 않더라도 문제없게 되었다.

그럼 이대로 괜찮은 것일까?

서버에서 고려해야 하는 경우의 수가 불필요하게 많은 것 같다. @IsOptional()이 붙으면 값이 null 또는 undefined(생략한 경우)일 때 @IsEmail()과 같은 validator들을 전무 무시하므로 customerEmail 프로퍼티에는 string 외에도 null 또는 undefined가 할당될 수 있다. 그럼 서버에서는 세 가지 경우를 모두 고려하면서 코딩해야 한다.
( 물론 단순히 ORM을 사용해 DB에 저장하기만 하는데 DB 컬럼도 null을 허용한다면 세 경우 구분 없이 그대로 저장하면 되어 그리 와닿지 않을 수 있다. 그러나 일례로 먼저 그 값을 조건(WHERE)으로 DB를 조회해야 한다면? 상황에 따라 에러가 발생할 것이다. )
그런데 null이나 undefined(필드 생략) 중에 한 가지만 허용해서 경우의 수를 줄여도 괜찮지 않을까? 그러자면 먼저 다음의 고민부터 해결해야 했다.

어떤 방식을 선택해야 하는가?

사용자로부터 입력받은 값이 없을 때 이것을 클라이언트가 서버에게 어떻게 전해야 할까?
null을 보내야 할까?
필드를 생략해야 할까?
아니면, empty string('')을 전달하는 것은 또 어떨까?

null 전달

처음에는 다음과 같은 생각으로 null을 전달하는 것이 더 좋지 않을까 싶었다.

'필드를 생략할 수 있으면 프론트엔드에서 실수로 필드를 누락할 때 에러 없는 버그가 발생할 수 있다.'
'null을 전달하면서 데이터가 없다는 것을 명확하게 표현하는 것이 좋을 것 같다.'
'empty string은 email 값으로서 유효한 값이 아니므로 허용하면 안 된다.'

그래서 null만 허용하고 필드 생략(undefined)은 막기 위해 아래와 같이 작성했다.

@ValidateIf((_, value) => value !== null) // null이면 validation 통과
@IsEmail() // empty string 허용하지 않음
customerEmail: string; // string | null

그런데 이렇게 하는 것이 정말 최선이었을까?
클라이언트 입장에서의 API 호출이나 서버 입장에서의 API 구현의 편의성은 어떠한가?

아래 클라이언트 쪽 코드를 보자. 둘 다 사용자 입력에 따라 Request body로 사용할 object를 만드는 코드이다.

// 값이 없으면 생략
interface CardPay {
  customerEmail?: string; // string | undefined
}

const body: CardPay = {
  ...(input !== '' && { customerEmail: input }),
};
// 값이 없으면 null을 전달
interface CardPay {
  customerEmail: string | null;
}

const body: CardPay = {
  ...(input !== '' ? { customerEmail: input } : { customerEmail: null }),
};

( 참고로, form input에 입력된 값이 없으면 기본적으로 empty string )
위 코드를 보면, 클라이언트 쪽에서는 null을 전달하는 것이 더 번잡한 것 같다.
그리고 서버 쪽에서는 null이든 undefined든 경우의 수가 줄어드는 게 중요해 보였다.

다른 기업들의 API도 살펴보았다. 토스 페이는 값이 없으면 생략한다. 그리고 쿠팡은 empty string을 보내며 경우에 따라 flag 역할의 부가적인 필드 값을 함께 보내기도 한다.

여기까지 보았을 때, 클라이언트 사용성이 떨어져서인지 그 외에도 다른 문제가 있는지 아무래도 null을 보내는 것이 더 나은 방법은 아닌 것 같다.

필드 생략

이번에는 필드를 생략하는 방법을 살펴보자. 이를 위한 서버 쪽 구현 방법은 두 가지다.

  1. 아까 null만 허용할 때와 마찬가지로, undfined만 validator를 우회할 수 있도록 @ValidateIf()를 사용한다.

    @ValidateIf((_, value) => value !== undefined)
    @IsEmail()
    customerEmail: string; // string | undefined
  2. 기본값을 활용하고 그 값만 우회하게 한다.

    @ValidateIf((_, value) => value !== '')
    @IsEmail()
    customerEmail: string = '';

    정상적인 고객의 이메일 주소 값이 empty string일 수는 없으니 이것을 기본 값으로 할당했다.

2번으로 하면 customerEmail의 타입이 string 하나라 얼핏 경우의 수가 더 줄어든 것 같지만 비지니스 로직상 empty string인지 체크해야 한다면 1번과 다를 것은 없어 보인다. 내 입장에서는 말이다.

다른 개발자의 입장에서 1번으로 구현한 코드를 보았을 때 바로 이해할 수 있을까?

1번은 class-validator 패키지의 @IsEmail()이 empty string을 허용하지 않는 것을 알아야 한다. 모른다면 비지니스 로직에서 값이 empty string인 경우를 생각하지 않는 것을 보고 실수라고 생각할 수도 있다.
반면에, 2번은 몰라도 된다. validator들을 자세히 보지 않아도 empty string이 할당될 수 있음이 기본값을 통해 명확하게 드러난다. 그리고 당연히 비지니스 로직에서도 이 경우를 다룬다.

아 그리고 사실 2번 코드는 클라이언트에게 직접 empty string을 받을 수도 있다.

결과적으로, @IsOptional()을 사용할 때와 비교하면, 클라이언트 입장에서도 필드를 생략하든 empty string을 보내든 편한 대로 할 수 있게 되었고, 서버에서도 경우의 수가 줄고 가독성이 좋아졌다.

post-custom-banner

0개의 댓글