Notice
Recent Posts
Recent Comments
Link
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
Archives
Today
Total
관리 메뉴

Develog

코드스테이츠 44일차 본문

코드스테이츠

코드스테이츠 44일차

안형준 2022. 6. 28. 18:26

학습 목표

  • API 계층과 서비스 계층에서 발생하는 예외를 처리할 수 있다.
  • 예외 발생 시, 클라이언트에게 예외 메시지를 전달할 수 있다.
  • @ExceptionHandler 애너테이션을 사용해서 예외를 처리할 수 있다.
  • @RestControllerAdvice 애너테이션을 사용해서 예외를 처리할 수 있다.

 

@ExceptionHandler를 이용한 예외 처리

회원 등록 시, 유효성 검증에 실패할 경우의 응답 메시지 예

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

나도 최근에 페어 프로그래밍을 진행했을 때 이와 비슷한 에러로 골머리를 앓은 기억이 난다.

그렇기에 이러한 문제를 해결하기 위해 클라이언트 쪽에서 에러메시지를 조금 더 구체적으로 알 수 있도록 바꾸는 작업이 필요할 것이다.

 

@ExceptionHandler를 이용한 Controller 레벨에서의 예외 처리

Spring에서의 예외는 애플리케이션에 문제가 발생할 경우, 이 문제를 알려서 처리하는 것 뿐만 아니라 유효성 검증에 실패했을 때와 같이 이 실패를 하나의 예외로 간주하여 이 예외를 던져(throw) 예외 처리를 유도한다.

@ExceptionHandler

    public ResponseEntity handleException(MethodArgumentNotValidException e) {

// (1)

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



// (2)

        return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);

    }

위와같이 @ExceptionHandler 애너테이션을 이용해 예외를 처리하도록 handleException() 메서드를 추가할 수 있다.

그리고 실행을 하게되면

[

    {

        "codes": [

            "Email.memberPostDto.email",

            "Email.email",

            "Email.java.lang.String",

            "Email"



…



"defaultMessage": "올바른 형식의 이메일 주소여야 합니다",

        "objectName": "memberPostDto",

        "field": "email",

        "rejectedValue": "hgd@",

        "bindingFailure": false,

        "code": "Email"

    }

]

이와같이 MemberController의 handleException() 메서드에서 유효성 검사 실패에 대한 에러 메시지를 구체적으로 전송해주기 때문에 클라이언트 입장에서는 어느 곳에 문제가 있는지 정확하게 알 수 있다.

 

하지만 여기서 의문점이 들 수 있다.

Response Body 전체 정보를 굳이 다 전달 받을 필요는 없어보이는데…

그렇다 우리는 필요한 정보만 받으면 되기때문에 에러 정보를 기반으로 하여 아래와 같이 Error Response 클래스를 만들어서 필요한 정보만 담은 후에 클라이언트 쪽에 전달해주면 된다.

@Getter

@AllArgsConstructor

public class ErrorResponse {

// (1)

    private List<FieldError> fieldErrors;



    @Getter

    @AllArgsConstructor

    public static class FieldError {

        private String field;

        private Object rejectedValue;

        private String reason;

    }

}

위 코드에서는 응답 객체로 배열을 사용했다. 왜 그럴까?

DTO 클래스에서 검증해야 되는 멤버 변수에서 유효성 검증에 실패하는 멤버 변수들이 하나 이상이 될 수 있기 때문에 유효성 검증 실패 에러 역시 하나 이상이 될 수 있다는 의미이다.

그렇기 때문에 (1)과 같이 한 개 이상의 유효성 검증에 실패한 필드의 에러 정보를 담기 위해서 List 객체를 이용하며, 이 한개의 필드 에러 정보는 FieldError 라는 별도의 static class를 ErrorResponse 클래스의 멤버 클래스로 정의했다.

 

FieldError 클래스는 ErrorResponse 클래스 내부에 정의되어 있다고 해서 내부(Inner) 클래스라고 부르기보다는 ErrorResponse 클래스의 static 멤버 클래스라고 부르자

또한 ErrorResponse는 에러 정보만 담는 클래스이기 때문에 필드의 에러 정보를 담는 FieldError 클래스 역시 에러라는 공통의 관심사를 가지고 있으므로 ErrorResponse의 멤버라고 할 수 있다.

 

이제 ErrorResponse클래스를 만들었기에 MemberController의 handleException() 메서드를 수정해보자

@ExceptionHandler

    public ResponseEntity handleException(MethodArgumentNotValidException e) {

// (1)

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



// (2)

        List<ErrorResponse.FieldError> errors =

                fieldErrors.stream()

                            .map(error -> new ErrorResponse.FieldError(

                                error.getField(),

                                error.getRejectedValue(),

                                error.getDefaultMessage()))

                            .collect(Collectors.toList());



        return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);

    }

(1)의 List<FieldError>를 통째로 ResponseEntity 클래스에 실어서 전달했지만 이번에는 (2)와 같이 필요한 정보들만 선택적으로 골라서 ErrorResponse.FieldError 클래스에 담아서 List로 변환 후, List<ErrorResponse.FieldError>를 ResponseEntity 클래스에 실어서 전달하고 있다.

 

@ExceptionHandler의 단점

@ExceptionHandler 애너테이션으로 에러 처리를 하게되면 다음과 같은 문제점이 발생할 수 있다.

1. 각각의 Controller 클래스에서 @ExceptionHandler 애너테이션을 사용하여 Request Body에 대한 유효성 검증 실패에 대한 에러 처리를 해야되므로 각 Controller 클래스마다 코드 중복이 발생한다.

2. Controller에서 처리해야 되는 예외(Exception)가 유효성 검증 실패에 대한 예외(MethodArgumentNotValidException)만 있는것이 아니기 때문에 하나의 Controller 클래스 내에서 @ExceptionHandler를 추가한 에러 처리 핸들러 메서드가 늘어닐 수 밖에 없다.

총정리

  • Controller 클래스 레벨에서 @ExceptionHandler 애너테이션을 사용하면 해당 Controller에서 발생하는 예외를 처리할 수 있다.
  • 필요한 Error 정보만 담을 수 있는 Error 전용 Response 객체를 사용하면 클라이언트에게 조금 더 친절한 에러 정보를 제공할 수 있다.
  • @ExceptionHandler 애너테이션 방식은 Controller마다 동일하게 발생하는 예외 처리에 대한 중복 코드가 발생할 수 있다.
  • @ExceptionHandler 애너테이션 방식은 다양한 유형의 예외를 처리하기에는 적절하지 않은 방식이다.

 

@RestControllerAdvice를 사용한 예외 처리 공통화

특정 클래스에 @RestControllerAdvice 애너테이션을 추가하면 여러개의 Controller 클래스에서 @ExceptionHandler, @InitBinder 또는 @ModelAttribute 가 추가된 메서드를 공유해서 사용할 수 있다.

@RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화 할 수 있다는 것이다.

 

여기서 @InitBinder 와 @ModelAttribute 애너테이션이란?

@InitBinder 와 @ModelAttribute 애너테이션은 JSP, Thymeleaf 같은 서버 사이드 렌더링(SSR, Server Side Rendering) 방식에서 주로 사용되는 방식인데, 궁금한 점이 있다면 따로 찾아보자

 

위에서 언급한 @ExceptionHandler 애너테이션의 단점을 GlobalExceptionAdvice 클래스를 통해 개선하는 과정을 진행해보자

MemberController 클래스에서 @ExceptionHandler 로직 제거

 

ExceptionAdvice 클래스 정의

import org.springframework.web.bind.annotation.RestControllerAdvice;



@RestControllerAdvice

public class GlobalExceptionAdvice {



}

예외를 처리할 ExceptionAdvice 클래스에 @RestControllerAdvice 애너테이션을 추가하면 이 클래스는 이제 Controller 클래스에서 발생하는 예외를 도맡아서 처리하게 된다.

 

Exception 핸들러 메서드 구현

이제 GlobalExceptionAdvice 클래스에서 처리할 Exception 핸들러 메서드를 구현해 주면 된다. (@RestControllerAdvice 잊지말기!)

 

ErrorResponse 수정

 

Exception 핸들러 메서드 수정

수정된 ErrorResponse 클래스의 메서드를 사용하도록 GlobalExceptionAdvice 클래스를 수정한다.

 

@RestControllerAdvice vs @ControllerAdvice

Spring MVC 4.3 버전 이후부터 @RestControllerAdvice 애너테이션을 지원하는데, 둘 사이의 차이점을 한마디로 정리하자면

• @RestControllerAdvice = @ControllerAdvice + @ResponseBody

@RestControllerAdvice 애너테이션은 ****@ControllerAdvice 의 기능을 포함하고 있으며, @ResponseBody 의 기능 역시 포함하고 있기 때문에 JSON 형식의 데이터를 Response Body로 전송하기 위해서 ResponseEntity로 데이터를 래핑할 필요가 없다.

 

총정리

  • @RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화 할 수 있다.
  • @RestControllerAdvice 애너테이션을 사용하면 JSON 형식의 데이터를 Response Body로 전송하기 위해 ResponseEntity로 래핑할 필요가 없다.
  • @ResponseStatus 애너테이션으로 HTTP Status를 대신 표현할 수 있다.

'코드스테이츠' 카테고리의 다른 글

코드스테이츠 46일차  (0) 2022.06.30
코드스테이츠 45일차  (0) 2022.06.29
코드스테이츠 43일차  (2) 2022.06.27
코드스테이츠 42일차  (0) 2022.06.24
코드스테이츠 41일차  (0) 2022.06.23