매일 매일 다짐하지만
잠자는 수면패턴 바꾸는건 쉽지가 않은 것 같다 ㅎ..
















어제까지 Spring Service 계층에 대해 공부했다.
오늘은 Spring Exception에 대해 공부하는 날이다.
총 2일로 잡혀있고, 2일이 잡혀있다는 것에 약간 어떤 내용이 있을지
벌써 겁이나기 시작한다 ㅠㅠ..

그래도 차근차근 학습해보자

Spring Exception

클라이언트가 전달 받는 Response Body는
애플리케이션에서 예외가 발생했을 때, 내부적으로 Spring에서
에러 응답 메세지를 전송해준다.
ex) Postman API 요청시, 400 Bad Requset

하지만 이렇게 보내주면 Response Body의 내용만으로
요청 데이터 중에서 어떤 항목이 유효성 검증에 실패했는지 알수가 없다.

이제 클라이언트쪽에 에러메세지를 더 구체적으로 해줄 수 있는
방법에 대해 공부해보자

@ExceptionHandler

Controller 레벨에서 하는 예외처리다.

예를 들어 Controller 부분에서 Handler 매서드를
POST 이용해 Json 값을 받아온다고 가정하였을 경우
이전에 학습한 @Vaild를 이용해 유효성검증을 할 수 있다.

만약에 핸드폰번호 값을 받아오는 유효성검증에 ‘-‘가 없거나 숫자가 아닐떄
등 유효성 실패 조건을 걸어놓고 해당하는 값이 통과되지 못할 때
기존에는 Postman에 400, Bad Request만 발생하였지만





















위와 같이 어떻게 유효성 검증이 실패되었는지
List 형태로 여러가지 정보들을 확인해 볼 수 있는 메서드가
@ExceptionHandler 라고 생각해보면 될 것 같다.

@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e){
    final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
    return new ResponseEntity(fieldErrors, HttpStatus.BAD_REQUEST);
}

위의 코드는 ExceptionHandler 애노테이션을 이용하는 방법이다.
Controller 클래스 아래에 @ExceptionHandler를 사용할
메서드를 만들어주고, MethodArgumentNotVaildException이라는 클래스로
예외에 대한 객체를 받을 수 있다.

이제 객체를 이용해 원하는 데이터들만 호출해
(.getBindingResult().getFieldErrors() 등등..)
ResponseEntity에 상태를 담아서 돌려보내주면

똑같이 Bad Request를 응답 받아도
JSON 객체로 어떤 정보들이 담겨져있는지 친절하게 확인이 가능하다는 점!


데이터 구조는?

그럼 전달 받은 객체의 내용을 자세히 보자면

List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();  

를 통해서 값을 가져온다. fieldErrors를 Json으로 변환하면
아래와 같이 모습이 나오게된다. (이메일형식과 전화번호 형식이 틀렸다고 가정)

[
    {
        "codes": [
            "Pattern.memberPostDto.phone",
            "Pattern.phone",
            "Pattern.java.lang.String",
            "Pattern"
        ],
        "arguments": [
            {
                "codes": [
                    "memberPostDto.phone",
                    "phone"
                ],
                "arguments": null,
                "defaultMessage": "phone",
                "code": "phone"
            },
            [],
            {
                "arguments": null,
                "defaultMessage": "^010-\\d{3,4}-\\d{4}$",
                "codes": [
                    "^010-\\d{3,4}-\\d{4}$"
                ]
            }
        ],
        "defaultMessage": "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다.",
        "objectName": "memberPostDto",
        "field": "phone",
        "rejectedValue": "010-12345678",
        "bindingFailure": false,
        "code": "Pattern"
    },
    {
        "codes": [
            "Email.memberPostDto.email",
            "Email.email",
            "Email.java.lang.String",
            "Email"
        ],
        "arguments": [
            {
                "codes": [
                    "memberPostDto.email",
                    "email"
                ],
                "arguments": null,
                "defaultMessage": "email",
                "code": "email"
            },
            [],
            {
                "arguments": null,
                "defaultMessage": ".*",
                "codes": [
                    ".*"
                ]
            }
        ],
        "defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
        "objectName": "memberPostDto",
        "field": "email",
        "rejectedValue": "dhfif718",
        "bindingFailure": false,
        "code": "Email"
    }
]

구조를 자세히 보면 어렵지 않다.
유효성 검증이 틀린 부분마다 List<>에 정보가 add가 되어진다.
크게 나누자면 아래와 같이

"codes": [],
"arguments": [],
"defaultMessage": "올바른 형식의 이메일 주소여야 합니다",
"objectName": "memberPostDto",
"field": "email",
"rejectedValue": "dhfif718",
"bindingFailure": false,
"code": "Email"

8개의 큰 항목으로 분리되고 그 안에서
codes, arguments를 깊이탐색을 통해서 다른 정보도
접근이 가능하다.


원하는 데이터만?

위의 데이터 구조로만 보았을 때 List로 작성했기 때문에
데이터 접근이 가능하여, 원하는 데이터만 출력이 가능하다.

만약에 field, rejectedValue, reason이란 값만 가져온다고 가정하고
코드를 작성해보자.

@Getter
@AllArgsConstructor
public class ErrorResponse {
    // AllArgsConstructor 덕분에 생성자가 자동으로 생기면 의존성 주입도됨
    private List<FieldError> fieldErrors;

    @Getter
    @AllArgsConstructor
    public static class FieldError{
        private String field;
        private Object rejectedValue;
        private String reason;
    }
}
@ExceptionHandler
public ResponseEntity handleException(MethodArgumentNotValidException e){
    final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
    List<ErrorResponse.FieldError> errors = fieldErrors.stream()
            .peek(error -> System.out.println(error))
            .map(error -> new ErrorResponse.FieldError(
                    error.getField(),
                    error.getRejectedValue(),
                    error.getDefaultMessage()
            ))
            .collect(Collectors.toList());
    return new ResponseEntity(errors, HttpStatus.BAD_REQUEST);
}

편의상 Class 부분과 POST 핸들러 메서드는 생략했다.
이전과 동일하게 fieldErrors라는 객체를 만들고
그 객체를 stream을 이용해 원하는 목록의 데이터를 get하여
생성자로 ErrorResponse.FieldError();을 통해 객체를 만들어준다
그렇게 만들어진 객체를 List안에 넣어 반환해주고

Postman에서 API 요청 시
아래와 같이 우리가 원하는 데이터만 List화하여
Json 객체로 반환하는 모습을 볼 수 있다.

[
    {
        "field": "phone",
        "rejectedValue": "010-12345678",
        "reason": "휴대폰 번호는 010으로 시작하는 11자리 숫자와 '-'로 구성되어야 합니다."
    },
    {
        "field": "email",
        "rejectedValue": "dhfif718naver.com",
        "reason": "올바른 형식의 이메일 주소여야 합니다"
    }
]

이렇게 ExceptionHandler를 사용하여
유효성 검증에 통과하지 못하면, 우리가 원하는대로
커스터마이징하여 클라이언트측에 데이터를 보낼 수 있게된다.

하지만 이 메서드의 큰 단점이 존재한다.

1. MethodArgumentNotValidException 클래스의 예외 말고 다른 예외를 처리할때?
-> 그에 해당하는 에러 핸들러 메서드를 또 만들어줘야함..

2. Controller 클래스가 2개이상 일경우
-> 위에서 했던 메서드들을 각 Controller 클래스마다 똑같이 만들어야함. (중복발생 !!)


@RestControllerAdvice

ExceptionHandler와 같이 사용하게 될 경우
위에서 학습했던 단점을 해결해주기위한 애노테이션이다 !

이전에는 우리가 Controller에 직접
ExecptionHandler를 작성해주었지만..

별도의 클래스를 하나 만들고 @RestControllerAdvice를
클래스 애노테이션을 달아주게된다면
REST API 요청시 Vaild 검증실패할 경우 해당 클래스의
ExceptionHandler에 잡혀 구현된 동작을 실행하게 되는 것이다.

!! SSR 방식에서는 주로 @InitBinder, @ModelAttribute를 사용한다고한다.
우리는 CSR 방식을 사용하기때문에 @ExceptionHandler 사용.

@RestControllerAdvice
public class GlobalExceptionAdvice {

    @ExceptionHandler
    public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e){

        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        List<ErrorResponse.FieldError> errors = fieldErrors.stream()
                .map(error -> new ErrorResponse.FieldError(
                        error.getField(),
                        error.getRejectedValue(),
                        error.getDefaultMessage()
                ))
                .collect(Collectors.toList());

        //최종 객체 Json + 응답상태 반환하기
        return new ResponseEntity(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
    }
}

이전에 Controller에 있었던 구현 메서드를
새로 만든 GlobalExceptionAdivce라는 클래스로 옮기고
@RestControllerAdvice를 달아준 모습이다.

위에서 학습했던 것과 동일하게
원하는 데이터만 뽑아 Json 객체로 돌려주게 하였다

지금은 간단하게
MethodArgumentNotValidException를 통해서
FieldErrors만 가져오도록 프로그램을 하였는데

ConstraintViolationException를 통해서
ConstraintViolationErrors도 가져와야하는 프로그램을
작성해야한다.


한줄 정리

@Vaild 예외처리 - MethodArgumentNotValidException
@Vaildated 예외처리 - ConstraintViolationException


별도의 클래스생성과 애노테이션 하나로 이렇게
프로그램이 간단해지고 보기 편해질줄이야..

정말 먼저 개발을 시작하고 이러한 개념을 정착시킨
선배 개발자분들이 존경스럽다


오늘의 커피량: ☕️ ☕️ ☕️ ☕️
오늘의 점심: 불고기 덮밥, 스팸, 밥