@NotNull @NotEmpty @NotBlank

최창효·2023년 10월 13일
1
post-thumbnail
post-custom-banner

발단

사실 @NotNull @NotEmpty @NotBlank의 차이점은 많은 곳에서 소개되고 있습니다. 저 역시 다른 분들의 블로그를 보면서 해당 내용의 개념을 어렴풋이 알고 있었습니다. 하지만 최근 생각지 못한 동작을 목격(?)해서 해당 내용을 정리해보려 합니다.

그 내용은 바로 '올바른 값을 넣었는데 Validation에러가 터진다'는 것이었습니다!

기본 내용

우선 제가 알던 개념이 맞는지 확인하기 위해 스스로 테스트를 진행해 봤습니다. (테스트는 postman을 이용해 진행했습니다)
String타입의 변수를 기준으로 테스트했을 때 다음과 같은 결과를 얻을 수 있습니다.

  • @NotNull은 null을 허용하지 않습니다. 하지만 """ "는 허용합니다.
  • @NotEmpty는 null""를 허용하지 않습니다. 하지만 " "는 허용합니다.
  • @NotBlank는 null"", " "를 모두 허용하지 않습니다.

-> 허용하지 않는다는 건 해당 값을 넣었을 때 MethodArgumentNotValidException라는 400에러를 반환합니다.

List 변수를 기준으로 테스트했을 때는 다음과 같은 결과를 얻을 수 있었습니다.

  • @NotNull, @NotEmpty, @NotBlank모두 null을 허용하지 않습니다. (400 MethodArgumentNotValidException)

추가로 다음과 같은 차이가 있습니다.

  • @NotNull은 []는 허용합니다.
  • @NotEmpty는 []입력 시 400 MethodArgumentNotValidException에러를 반환합니다.
  • @NotBlank는 []입력 시 500 UnexpectedTypeException에러를 반환합니다. (왜 500에러인지는 아래에서 살펴보도록 하겠습니다.)

차이점

각 어노테이션의 설명을 한번 읽어보겠습니다.

NotNull

element가 null이면 안된다고 합니다. 아주 간단하군요!

NotEmpty

element가 null이 아니고 비어있지도 안아야 합니다.
또한 컬렉션, Map, Array에 대해서는 size로 NotEmpty여부를 평가한다고 합니다.
(CharSequence에 대해서는 정확히 이해하지 못했습니다. 다만 여기서는 대략적으로 String과 비슷(?)한 혹은 String이라고 생각해도 괜찮다고 판단했습니다)

NotBlank

element가 null이 아니며 적어도 공백이 아닌 문자를 하나 이상 가져야 합니다.

문제상황

팀원이 저를 불렀을 당시의 상황을 대략적으로 구현해보겠습니다.

Controller

@RestController
public class TestController {
    @PostMapping
    public ResponseEntity func(@Valid @RequestBody RequestDto requestDto) {
        System.out.println("requestDto = " + requestDto);
        return ResponseEntity.ok().build();
    }
}

RequestDto

@Getter
public class RequestDto {
    @NotBlank
    private Integer a;
    
}

(저처럼 @Getter를 빼먹고 '어? 왜 테스트가 실패하지'라는 말을 한다면 팀원의 따가운 눈초리를 받게 될 수도 있습니다)

postman 요청

Integer타입의 변수에 @NotBlank를 선언한 상태입니다. 그리고 Integer타입에 맞는 555라는 값을 전송했지만 에러가 발생했습니다!

원인

저는 에러가 발생하면 머리에 떠오르는 것들을 이것저것 해보곤 하는데, 이번 일을 통해
에러가 발생하면 먼저 어떤 에러가 발생했는지 확인하는 게 중요하다는 걸 느꼈습니다. 현재 발생한 에러는 500 UnExpectedTypeException입니다.

Validation에 의한 에러는 400 MethodArgumentNotValidException을 반환합니다. 즉, 지금 발생한 500에러는 Validation에 의한 에러가 아니라는 얘기입니다.

에러 내용을 구체적으로 살펴보면 다음과 같습니다. No validator could be found for constraint 'javax.validation.constraints.NotEmpty' validating type 'java.lang.Integer'. Check configuration for 'a'

Integer유형에 대해서 NotEmpty유효성을 검사하는 유효성 검사기가 없다고 합니다. 한마디로 Integer는 NotEmpty검사를 할 수 없다는 얘기입니다. (그래서 null을 넣어도 400에러가 아닌 500에러가 발생합니다. 다만 값을 안보냈을때는 400에러를 반환합니다)

이는 Integer뿐만 아니라 Long, Double 등의 모든 Wrapper Class에 해당하는 내용입니다.

추측?

에러로 미뤄봤을 때 '@NotNull, @NotEmpty, @NotBlank는 1. 우선적으로 입력받은 변수를 매핑해 타입을 맞추고 2. 그 후에 Validation을 진행하는 게 아닐까? 우리의 경우 1번 조건을 만족하지 못했기 때문에 500에러가 발생한 거 같다'라고 팀원과 잠정적인 결론을 내렸습니다.

기타

Primitive타입에 대해서는 이러한 문제가 발생하지 않습니다.

보너스

Mysql + JPA의 Entity에서 각 어노테이션들은 어떤 의미가 있을까요? 바로 코드로 확인해 봅시다.

import javax.validation.constraints.NotNull // com.sun.istack.NotNull이 아닙니다

@Entity
@Getter
@NoArgsConstructor
public class Product {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    private String a;

    @NotEmpty
    private String b;

    @NotBlank
    private String c;

}

@NotNull은 not null constraint를 걸어주지만 @NotEmpty와 @NotBlank는 not null constraint를 걸어주지 않는군요!

결론

  • String이 아닌 Wrapper Class의 Validation에는 @NotEmpty나 @NotBlank말고 @NotNull을 사용하자
  • JPA의 DDL에서는 오직 @NotNull만 not null constraint를 걸어준다
    그러니 @NotBlank가 Null검사와 Empty검사를 모두 해준다고 @NotBlank를 @NotEmpty + @NotNull로 오해하지 말자
profile
기록하고 정리하는 걸 좋아하는 개발자.
post-custom-banner

0개의 댓글