개발 중 컨트롤러에서 @RequestBody 객체를 검증할 때 @Valid 어노테이션을 사용해서 요청 객체에 대한 유효성 검증을 할 수 있다. 요청 객체에 대한 유효성 검증을 어떻게 하는지 차근차근 알아보도록 하자.

 

validation 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-validation'

build.gradle에 먼저 spring-boot-starter-validation 의존성을 추가한다.

 

@Valid 적용 (컨트롤러, 요청 DTO)

@PostMapping("/join")
public ResponseEntity<Object> joinMember(@Valid @RequestBody MemberJoinDto memberJoinDto) {
    return ResponseEntity.ok(memberService.joinMember(memberJoinDto));
}

컨트롤러에서 멤버등록 메소드의 @RequestBody인 MemberJoinDto에 @Valid 어노테이션을 붙여준다.

 

@Getter
public class MemberJoinDto {
    @NotBlank(message = "아이디는 공백일 수 없습니다.")
    private String userId;

    @NotBlank(message = "이름은 공백일 수 없습니다.")
    private String userName;

    @NotBlank(message = "비밀번호는 공백일 수 없습니다.")
    private String password;

    @NotBlank(message = "휴대폰번호는 공백일 수 없습니다.")
    private String phoneNumber;
}

MemberJoinDto의 필드들에 대해 Valid의 Constraints 중 하나인 @NotBlank를 적용하고 필드가 없을 때의 메세지를 정의한다.

 

여기까지 컨트롤러에서 @Valid 어노테이션과 DTO에 Contraints 어노테이션을 적용해 주면 유효성 검증 끝이다.

 

Valid Contraints

[문자열 검증]

@Null : null만 허용
@NotNull : null을 허용하지 않고,  "", " "는 허용
@NotEmpty : null, ""을 허용하지 않고, " "는 허용
@NotBlank : null, "", " " 모두 허용하지 않음

 

[최대 최소 검증]
@DecimalMax : 지정된 최대 값보다 작거나 같아야 함 (Require : String value  => max 값을 지정)
@DecimalMin : 지정된 최소 값보다 크거나 같아야 함 (Require : String value  => min 값을 지정)
@Max : 지정된 최대 값보다 작거나 같아야 함 (Require : int value  => max 값을 지정)
@Min : 지정된 최소 값보다 크거나 같아야 함 (Require : int value  => min 값을 지정)

 

[범위 값에 대한 검증]
@Positive : 양수인 값
@PositiveOrZero : 0이거나 양수인 값
@Negative : 음수인 값
@NegativeOrZero : 0이거나 음수인 값

 

[시간에 대한 검증]
@Future : Now 보다 미래의 날짜, 시간
@FutureOrPresent : Now 이거나 미래의 날짜, 시간
@Past : Now 보다 과거 의의 날짜, 시간
@PastOrPresent : Now 이거나 과거의 날짜, 시간

 

[자릿수검증]
@Digits : 자릿수 검증 (ex. @Digits(integer = 5, fraction = 5))

[크기 검증]
@Size(min=, max=) : 길이 제한

 

[이메일 검증]
@Email : 이메일 형식을 검사 (""의 경우 통과되기 때문에 @Patten을 통한 정규식 검사를 더 많이 사용)

 

[정규식 검증]
@Pattern(regexp = ) : 정규식 검사

[Boolean값 검증]
@AssertFalse : false 여부 (null은 체크하지 않음)
@AssertTrue : true 여부 (null은 체크하지 않음)

 

예외 처리

@Valid 어노테이션을 컨트롤러에 적용했다면, 유효성 검증에 실패한 경우 발생하는 예외를 처리해 보도록 하자.

유효성 검증에 실패하는 경우 발생하는 예외는 MethodArgumentNotValidException이다.

@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {
    ...
    
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        String errorMessage = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        return new ResponseEntity<>(new ErrorResponse(String.valueOf(ex.getStatusCode().value()), errorMessage), ex.getStatusCode());
    }
    
    ...
}

내 프로젝트의 경우 커스텀 예외처리 핸들러에서 ResponseEntityExceptionHandler를 상속받기 때문에 해당 예외를 오버라이드해서 처리했다. MethodArgumentNotValidException 예외 발생 시 DefaultMessage(DTO에서 constraints에 설정했던 message)를 ErrorResponse에 담아준다.

 

{
  "result": false,
  "code": "400",
  "message": "비밀번호는 공백일 수 없습니다."
}

멤버등록 메소드에 password 필드를 넣지 않으면 위와 같은 에러를 반환한다.

 

@Slf4j
@RestControllerAdvice
public class CustomExceptionHandler {
    ...
    
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request) {
        String errorMessage = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage();
        return new ResponseEntity<>(new ErrorResponse(String.valueOf(ex.getStatusCode().value()), errorMessage), ex.getStatusCode());
    }
    
    ...
}

만약 본인의 프로젝트의 예외처리 핸들러가 ResponseEntityExceptionHandler를 상속받고 있지 않다면 @ExceptionHandler 어노테이션으로 해당 예외를 받아서 처리하면 된다.

 

ResponseEntityExceptionHandler를 상속받는데 @ExceptionHandler를 통해 예외를 받게 되면 이미 해당 예외를 처리하고 있기 때문에 "Ambiguous @ExceptionHandler method mapped for ..." 와 같은 에러 메세지를 만날 것이다.

 

 

-끝-