본문 바로가기

SPRING

[Spring Boot] @NotNull, @NotEmpty, @NotBlank 의 차이점 및 사용법

개발하시는 API 의 request parameter 의 null 체크를 어떻게 하고 계신가요?

대부분 별도의 null 체크 util 을 사용하거나, Controller 에서 조건문을 사용하여 null 을 체크하기도 합니다.

이러한 조건문과 메서드 들은 변수별 특징과 조건에 관련하여 공통화하기가 쉽지 않습니다.

또한 별도의 API 에 따라서 같은 값이여도 필수인 경우가 있고, 아닌 경우가 있을 수 있습니다.

따라서, 해당 API 의 parameter 에 따라 별도의 오류 메시지조건 이 필요합니다.

이런 경우 쉽게 사용할 수 있는 것이 @NotNull & @NotEmpty & @NotBlank 입니다.

이 3가지 Annotiation 은 Bean Validation (Hibernate Validation) 에서 제공하는 표준 Validation 입니다.

사용하는 방법은 매우 유사하지만 잘못 사용했다간 완전 다른 결과를 초래할 수 있기 때문에 세개의 차이에 대해서 알고 넘어가야 합니다.

 

 

 

Bean Validation 사용법

차이점에 대해서 알아보기 전에 Bean Validator 인 @NotNull 과 @NotEmpty, @NotBlank 에 대한 사용법에 대해 알아보겠습니다. (선언부를 제외하고는 꼭 이렇게 사용해야하는 것은 아닙니다.)

 

public class UserLoginRequestDto {
    @NotNull(message = "이름은 Null 일 수 없습니다!")
    @Size(min = 1, max = 10, message = "이름은 1 ~ 10자 이여야 합니다!") 
    private String name;

    @NotNull(message = "이름은 Null 일 수 없습니다!")
    @Min(1)
    @Max(10)
    @Email
    private String email;
}

 

위의 내용을 보시면 사용자가 Login 을 하기 위해 name 을 parameter 에 담아서 요청하는 것입니다.

@NotNull 은 이름 그대로 Null만 허용하지 않습니다.

따라서, "" 이나 " " 은 허용하게 됩니다.

@Size 는 name 의 최소, 최대 사이즈를 지정할 수 있으며 해당 사이즈에 올바르지 않는 경우 @NotNull 과 같이 message 를 담아 예외를 던질 수 있습니다.

@Min, @Max 는 @Size 에서 min, max 를 의미하며 똑같이 message 를 속성에 추가할 수 있습니다.

@Email 은 이메일 형식이 아닌경우 예외를 던지도록 설정할 수 있습니다.

이외에도 다양한 Annotation 과 속성이 존재하고, 필요한 경우 Custom Annotation 을 쉽게 생성하여 사용할 수 있습니다.

다양한 속성 및 기능을 확인은 https://beanvalidation.org/2.0/spec/ 에서 확인할 수 있습니다.

 

 

 

DTO 를 쓰는 이유

class 명을 보시면 DTO 를 사용하여 해당 파라미터를 담고 있는 것을 볼 수 있습니다.

DTO 를 사용하는 이유는, User 라는 Domain 에 name 이 모든 request 및 response 에 필요하지 않음에도 불필요하게 사용될 수 있으며, 만약 각 API 의 request 와 response 에 맞추기 위해 domain 이 수정되서는 안되기 떄문입니다.

따라서 각 DTO 에 필요한 데이터만 정의 되어야하며, 필수 값에 대한 조건 체크하는 것이나 DTO 에서 Domain 으로 변환하거나, Domain 에서 DTO 로 변환하는 로직은 Domain 이 아닌 DTO 에 담겨야 합니다.

따라서, 위의 @NotNull 과 같은 Data 의 Validation 도 DTO 의 역할 이기 때문에 DTO 에 넣어주게 되면 역할과 책임이 좀 더 명백해지게 됩니다.

 

 

 

Controller 설정

@PostMapping("/login")
public ResponseEntity login(@Valid @RequestBody UserLoginRequestDto loginUser) {    
    UserLoginResponseDto login = userService.login(loginUser);
    return new ResponseEntity<>(new BaseResult.Normal(login), HttpStatus.OK);
}

 

DTO 에서 @NotNull 등을 설정 후 사용하고자 하는 Controller 내 API 에서 RequestBody@Valid 들 추가해주면 설정된 Bean Validation 을 사용할 수 있습니다.

 

 

 

예외를 처리하는 방법

message 와 같이 속성을 추가하게되면 만약 Null 이 들어왔을 때 해당 메시지를 사용할 수 있게 됩니다.

@ExceptionHandler(MethodArgumentNotValidException.class)
public Object handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    String errorMessage = e.getBindingResult()
    .getAllErrors()
    .get(0)
    .getDefaultMessage();

    printExceptionMessage(errorMessage);
    return new ResponseEntity<>(new BaseResult.Normal(INVALID_PARAMETER), HttpStatus.BAD_REQUEST);
}

 

위의 내용을 보시면 @ControllerAdvice 내 @ExceptionHandler 에서 Bean Validation 에 대한 오류가 발생하였을 경우(name 에 null 이 들어온 경우) 처리하기 위한 로직입니다.

만약, name 에 null 이 들어오면 MethodArgumentNotValidException 의 예외가 던져집니다.

따라서, 해당 예외를 ExceptionHandler 에서 예외를 잡은 후,

e.getBindingResult().getAllErrors().get(0).getDefaultMessage();

와 같이 해당 DTO 에서 선언한 message 에 내용을 가져올 수 있습니다.

위의 예시를 고려한다면 "이름은 Null 일 수 없습니다!" 가 넘어오게 될 것 입니다.

따라서 해당 메시지를 리턴하거나, Error log 로 출력할 수 있습니다.

 

 

 

@NotNull, @NotEmpty, @NotBlank 의 차이점

 

앞서 공통적으로 사용하는 방법 및 예외처리에 대해서 알아봤습니다.

@NotNull & @NotEmpty & @NotBlank 는 사용법은 매우 유사하지만 중요한 차이가 있습니다.

 

 

@NotNull

우선 @NotNull 은 위에 살펴본 것 처럼 이름 그대로 Null만 허용하지 않습니다.

따라서, "" 이나 " " 은 허용하게 됩니다.

그렇기 때문에 만약 "" (초기화된 String) )이나 " " (공백) 을 허용하지 않는다면 사용해서는 안됩니다.

Null 이 들어오게 되면, 로직에 예상치 못한 오류가 발생하거나 문제가 생길 경우 사용해야 합니다.

즉, 초기화나 공백의 값이 들어와 저장은 되야하지만 Null 로 들어온 경우 오류가 나는 변수를 받을 때 사용하면 됩니다.

 

@Test
public void 사용자_이름_DTO_NotNull_체크() {
    //given
    UserLoginRequestDto user = UserLoginRequestDto.builder()
                                        .name(null)
                                        .email("")
                                        .phone(" ")
                                        .build();
    //when
    Set<ConstraintViolation<UserLoginRequestDto>> violations = validator.validate(user);

    //then
    assertThat(violations.size()).isEqualTo(1);
}

 

name, email, phone 에 모두 @NotNull 이 걸려있다고 가정했을 때,

위의 테스트를 실행하면 통과를 하게됩니다.

name 은 null 이 들어왔기 때문에 violations 에 검증이 되어 잘못된 값이라 판단하여 추가가 되고, email 같은 경우 ""(empty) 는 null 이 아니기 때문에 violations 에 추가되지 않게 됩니다.

phone 의 경우 " "(blank) 이기 때문에 email 과 마찬가지로 null 이 아니기 때문에 violations 에 추가되지 않습니다.

 

 

@NotEmpty

@NotEmptynull"" 둘 다 허용하지 않게 합니다.

@NotNull 에서 "" validation 이 추가된 것입니다.

즉, @NotEmptynull"" 은 막히되, " " 은 허용이 됩니다.

@Test
public void 사용자_이름_DTO_NotNull_체크() {
 //given
 UserLoginRequestDto user = UserLoginRequestDto.builder()
                                 .name(null)
                                 .email("")
                                 .phone(" ")
                                 .build();
 //when
 Set<ConstraintViolation<UserLoginRequestDto>> violations = validator.validate(user);

 //then
 assertThat(violations.size()).isEqualTo(2);
}

name, email, phone 에 모두 @NotEmpty 가 걸려있다고 가정했을 때,

위의 테스트를 실행하면 통과를 하게됩니다.

name 은 null 이 들어왔기 때문에 violations 에 검증이 되어 잘못된 값이라 판단하여 추가가 되고, email 같은 경우 ""(empty) 는 ""(empty) 이기 때문에 violations 에 추가가 됩니다.

phone 의 경우 " "(blank) 이기 때문에 nullempty 가 아니기 때문에 violations 에 추가되지 않습니다.

 

 

@NotBlank

@NotBlanknull""" " 모두 허용하지 않습니다.

@NotEmpty 에서 " " validation 이 추가된 것입니다.

즉, 세개 중 가장 validation 강도가 높은 것으로,@NotBlanknull""" " 모두 허용하지 않습니다.

@Test
public void 사용자_이름_DTO_NotNull_체크() {
 //given
 UserLoginRequestDto user = UserLoginRequestDto.builder()
                                 .name(null)
                                 .email("")
                                 .phone(" ")
                                 .build();
 //when
 Set<ConstraintViolation<UserLoginRequestDto>> violations = validator.validate(user);
  //then
 assertThat(violations.size()).isEqualTo(3);
}

name, email, phone 에 모두 @NotBlank 가 걸려있다고 가정했을 때,

위의 테스트를 실행하면 통과를 하게됩니다.

name 은 null 이 들어왔기 때문에 violations 에 검증이 되어 잘못된 값이라 판단하여 추가가 되고, email 같은 경우 ""(empty) 는 ""(empty) 이기 때문에 violations 에 추가가 됩니다.

phone 의 경우 " "(blank) 이기 때문에 violations 에 추가가 되어 총 3개가 violations 에 추가가되어 성공하게 됩니다 .

 

 

 

 

 

결론

앞서 3개의 Bean Validation 의 사용방법, 예외 처리하는 방법, 검증(테스트) 방법에 대해 설명하였습니다.

총 3개의 Validatior 는 사용법과 기능이 유사하기 때문에 쉽게 혼용되거나 잘못 사용될 수 있습니다.

따라서, 해당 DTO 에 대한 테스트를 Validator 에 따라 null , "", " " 에 구분하여 나눠서 추가해야 합니다.

만약, @NotBlank 이어야하는 값이 @NotNull 이 설정되어 공백이 들어오게되면 큰 문제가 발생할 수 있기 때문입니다.

중간에 DTO 를 사용해야하는 이유에 대해서 설명을 드렸습니다.

테스트를 추가할 때도 각 DTO 를 분리하게 되면 각각의 request 에 따라 테스트 코드를 추가해줄 수 있기 때문에 요청을 모두 검증할 수 있어, 테스트 코드에 대한 커버리지가 높아질 수 있습니다.

이처럼 이러한 Validation 을 용도 및 상황에 맞게 사용한다면 사용자의 오류나 시스템의 오류를 최소화 할 수 있습니다.

 

 

 

참조

https://www.baeldung.com/java-bean-validation-not-null-empty-blank

https://beanvalidation.org/2.0/spec/

http://hibernate.org/validator/

https://jojoldu.tistory.com/129