본문 바로가기

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

  • spring공부 2021.05.18 23:11

    흐아... 덕분에 오류를 찾을 수 있었습니다!!

    원래 댓글 잘 안 다는데 너무 고맙습니다~~~

  • 질문 2021.07.21 15:47

    궁금한점이 있습니다.
    "DTO를 쓰는 이유" 섹션에서 "User라는 Domain"이라는 말을 쓰셨는데, domain이 entity와 동일하다고 생각해도 될까요?
    만약 다르다면 어떻게 다른건가요?
    만약 동일하다면 궁금한점이 있습니다. "따라서 각 DTO 에 필요한 데이터만 정의 되어야하며, 필수 값에 대한 조건 체크하는 것이나 DTO 에서 Domain 으로 변환하거나, Domain 에서 DTO 로 변환하는 로직은 Domain 이 아닌 DTO 에 담겨야 합니다." 라고 하셨습니다.
    제가 궁금해서 좀 찾아봤는데 https://stackoverflow.com/questions/28703401/conversion-of-dto-to-entity-and-vice-versa 같은 글을 보면 "DTO's are simple objects that should not contain any business logic, i.e. they should not be aware of entities (value objects) or how to convert themselves to an entity (value object)" 라는 부분이 있어 조금 상충되는 부분이 있는것 같습니다.

    • 시니시즘 2022.03.18 13:03

      FM은 Entity에서 DTO로 변환하는 비즈니스 로직이 담긴 레이어를 분리하는게 맞습니다. 보통 오브젝트 맵핑 레이어라고 불리며 MapStruct 같은 라이브러리를 사용하면 많은 상용구 코드를 감소시켜 코딩 퍼포먼스를 향상시킬 수 있습니다. 핵심은 POJO 및 DTO 레이어는 프로젝트 및 프레임워크에 종속적이지 않아야 하고 언제든지 해당 프로젝트에서 복사해서 다른 프로젝트로 붙여넣기 해서 작업할 수 있도록 하는게 주목적입니다.

  • 털쟁이개발자 2021.09.30 11:28 신고

    @NotBlank 설명하는 부분에서 @NotBlank 를 @NotEmpty라고 잘 못 쓰신것 같아요!