[Spring] Exception Handling

Noah·2022년 1월 13일
0

Spring

목록 보기
6/8
post-thumbnail

Exception Handling

말 그대로 예외 처리라고 한다. 보통 유효성 검사에서 많은 예외 처리를 하게 되고 비즈니스 로직을 구현하면서도 하게 된다. 예를 들어 클라이언트로부터 회원 가입을 하기 위해 유저의 나이 속성을 받아와야 한다고 생각해 보자. 나이의 최솟값은 1, 최댓값은 99라고 해보자.

만약 클라이언트가 나잇값을 0을 보낸다면 서버는 잘못된 값이라고 판단하여 에러를 낼 것이다. 여기서 예외 처리가 필요하다. 이 잘못된 값을 클라이언트에 적절한 응답으로 내려줄 필요가 있다. 그래야 클라이언트 또한 무언가 잘못된 값을 보냈음을 알게 될 테니까.


@Exception Handler

@ExceptionHandler은 @Controller 또는 @RestController가 적용된 Bean 내에서 발생하는 예외를 잡아서 하나의 메서드에서 예외를 처리해주는 기능을 한다.

@ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest httpServletRequest) {
        List<Error> errorList = new ArrayList<>();

        BindingResult bindingResult = e.getBindingResult();
        bindingResult.getAllErrors().forEach(error -> {
            FieldError field = (FieldError) error;

            String fieldName = field.getField();
            String message = field.getDefaultMessage();
            String value = field.getRejectedValue().toString();

            Error errorMessage = new Error();
            errorMessage.setField(fieldName);
            errorMessage.setMessage(message);
            errorMessage.setInvalidValue(value);

            errorList.add(errorMessage);
        });

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("");
        errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAIL");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }

그리고 인자 값 value를 통해 특정 예외 클래스가 발생했을 때만 작동하도록 명시할 수 있다.

특징
1. @Service 같은 빈에서는 적용 불가능하다.
2. 반환 타입은 자유롭게 해도 된다. 컨트롤러가 성공적으로 수행했을 때의 반환 타입에 자유롭다는 뜻이다.
3. 각 컨트롤러에서 등록한 @ExceptionHandler는 그 컨트롤러 내부의 예외에 대해서만 유효하다.
4. 지정된 메서드의 파라미터로 다양한 예외 타입을 받아올 수 있다.


@ControllerAdvice

@ControllerAdvice는 모든 @Controller 즉, 전역에서 발생할 수 있는 예외를 잡아 처리해 주는 annotation이다.

@RestControllerAdvice(basePackageClasses = ApiController.class)
// ApiController 클래스에서만 작동하는 예외 처리 클래스가 되었음.
public class GlobalControllerAdvice {

    @ExceptionHandler(value = Exception.class)
    public ResponseEntity exception(Exception e) {
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("");
    }

    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public ResponseEntity methodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest httpServletRequest) {
        List<Error> errorList = new ArrayList<>();

        BindingResult bindingResult = e.getBindingResult();
        bindingResult.getAllErrors().forEach(error -> {
            FieldError field = (FieldError) error;

            String fieldName = field.getField();
            String message = field.getDefaultMessage();
            String value = field.getRejectedValue().toString();

            Error errorMessage = new Error();
            errorMessage.setField(fieldName);
            errorMessage.setMessage(message);
            errorMessage.setInvalidValue(value);

            errorList.add(errorMessage);
        });

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("");
        errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAIL");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }

    @ExceptionHandler(value = ConstraintViolationException.class)
    public ResponseEntity constraintViolationException(ConstraintViolationException e, HttpServletRequest httpServletRequest) {

        List<Error> errorList = new ArrayList<>();

        e.getConstraintViolations().forEach(error -> {

            Stream<Path.Node> stream = StreamSupport.stream(error.getPropertyPath().spliterator(), false);
            List<Path.Node> list = stream.collect(Collectors.toList());

            String field = list.get(list.size() - 1).getName();
            String message = error.getMessage();
            String invalidValue = error.getInvalidValue().toString();

            Error errorMessage = new Error();
            errorMessage.setField(field);
            errorMessage.setMessage(message);
            errorMessage.setInvalidValue(invalidValue);

            errorList.add(errorMessage);
        });

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("");
        errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAIL");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }

    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public ResponseEntity missingServletRequestParameterException(MissingServletRequestParameterException e, HttpServletRequest httpServletRequest) {

        List<Error> errorList = new ArrayList<>();

        String fieldName = e.getParameterName();

        Error errorMessage = new Error();
        errorMessage.setField(fieldName);
        errorMessage.setMessage(e.getMessage());

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setErrorList(errorList);
        errorResponse.setMessage("");
        errorResponse.setRequestUrl(httpServletRequest.getRequestURI());
        errorResponse.setStatusCode(HttpStatus.BAD_REQUEST.toString());
        errorResponse.setResultCode("FAIL");

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
    }
}

basePackageClasses에 적절한 값을 주면 특정 패키지로 예외를 적용할 범위를 제한할 수 있다. 그런데 위의 예제에서 사용된 어노테이션은 RestControllerAdvice인데 둘의 차이는 무엇일까?


@RestControllerAdvice = @ResponseBody + @ControllerAdvice

제목을 보고 유추할 수 있겠지만 @ControllerAdvice와 동일한 역할 즉, 예외 처리 기능을 수행하면서 @ResponseBody를 통해 객체를 리턴할 수도 있다는 얘기다.

API 서버여서 에러 응답으로 객체를 리턴해야 한다면 @ResponseBody 어노테이션이 추가된 @RestControllerAdvice를 적용하면 되는 것이다.


정리
설명한 어노테이션들로 적절한 예외 처리를 할 수 있다.
중요한 것은 각 컨트롤러마다 적용할 커스텀한 예외 처리 핸들러를 작성하는 것과 공통된 전역 예외 처리 핸들러를 잘 구분하는 것이고 또 적절한 에러 인터페이스를 설정하여 클라이언트에게 항상 일관된 에러 양식을 보내주어야 한다는 것이다.

profile
개발 공부는 🌳 구조다…

0개의 댓글