개발하시는 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
@NotEmpty
는 null
과 ""
둘 다 허용하지 않게 합니다.
@NotNull
에서 ""
validation 이 추가된 것입니다.
즉, @NotEmpty
는 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(2);
}
name, email, phone 에 모두 @NotEmpty
가 걸려있다고 가정했을 때,
위의 테스트를 실행하면 통과를 하게됩니다.
name 은 null 이 들어왔기 때문에 violations
에 검증이 되어 잘못된 값이라 판단하여 추가가 되고, email 같은 경우 ""
(empty) 는 ""(empty)
이기 때문에 violations
에 추가가 됩니다.
phone 의 경우 " "
(blank) 이기 때문에 null
과 empty
가 아니기 때문에 violations 에 추가되지 않습니다.
@NotBlank
@NotBlank
는 null
과 ""
과 " "
모두 허용하지 않습니다.
@NotEmpty
에서 " "
validation 이 추가된 것입니다.
즉, 세개 중 가장 validation 강도가 높은 것으로,@NotBlank
는 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(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/
'SPRING' 카테고리의 다른 글
[Spring] Optional 의 3가지 생성 방법 (0) | 2020.07.25 |
---|---|
[Spring] Meta Annotation 이란?(@Target, @Retention) (4) | 2020.07.25 |
[Spring] Maven Wrapper 설명 및 실행방법 (1) | 2020.01.31 |
[Spring boot] java 파일을 특정 패키지로 이동 시 발생하는 ConflictingBeanDefinitionException 문제 해결 (9) | 2020.01.03 |
[Mybatis] Spring boot 에서 mybatis 로 2개의 DB connection 하기 (2) | 2019.12.20 |