[42일차] @ExceptionHandler, @RestControllerAdvice

유태형·2022년 6월 28일
0

코드스테이츠

목록 보기
42/77

오늘의 목표

  1. @ExceptionHandler
  2. @RestControllerAdvice



내용

@ExceptionHandler

예외발생시 ResponseBody에 에러메시지가 포함되어 클라이언트에게 전송됩니다. 하지만 클라이언트는 전송된 에러메세지만 보고 어디어 어떤 예외가 발생한 에러인지 알 수가 없기 때문에 서버측에서 에러에 대한 상세정보를 전달해 주어야 합니다.

@ExceptionHandler에너테이션이 붙은 메서드를 컨트롤러에 추가하여 클라이언테에게 좀더 상세한 에러메세지를 전달할 수 있습니다.

@RestController
@RequestMapping("공통경로")
@Validated
@Slf4j
public class 컨트롤러{
	....
    
    @ExceptionHandler
    public ResponseEntity handleException(MethodArgumentValidException e){
    	final List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        
        return new ResponseEntity<>(fieldErrors,HttpStatus.BAD_REQUEST);
    }
}

MethodArgumentValidException 예외상황이 발생하면 컨트롤러는 @ExceptionHandler 에너테이션이 있는 메서드들 중 MethodArgumentValidException 매개변수가 있는 에러처리 메서드를 실행시킵니다.



Error클래스

모든 에러정보를 클라이언트에게 전달 할 수도 있으나, 클라이언트는 모든 에러정보가 다 필요하지 않고 일부만 필요할 수도 있습니다. 그럴때 Error정보를 담을 클래스를 정의하여 해당 에러정보만 포함시켜 클라이언트에게 응답보낼 수 도 있습니다.

@Getter
@AllArgsConstructor
public class ErrorResposne{
	private List<FieldError> fieldErrors;
    
    @Getter
    @AllArgsConstructor
    public static class FieldError{
    	private String field;
        private Object rejectedVALUE;
        private String reason;
    }
}

List<FieldError>객체는 항상 하나의 필드만 예외가 발생할 수 있는게 아니라 1개이상의 복수의 필드에서 동시에 예외가 발생할 수도 있기 때문에 List를 멤버변수로 에러가 발생한 필드들으 저장합니다.

static class FieldError : 예외가 발생한 하나의 필드에 대하여 필드명, 거부된값, 이유등을 저장할 변수를 가지는 내부 static class입니다.

모든 에러 정보를 보내던 기존의 방식 필요한 정보만을 담는 정의된 클래스에 담아 전송하는 방식으로 컨트롤러에서 매핑을 진행해보겠습니다.

@RestController
@RequestMapping("기존경로")
@Validated
@Slf4j
public class 컨트롤러{
	//헨들러 메서드
	...	

	@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 ErrorResposne(errors), HttpStatus.BAD_REQUEST);
    }
    
    @ExceptionHandler
    public ResponseEntity handleException(ConstraintViolationException e){
    	
        return new ResponseEntity(HttpStatus.BAD_REQUEST);
    }
}

@ExceptionHandler메서드 내에서 모든 에러 정보를 담고있는 객체 -> 정의한 객체로 스트림을 통한 매핑이 이루어졌습니다.

@ExceptionHandler를 추가하여 다른 예외 객체를 매개변수로 받는 메서드를 정의하면 또 다른 예외 상황을 처리할 수 있습니다.




@RestControllerAdvice

컨트롤러에 직접 @ExceptionHandler 애너테이션 메서드 추가시 에러의 종류마다 추가해야하고 또 컨트롤러 마다 추가해야해서 코드의 중복이 발생합니다.

@RestControllerAdivce 애너테이션이 있는 클래스에 @ExceptionHandler 사용시 컨트롤러들이 메서드를 공유해서 사용할 수 있습니다.

@RestControllerAdvice
public clas Exception어드바이스{

	@ExceptionHandler
    public ResponseEntity 예외처리메서드1(
    	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(Collecttors.toList());
        return new ResponseEntity<>(ErrorResponse(erorrs), HttpStatus.BAD_REQUEST);
   	}
    
    @ExceptionHandler
    public ResponseEntity 예외처리메서드2(
    	ConstraintViolationException e){
    	   
    }
}

A컨틀롤러도, B컨트롤러도 @RestControllerAdvice가 있는 어드바이스를 공유해서 사용할수 있습니다.

위의 코드에서 MethodArgumentNotValidException예외와 ConstraintViolationException 예외를 어떤 컨트롤러든 공유할 수 있습니다.

Controller 클래스에서 발생하는 예외를 자동으로 도맡아서 처리합니다.



어드바이스에서 Error클래스로 역할 분담

어드바이스에서 해당하는 예외(Exception)이 선택되어 예외 정보를 가져와서 원하는 정보만 필터링 하는 역할까지 모두 수행하였습니다.

하지만, 하나의 객체는 하나의 관심사만 처리하는 객체지향 프로그래밍의 의의를 벗어나게 됩니다.

어드바이스는 해당하는 예외(MethodArgumentNotValidException, ConstraintViolationException)의 정보를 가져오고, 에러클래스에서 원하는 정보를 필터링하도록 역할을 분할 하겠습니다.

Error클래스

@Getter
public class ErrorResponse{
	private List<FieldError> fieldErrors;
    private List<ConstraintViolationError> violationErrors;
    
    private ErrorResponse(final List<FieldError> fieldERRORS,
    					  final List<ConstraintViolationError> violationErrors){
    	this.fieldErrors = fieldErrors;
        this.violationErrors = violationErrors;
    }
    
    public static ErrorResponse of(BindingResult bindingResult) {
    	return new ErrorResponse(FieldError.of(bindingResult), null);
    }
    
    public static ErrorResponse of(Set<ConstraintViolation<?>> violations) {
    	return new ErrorResponse(null, ConstraintViolationError.of(violatoins));
    }
    
    @Getter
    public static class FieldError{
    	private String field;
        private Object rejectedValue;
        private String reasson;
        
        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());
        }
    }
    
    @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(),
                    constraintViolatino.getInValidValue().toString(),
                    constraintViolation.getMessage()
                )).collect(Collectors.toList());
        }
    }
}

Error클래스 내부에 static 클래스가 2개 존재합니다. 각각의 static 클래스는 각각의 예외를 정의한 클래스에 맞도록 필터링하는 of메서드를 가집니다.

Error클래스내부의 of메서드는 각각의 스태틱클래스의 of메서드를 이용하여 생성자를 맞춰서 호출하게 됩니다.

Error클래스의 생성자의 접근지정자는 private지만 of메서드에서 매개변수(예외)에 맞게 다르게 생성하게 되므로 예외마다 Error클래스의 객체가 달라지게 됩니다.

Exception 핸들러

@RestControllerAdvice
public class GlobalExceptionAdvice {
	@ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMethodArgumentNotValidException(
    	MethodArgumentNotValidException e){
     	final ErrorResponse response = ErrorResponse.of(e.getBindingResult());
        
        return response;
    }
    
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleConstraintViolationException (
    	ConstraintViolationException e){
     	final ErrorResponse response = ErrorResponse.of(e.getConstraintViolations());
        
        return response;
    }
}

@ResponseStatus(HttpStatus.BAD_REQUEST)는 Request상태코드 404를 전달합니다. Error클래스에서 해당되는 정보만 매핑하므로 @ExceptionHandler에선 해당하는 Error클래스의 of메서드만 실행시켜주면 됩니다.




후기

컨트롤러 에러처리를 배웠습니다. 에러는 여러 종류가 있고 그것을 처리하기 위한 데이터타입도 무척 다야하다는것을 알 수 있었습니다. @RestControllerAdvice와 @ExceptionHandler를 적절히 사용하여 코드 중복을 줄이고 에러에 대처하는것이 중요한것 같습니다.




GitHub

없음!

profile
오늘도 내일도 화이팅!

0개의 댓글