회원가입 유효성 검사를 위한 Global Custom Validation 설정하기
spring-boot-starter validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
spring-boot-starter validation 을 사용하면 hibernate validation 을 쓰게 됩니다.
@Valid
컨트롤러 단에 @Valid 를 @RequestBody 앞에 붙여서 개별 파라미터가 아닌 리퀘스트 바디 전체를 검증 할 수 있습니다.
@PostMapping("/signup")
public ResponseEntity<MessageRes> signUp(@Valid @RequestBody SignUpReq signUpReq){
...
return new ResponseEntity<MessageRes>(map, HttpStatus.OK);
}
@Pattern, @NotBlank, @Email
SignUpReq 에는 annotation을 붙여서 검증해줍니다.
public class SignUpReq {
@Pattern(regexp="(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}",
message = "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 16자의 비밀번호여야 합니다.")
@NotBlank(message = "비밀번호는 필수 입력 값입니다.")
private String password;
@NotBlank(message = "닉네임은 필수 입력 값입니다.")
private String nickname;
@Email(message = "이메일 형식에 맞지 않습니다.")
@NotBlank
private String email;
}
참고로 service, repository 로직에서 발생하는 에러는 따로 처리해야 합니다.
ResponseEntity 를 반환할 때, 어떤 자료구조로 어떤 데이터를 넣어 반환할지,
Httpstatus 는 처리는 무엇으로 어디에 어떻게 반환할지 생각해야 합니다.
이 부분은 다른 게시물에 정리했고, 여기서는 validation error 처리에 집중해 보겠습니다.
@Server Response
@Valid 에 의해, Validation 관련 Exception이 발생했을 때 출력되는 형식입니다.
delete "trace"
trace 가 출력되는 것은 보안의 위협이 되므로 없애줍니다.
application.properties 에 다음 코드를 추가합니다.
server.error.include-stacktrace=never
trace가 사라졌네요.
그런데 출력 형식이 영 마음에 들지 않습니다.
CustomExceptionHandler
validation 을 통과하지 못했을 때의 Exception 코드를 재정의해봅시다.
사실 이 부분에 있어서 자료를 많이 찾아보았는데, 저의 수준에 맞는 소스가 많지는 않았습니다.
쉽게 따라할 수 있는 간단한 코드를 원했거든요.
Mkyoung 님 좋은 자료 감사합니다.
그리고 도와준 친구 Bryce 고맙습니다.
ResponseEntityExceptionHandler 를 상속받는 CustomExceptionHandler 클래스를 만듭니다.
@ControllerAdvice
public class CustomGlobalExceptionHandler extends ResponseEntityExceptionHandler {
// error handle for @Valid
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status, WebRequest request) {
Map<String, Object> body = new LinkedHashMap<>();
body.put("statusValue", status.value());
body.put("status", status);
// Get all errors
List<String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(x -> x.getDefaultMessage())
.collect(Collectors.toList());
body.put("errors", errors);
return new ResponseEntity<>(body, headers, status);
}
}
MethodArgumentNotValidException
문자 그대로 메서드의 arguments 가 유효하지 않을 경우 반환되는 예외겠지요.
이것과 http header, status, request 를 parameter 로 넣습니다.
다음의 메서드들을 실행하고, 담고, 리스트로 만들어줍니다.
getBindingResult()
getFieldErrors()
getDefaultMessage()
stream()
collect()
"message": "Validation failed for object='signUpReq'. Error count: 1",
"errors": [
{
"codes": [
"Pattern.signUpReq.password",
"Pattern.password",
"Pattern.java.lang.String",
"Pattern"
],
"arguments": [
{
"codes": [
"signUpReq.password",
"password"
],
"arguments": null,
"defaultMessage": "password",
"code": "password"
},
[],
{
"arguments": null,
"codes": [
"(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}"
],
"defaultMessage": "(?=.*[0-9])(?=.*[a-zA-Z])(?=.*\\W)(?=\\S+$).{8,20}"
}
],
"defaultMessage": "비밀번호는 영문 대,소문자와 숫자, 특수기호가 적어도 1개 이상씩 포함된 8자 ~ 20자의 비밀번호여야 합니다.",
"objectName": "signUpReq",
"field": "password",
"rejectedValue": "123s4",
"bindingFailure": false,
"code": "Pattern"
}
],
@ControllerAdvice
이 그림을 생각하면 이해가 쉽겠습니다.
다양한 컨트롤러에서 발생하는 Exception 을 GlobalExceptionHandler 에서 모아 처리하고,
그 핸들러에 어노테이션을 붙임으로써 여러 Controller에 글로벌하게 접근이 가능합니다.
이제 에러가 깔끔하게 출력되네요 ! :)
자료출처
https://mkyong.com/spring-boot/spring-rest-validation-example/
Understanding Spring’s @ControllerAdvice
The @ControllerAdvice annotation was first introduced in Spring 3.2. It allows you to handle exceptions across the whole application, not…
medium.com
MethodArgumentNotValidException (Spring Framework 5.3.15 API)
getMessage() Returns diagnostic information about the errors held in this object.
docs.spring.io
MessageSourceResolvable (Spring Framework 5.3.15 API)
docs.spring.io
추가 자료
https://velog.io/@hanblueblue/Spring-ExceptionHandler
[Spring] ExceptionHandler
커스텀 익셉션 처리와 클라이언트로의 예외처리 메세지 전달
velog.io
https://www.baeldung.com/spring-boot-bean-validation