티스토리 뷰

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/

 

https://medium.com/@jovannypcg/understanding-springs-controlleradvice-cd96a364033f#:~:text=The%20%40ControllerAdvice%20annotation%20was%20first,or%20one%20of%20the%20shortcuts.

 

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

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/MethodArgumentNotValidException.html

 

MethodArgumentNotValidException (Spring Framework 5.3.15 API)

getMessage() Returns diagnostic information about the errors held in this object.

docs.spring.io

 

https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/MessageSourceResolvable.html#getDefaultMessage--

 

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

 

댓글