개발소설

Spring MVC 예외처리 본문

Spring Framework

Spring MVC 예외처리

ChaeHing 2023. 4. 16. 23:33

@ExceptionHandler를 이용한 예외 처리

 

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

    @PostMapping
    public ResponseEntity postMember(@Valid @RequestBody MemberPostDto memberDto) {
        Member member = mapper.memberPostDtoToMember(memberDto);

        Member response = memberService.createMember(member);

        return new ResponseEntity<>(mapper.memberToMemberResponseDto(response),
                HttpStatus.CREATED);
    }
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

@ExceptionHandler
    public ResponseEntity handleException(MethodArgumentNotValidException e) {
        
        final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();

        return new ResponseEntity<>(fieldErrors, HttpStatus.BAD_REQUEST);
    }
  • Controller 내의 @ExceptionHandler 적용
  • postMethod(postMember)에서 유효성 검증이 실패 했을때
    • RequestBody의 유효하지 않은 요청 데이터가 포함되어 있을때
    • MethodArgumentNotValidException 발생
    • @ExceptionHandler가 적용된 handleException()가 메서드가 예외를 전달 받음
    •  e.getBindingResult().getFieldErrors()를 통해 발생한 에러 정보를 저장 (fieldErrors)
    •  ResponseEntity를 통해 ResponseBody로 전달
  • 해당 방법은 에러메시지의 내용을 전체 전달하기 때문에 불필요한 정보까지 전부 Response Body로 전달함
    • 정보 가공이 안됨

 

 

Error Response 클래스를 통한 전달

  • 필요한 정보만 가공하여 전달

ErrorResponse 클래스

package com.codestates.response.v1;

import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.List;

@Getter
@AllArgsConstructor
public class ErrorResponse {
    private List<FieldError> fieldErrors;

    @Getter
    @AllArgsConstructor
    public static class FieldError{
        private String field;
        private Object rejectedValue;
        private String reason;
    }

}
  • 필요한 정보만 필드로 생성
  • fieldErrors가 배열인 이유
    • DTO 클래스에서 검증해야 되는 멤버 변수에서 유효성 검증에 실패하는 멤버 변수들이 하나 이상이 될 수 있기 때문에 유효성 검증 실패 에러 역시 하나 이상이 될 수 있기 때문에
    • JSON 응답 객체가 배열
  • FieldError 클래스는 static 멤버 클래스, ErrorResponse의 멤버

Controller내 적용

import com.codestates.response.v1.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;

import java.util.stream.Collectors;

@ExceptionHandler
public ResponseEntity handleException(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());
    return new ResponseEntity<>(new ErrorResponse(errors), HttpStatus.BAD_REQUEST);
}
  • e.getBindingResult().getFieldErrors()로 생성한 fieldErrors를 ErrorResponse.FieldError로 가공(Stream)
  • ErrorResponse.FieldError(list)를 ResponseEntity를 통해 ResponseBody로 전달
  • 필요한 정보들만 선택적으로 골라서 Response Body를 전달하는 것

 

@ExceptionHandler의 단점 - 현재 예외처리 방법

  • 각각의 Controller에서 (coffee, member등) @ExceptionHandler로 유효성 검증 실패 예외처리를 해야함
    • 코드 중복
  • Controller에서 발생하는 예외가 유효성 검증 실패만 있는것이 아니라 다른 예외도 있기 때문에 Controller내에 @ExceptionHandler을 사용하여 추가적인 예외처리 메서드를 만들어야함
    • 코드 복잡도 및 가독성 

 

 

 

 


 

 

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

  • @RestControllerAdvice 애너테이션을 추가하면 여러 개의 Controller 클래스에서 @ExceptionHandler, @InitBinder 또는 @ModelAttribute가 추가된 메서드를 공유해서 사용
  • @RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화 (AOP)

 

ExceptionAdvice 클래스

  • Controller 클래스에서 발생하는 예외들을 공통으로 처리할 클래스
import com.codestates.response.v2.ErrorResponse;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolationException;

@RestControllerAdvice
public class GlobalExceptionAdvice {
    
    // (1)
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodArgumentNotValidException(
            MethodArgumentNotValidException e) {
        final ErrorResponse response = ErrorResponse.of(e.getBindingResult());

        return response;
    }

    // (2)
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleConstraintViolationException(
            ConstraintViolationException e) {
        final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());

        return response;
    }
}
  • @RestControllerAdvice 애너테이션 추가시 Controller 클래스에서 발생하는 예외를 처리
  • @ResponsStatus 애너테이션을 이용해서 Http Status를 Http Response에 포함
  • (1) : 유효성 검증 예외 처리 (MethodArgumentNotValidException)
    • of() 메서드를 호출, 아규먼트로 BindingResult를 보낸다.
  • (2) : uri 가 변수로 넘어오는 경우 예외 (ConstraintViolationException)
    • of() 메서드를 호출, 아규먼트로 ConstraintViolations을 보낸다.
    • patch, delete 메서드 등에서 특정 엔드포인트로 접근할때
    • http://127.0.0.1:8080/v5/members/1 - 멤버 1로 접근
    • 0이나 음수등으로 접근하는 경우 ConstraintViolationException 예외 발생

ErrorResponse 수정

package com.codestates.response.v2;

import lombok.Getter;
import org.springframework.validation.BindingResult;

import javax.validation.ConstraintViolation;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

@Getter
public class ErrorResponse {

    private List<FieldError> fieldErrors; // (1)
    private List<ConstraintViolationError> violationErrors;  // (2)

    // (3)
    private ErrorResponse(List<FieldError> fieldErrors, List<ConstraintViolationError> violationErrors) {
        this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }

    // (4) BindingResult에 대한 ErrorResponse 객체 생성
    public static ErrorResponse of(BindingResult bindingResult) {
        return new ErrorResponse(FieldError.of(bindingResult), null);
    }

    // (5) Set<ConstraintViolation<?>> 객체에 대한 ErrorResponse 객체 생성
    public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
        return new ErrorResponse(null, ConstraintViolationError.of(violations));
    }

    // (6) Field Error 가공
    @Getter
    public static class FieldError {
        private String field;
        private Object rejectedValue;
        private String reason;

        private FieldError(String field, Object rejectedValue, String reason) {
            this.field = field;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<FieldError> of(BindingResult bindingResult) {
            final List<org.springframework.validation.FieldError> fieldErrors =
                    bindingResult.getFieldErrors();
            return fieldErrors.stream()
                    .map(error -> new FieldError(
                            error.getField(),
                            error.getRejectedValue() == null ?
                                    "" : error.getRejectedValue().toString(),
                            error.getDefaultMessage()))
                    .collect(Collectors.toList());
        }
    }

    // (7) ConstraintViolation Error 가공
    @Getter
    public static class ConstraintViolationError {
        private String propertyPath;
        private Object rejectedValue;
        private String reason;

        private ConstraintViolationError(String propertyPath, Object rejectedValue,
                                         String reason) {
            this.propertyPath = propertyPath;
            this.rejectedValue = rejectedValue;
            this.reason = reason;
        }

        public static List<ConstraintViolationError> of(
                Set<ConstraintViolation<?>> constraintViolations) {
            return constraintViolations.stream()
                    .map(constraintViolation -> new ConstraintViolationError(
                            constraintViolation.getPropertyPath().toString(),
                            constraintViolation.getInvalidValue().toString(),
                            constraintViolation.getMessage()
                    )).collect(Collectors.toList());
        }
    }

}
  • (1) : MethodArgumentNotValidException으로부터 발생하는 에러 정보를 담는 멤버 변수
  • (2) : ConstraintViolationException으로부터 발생하는 에러 정보를 담는 멤버 변수
  • (3) : ErrorResponse 클래스의 생성자 접근제어자를 private로 하여 new()를 통한 생성 불가
  • (4) : MethodArgumentNotValidException에 대한 ErrorResponse 객체를 생성
    • **of() 메서드를 호출하는 쪽에서 BindingResult 객체를 파라미터로 넘겨야 한다. - 생성자 오버로딩
    • 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 FieldError 클래스에게 위임
  • (5) : ConstraintViolationException에 대한 ErrorResponse 객체를 생성
    • of() 메서드를 호출하는 쪽에서  Set<ConstraintViolation<?>> 객체를 파라미터로 넘겨야 한다. - 생성자 오버로딩
    • 에러 정보를 추출하고 가공하는 일은 ErrorResponse 클래스의 static 멤버 클래스인 ConstraintViolationError 클래스에게 위임
  • (6) : 필드(DTO 클래스의 멤버 변수)의 유효성 검증에서 발생하는 에러 정보를 생성 (가공)
  • (7) : URI 변수 값에 대한 에러 정보를 생성 (가공)

 

**of() 메서드

  • 정적 팩토리 메서드
    • 직접적(new)으로 생성자를 통해 객체를 생성하는 것이 아닌 메서드를 통해서 객체를 생성하는 것
  • 네이밍 컨벤션(Naming Convention) 
    • 주로 객체 생성 시 어떤 값들의(of~) 객체를 생성한다는 의미에서 of() 메서드를 사용

 

요약

  • Controller 클래스 레벨에서 @ExceptionHandler 애너테이션을 사용하면 해당 Controller에서 발생하는 예외를 처리할 수 있다.
  • 필요한 Error 정보만 담을 수 있는 Error 전용 Response 객체를 사용하면 클라이언트에게 조금 더 친절한 에러 정보를 제공할 수 있다.
  • @ExceptionHandler 애너테이션 방식은 Controller마다 동일하게 발생하는 예외 처리에 대한 중복 코드가 발생할 수 있다.
  • @ExceptionHandler 애너테이션 방식은 다양한 유형의 예외를 처리하기에는 적절하지 않은 방식이다.
  • @RestControllerAdvice 애너테이션을 추가한 클래스를 이용하면 예외 처리를 공통화할 수 있다.
  • @RestControllerAdvice 애너테이션을 사용하면 JSON 형식의 데이터를 Response Body로 전송하기 위해 ResponseEntity로 래핑 할 필요가 없다.
  • @ResponseStatus 애너테이션으로 HTTP Status를 대신 표현할 수 있다.
  • 너무 많은 @RestControllerAdvice를 사용하면 애플리케이션 실행 타임이 늘어난다. -적절한 개수를 잘 정해서 사용
Comments