Spring validataion을 사용해볼 예정이다.
클라이언트로부터 받은 데이터를 @RequestBody, 또는 @RequestParam, @PathVariable 등의 어노테이션을 활용하여 api로 호출하는데 이때 전달 받은 데이터를 검증하는 절차가 필요하다.
가장 대표적으로 회원가입을 할 경우 해당 유저의 이메일은 정상적인가, 폰 번호 형식은 일치하는가 이름은 비어있지않는가 등 컨트롤러에서 해당 데이터를 처리하기 전에 유효성 검사를 먼저 해준다고 생각하면된다. 해당 validation을 통과하지 못할 경우에는 ( MethodArgumentNotValidException ) 에러를 발생하도록 기능을 수행한다.
데이터에 대한 유효성 검증 관련 Annotation
- 해당 Annotation은 Spring Validation에서 제공하는 Annotation이며 VO내에서 사용된다.
| Annotation | 설명 | 참고 |
| @Null | - Null 값만 입력 가능함을 유효성 검증 | @Null String unusedString; |
| @NotNull | - Not NULL 값만 입력 가능함을 유효성 검증 | @NotNull String userName; |
| @NotEmpty | - Null, 빈 문자열 불 가능함을 유효성 검증 | @NotEmpty String userName; |
| @NotBlank | - Null, 빈 문자열, 스페이스만 있는 문자열 불가능 함을 유효성 검증 | @NotBlank String userName; |
| @Size(min=, max=) | - 해당 값에 대한 최소값과 최대값을 지정함을 유효성 검증 | @Size(min=2, max=240) String message; |
| @Max | - 해당 값의 최대값을 지정함을 유효성 검증 | @Max(10) int quantity; |
| @Min | - 해당 값의 최소값을 지정함을 유효성 검증 | @Min(5) int quantity; |
| @Pattern | - 해당 값의 유효성 패턴을 지정함을 유효성 검증 | @Pattern(regexp="\\(\\d{3}\\)\\d{3}-\\d{4}") String phoneNumber; |
| @Future | - 입력된 날짜는 미래의 날짜여야 함을 유효성 검증 | @Future Date eventDate; |
| @Past | - 입력된 날짜는 과거의 날짜여야 함을 유효성 검증 | @Past Date birthday; |
ㅇㅁㄴㅇ
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
@JsonNaming(value = PropertyNamingStrategies.SnakeCaseStrategy.class)
public class UserRegisterRequest {
//@NotNull // != NULL
//@NotEmpty // != NULL && NAME != ""
//@NotBlank // != NULL && NAME != "" && NAME != " "
@Size(min = 1 , max = 12)
@NotBlank
private String name;
@Min(1)
@Max(100)
@NotNull
private Integer age;
@Email
private String email;
@Pattern(regexp= "^\\d{2,3}-\\d{3,4}-\\d{4}$")
private String phoneNumber;
@FutureOrPresent //현재 또는 미래
private LocalDateTime registerAt;
}
일반적인 vo가 아닌 위에서 설명된 어노테이션을 기반으로 필요한 곳에 해당 어노테이션을 붙여주었다.
가장 많이 쓰이는 @NotBlank는 String에서만 사용가능하여 Integer타입에는 @NotNull을 사용한다.
컨트롤러에는 @Valid 어노테이션을 통해서 해당 유효성에 대한 에러를 보내주게된다.
import com.example.validation.model.UserRegisterRequest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@PostMapping("")
public UserRegisterRequest register(@Valid @RequestBody UserRegisterRequest userRegisterRequest){
log.info("init : {}", userRegisterRequest);
return userRegisterRequest;
}
}
해당 vo를 선언하고 잘못된 데이터 형식을 받았을 경우를 살펴보자.

다음과 같은 에러가 발생하였다.
//2023-11-05 21:37:04.889 WARN 7936 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public com.example.validation.model.UserRegisterRequest com.example.validation.controller.UserApiController.register(com.example.validation.model.UserRegisterRequest)
// with 4 errors:
// [Field error in object 'userRegisterRequest' on field 'phoneNumber': rejected value [];
// codes [Pattern.userRegisterRequest.phoneNumber,Pattern.phoneNumber,Pattern.java.lang.String,Pattern];
// arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
// codes [userRegisterRequest.phoneNumber,phoneNumber];
// arguments []; default message [phoneNumber],[Ljavax.validation.constraints.Pattern$Flag;@220b9cc4,^\d{2,3}-\d{3,4}-\d{4}$]; default message ["^\d{2,3}-\d{3,4}-\d{4}$"와 일치해야 합니다]] [Field error in object 'userRegisterRequest' on field 'name': rejected value [];
// codes [NotBlank.userRegisterRequest.name,NotBlank.name,NotBlank.java.lang.String,NotBlank];
// arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
// codes [userRegisterRequest.name,name]; arguments []; default message [name]]; default message [공백일 수 없습니다]] [Field error in object 'userRegisterRequest' on field 'name': rejected value [];
// codes [Size.userRegisterRequest.name,Size.name,Size.java.lang.String,Size];
// arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
// codes [userRegisterRequest.name,name];
// arguments []; default message [name],12,1]; default message [크기가 1에서 12 사이여야 합니다]] [Field error in object 'userRegisterRequest' on field 'registerAt': rejected value [2023-11-05T13:05:10];
// codes [FutureOrPresent.userRegisterRequest.registerAt,FutureOrPresent.registerAt,FutureOrPresent.java.time.LocalDateTime,FutureOrPresent];
// arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
// codes [userRegisterRequest.registerAt,registerAt];
// arguments []; default message [registerAt]]; default message [현재 또는 미래의 날짜여야 합니다]] ]
콘솔에 post로 보낸 데이터 형식에대한 에러를 다음과 같이 보여주게된다.
정상적인 데이터를 삽입한경우에는 정상적으로 데이터를 가져오는 것을 확인할 수 있다!!

해당 로직에서는 데이터에대한 유효성 검사는 진행했지만 에러가 난것에 대해서 클라이언트입장에서는 어떠한 에러가 발생했는지를 알 수가 없다.
해당 에러를 잡고 어떠한 로직이 잘못되었는지까지 클라이언트에게 보내주도록 하자.
우선 비지니스 로직만 컨트롤러에 기입해준다.
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserApiController {
@PostMapping("") //@Valid => 요청이 들어올때 해당 컨트롤러에 대한 검증을 하겠다.
public Api<UserRegisterRequest> register(
@Valid
@RequestBody UserRegisterRequest userRegisterRequest
){
log.info("init : {}",userRegisterRequest);
var getData = userRegisterRequest;
Api<UserRegisterRequest> response = Api.<UserRegisterRequest>builder()
.resultCode(String.valueOf(HttpStatus.OK.value()))
.resultMessage(HttpStatus.OK.getReasonPhrase())
.data(getData).build();
return response;
}
}
해당 validation에 대한 예외를 처리해줄 컨트롤러를 생성해준다. 코드는 다음과 같다.
@Slf4j
@RestControllerAdvice
public class ValidationExceptionHandler {
@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<Api> validationException(
MethodArgumentNotValidException exception
){
log.error("",exception);
var errorMessageList = exception.getFieldErrors().stream()
.map( it ->{
var format = "%s: { %s } 은 %s";
var message = String.format(format, it.getField(),it.getRejectedValue(), it.getDefaultMessage());
return message;
}).collect(Collectors.toList());
var error = Api.Error
.builder()
.errorMessge(errorMessageList)
.build();
var errorResponse = Api
.builder()
.resultCode(String.valueOf(HttpStatus.BAD_REQUEST.value()))
.resultMessage(HttpStatus.BAD_REQUEST.getReasonPhrase())
.error(error)
.build();
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}
}
exception이 발생했을 경우에 errorMessgeList에 해당 필드의 이름 값 어떤 에러메세지를 가지고있는지 map형태로 감싸준다.
해당 에러를 지난 시간에 만들어뒀던 Api vo에 담아서 객체를 생성해준다.
에러에 대한 응답을 내려주기위해서 ResponseEntity타임으로 바디에 담아서 보내주게되면 된다.
잘못된 데이터를 보내줄 경우 다음과 같이 실행된다.
