@NonNull, @Nonnull, @NotNull, @NotEmpty, @NotBlank 비교
1. 정리하게된 배경
초기 도메인 필드의 유효성을 검증하고자, 아래와 같이 @NonNull을 이용해서 구현했다. 초기 프로젝트 환경에서 validation 의존성이 추가되지 않아서, Spring 에서 제공되는 @NonNull이 있길래 사용했다. 클라이언트에서 입력받았을 때 해당 필드값이 비어있으면 예외를 처리가하기 위함이었는데 이에 대해 리뷰어님께 코멘트를 받았다. 해당 어노테이션에 대한 이해가 부족한 것으로 판단해서 코멘트를 주신것 같아 이번기회에 정리해보기로 했다.
NonNull 과 NotNull, NotBlank 는 서로 어떤 차이가 있을까요?
이들의 차이를 알아보고 적용해보는 것도 좋을거 같습니다
AS-IS 코드
import org.springframework.lang.NonNull;
import roomescape.reservationTime.ReservationTime;
import java.time.LocalDate;
import java.util.Objects;
public class Reservation {
private Long id;
@NonNull
private String name;
@NonNull
private String date;
@NonNull
private ReservationTime reservationTime;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getDate() {
return date;
}
public ReservationTime getReservationTime() {
return reservationTime;
}
public Reservation(@NonNull String name, @NonNull String date, @NonNull ReservationTime reservationTime, ReservationPolicy reservationPolicy) {
this(null, name, date, reservationTime, reservationPolicy);
}
public Reservation(Long id, @NonNull String name, @NonNull String date, @NonNull ReservationTime reservationTime, ReservationPolicy reservationPolicy) {
this.id = id;
if(reservationPolicy.validateName(name)) {
throw new IllegalArgumentException("예약자 이름에 특수문자가 포함되어 있습니다.");
}
this.name = name;
if(reservationPolicy.validateDate(date)) {
throw new IllegalArgumentException("예약 날짜 형식이 올바르지 않습니다.");
}
this.date = date;
this.reservationTime = reservationTime;
}
}
2. 어노테이션 별 특징 비교 테이블
@Nonnull | @NonNull | @NotNull | @NotEmpty | @NotBlank | |
null 확인 | O | O | O | O | O |
"" 확인 | O | O | |||
" " 확인 | O | ||||
컴파일 시 확인 | O | O | X | X | X |
런타임 시 확인 | X | X(Lombok 가능) | O | O | O |
용도 | 변수, 파라미터 ,필드, 메서드 반환 값 등에 null 이 아님을 보장 | 변수, 파라미터 ,필드, 메서드 반환 값 등에 null 이 아님을 보장 | 변수, 파라미터 ,필드, 메서드 반환 값 등에 null 이 아님을 보장 | Collection, array, String[] 등에 null이 아니고, 비어있지 않음을 보장 | 문자열이 null이 아니고, 길이가 0이 아니며, 공백 문자가 아님을 보장 |
적용대상 | 모든 객체 타입 | 모든 객체 타입 | 모든 객체 타입 | 문자열, 컬렉션, 배열, 맵 등의 참조객체 | 문자열 |
특징 | 컴파일 타임 null 검사 목적 코드 분석 도구나 IDE에서 지원 | Lombok의 경우, null 체크 로직을 자동으로 생성 Spring의 @NonNull은 null 안전성을 보장하기 위해 사용 및 특정한 검증 로직을 포함하지 않습니다. |
Bean Valdation API(JSR-380)와 함께 사용. 런타임 시 검증 로직 추가 가능 | null 검사, 길이가 0인지 검사 |
null 검사와 빈 문자열 검사 및 문자열이 공백 문자로만 이루어 졌는지 검사 |
제공 패키지 | javax.annotation | Spring, Lombok, jetBrain |
javax.validation.constraints | javax.validation.constraints | javax.validation.constraints |
위와 같이 각 어노테이션 별 특징과 용도 등을 정리했다. 테이블을 정리하면서 리뷰어님이 왜 코멘트하셨는지 알 것 같았다.
우선 @NonNull과 @Nonnull의 경우 런타임 시 해당 유효성 검증을 수행하지 않는다. 즉, 내가 적용한 어노테이션은 런타임에서 수행하지 않는다는 얘기가 된다. 런타임 시에 유효성을 검증하기 위해서는 @NotNull, @NotEmpty, @NotBlank 중에 사용해야하고, 특히 문자열의 경우 @NotBlank 어노테이션을 사용해야, null & "" & " " 모두를 체크할 수 있다는 것을 알게 되었다.
위 내용을 기반하여, 코드 리팩토링을 수행했고, Reservation 엔티티에 있던 @NonNull 을 모두 제거했고, 클라이언트에 값을 받아오는 ReservationRequestDto에 @NotBlank 를 적용했다. 또한, Controller에 해당 dto에 @Valid를 적용했고, GlobalExceptionHandler.class 에 예외 핸들링을 위한 코드도 추가했다. 일련의 과정은 아래추가로 정리했다.
3. 유효성 검증을 위한 적용 과정
1. gradle을 통해 의존성을 추가한다.
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-validation' // 추가 의존성
...
}
2. 유효성 검증을 할 대상필드에 어노테이션을 추가한다.
public class ReservationThemeRequestDto {
@NotEmpty(message = "테마명을 입력 해주세요")
private String name;
@NotEmpty(message = "테마에 대한 설명을 입력해주세요")
private String description;
@NotEmpty(message = "썸네일 url 을 입력해주세요")
private String thumbnail;
}
3. @Valid 어노테이션이 활성화 되는데, @Valid는 controller의 각 함수의 파라미터에 붙여준다.
@PostMapping("/reservations")
public ResponseEntity<ReservationResponseDto> createReservation(@Valid @RequestBody ReservationRequestDto reservationRequestDto) {
final ReservationResponseDto responseDto = reservationService.save(reservationRequestDto);
return ResponseEntity.ok().body(responseDto);
}
4. 예외 핸들링을 위해서, @ExceptionHandler(MethodArgumentNotValidException.class) 어노테이션이 붙은 핸들링 함수를 정의해줘야한다.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
AS-IS 코드
import org.springframework.lang.NonNull;
import roomescape.reservationTime.ReservationTime;
import java.time.LocalDate;
import java.util.Objects;
public class Reservation {
private Long id;
@NonNull
private String name;
@NonNull
private String date;
@NonNull
private ReservationTime reservationTime;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public String getDate() {
return date;
}
public ReservationTime getReservationTime() {
return reservationTime;
}
public Reservation(@NonNull String name, @NonNull String date, @NonNull ReservationTime reservationTime, ReservationPolicy reservationPolicy) {
this(null, name, date, reservationTime, reservationPolicy);
}
public Reservation(Long id, @NonNull String name, @NonNull String date, @NonNull ReservationTime reservationTime, ReservationPolicy reservationPolicy) {
this.id = id;
if(reservationPolicy.validateName(name)) {
throw new IllegalArgumentException("예약자 이름에 특수문자가 포함되어 있습니다.");
}
this.name = name;
if(reservationPolicy.validateDate(date)) {
throw new IllegalArgumentException("예약 날짜 형식이 올바르지 않습니다.");
}
this.date = date;
this.reservationTime = reservationTime;
}
}
느낀점
어노테이션의 용도와 목적을 알고 사용합시다 :)