[Spring Boot] 예외 처리

hellonayeon·2021년 12월 2일
1
post-thumbnail
post-custom-banner

서버에서 클라이언트의 요청을 처리하던 도중 예외가 발생한다면 서버는 디폴트로 HTTP 500 Interval Server Error 상태의 응답을 전송한다. 하지만 서버 프로그램에서 발생하는 예외를 처리해줌으로써 예외에 따라 클라이언트에게 500 응답 이외의 개발자가 정해둔 다른 상태 코드오류 메시지를 내려줄 수 있다.

프로그램에서 예외 처리 구문들을 이용해서 예외를 처리해주면 예외를 맞닥드리더라도 프로그램이 죽지 않고 동작하지만 클라이언트에게 올바른 결과를 리턴하지 못하고 단순히 500 응답을 리턴한다. Spring에서는 예외가 발생했을때 예외를 감지해서 처리할 수 있도록하는 ExceptionHandler 기능을 제공한다. @ExceptionHandler 어노테이션으로 사용할 수 있으며 value 값에 핸들러 함수에서 어떤 예외를 처리할 것인지 적어주면 된다.

@ExceptionHandler(value = { NullPointerException.class })
public ResponseEntity<Object> handleNullPointerException(Exception ex) { ... }

@ExceptionHandler(value = { IllegalArgumentException.class, NullPointerException.class, UsernameNotFoundException.class })
public ResponseEntity<Object> handleNullPointerException(Exception ex) { ... }

예외처리 핸들러 함수는 클래스 내부에서 사용될 수 있다. 클래스 내부에 핸들러를 정의한 경우 해당 클래스에서 발생하는 예외만을 처리한다. 만약 A Controller 에 핸들러 메서드를 정의해놓은 경우 Controller 뿐만 아니라 Controller와 의존 관계를 맺고 있는 다른 계층들에서 발생한 예외도 처리할 수 있다. 하지만 B Controller에서 발생하는 예외는 처리하지 못한다.

@RestController
@RequiredArgsConstructor
public class AController {
	
    private final AService service;
	
    ...
    
    @ExceptionHandler(value = { NullPointerException.class })
	public ResponseEntity<Object> handleNullPointerException(Exception ex) { ... }
}

서비스를 구현하다보면 Controller가 한 개가 아닐텐데.. 그럼 Controller 마다 하나씩 핸들러 메서드를 정의해줘야 할까😱 다행히도 스프링은 AOP 라는 좋은 기능을 제공하고, 예외 처리 로직을 분리해서 하나의 모듈처럼 사용할 수 있다. @ControllerAdvice@RestControllerAdvice를 이용해서 컴포넌트를 생성하고 예외처리 메서드를 작성해놓으면 모든 클래스에 전역적으로 적용이 가능하다. 뿐만아니라 annotations 속성을 통해 특정 Controller 에만 적용할 수 있다.

What is @ControllerAdvice ?

Specialization of @Component for classes that declare @ExceptionHandler, @InitBinder, or @ModelAttribute methods to be shared across multiple @Controller classes.
// 모든 컨트롤러에 예외처리 핸들러 적용
@RestControllerAdvice
public class ApiExceptionHandler {
	...
}   
// 특정 컨트롤러에 예외처리 핸들러 적용
@RestControllerAdvice(annotations = AController.class)
public class ApiExceptionHandler {
	...
}   

@RestControllerAdvice@ControllerAdvice + @ResponseBody를 의미하며 예외 처리의 결과로 리턴되는 값을 자동으로 JSON 형식으로 직렬화해서 보내준다.

@AllArgsConstructor
@Getter
public class ApiException {
    private final String message;
    private final HttpStatus httpStatus;
}
@RestControllerAdvice
public class ApiExceptionHandler {
	@ExceptionHandler(value = { IllegalArgumentException.class, NullPointerException.class, UsernameNotFoundException.class })
    	public ResponseEntity<Object> handleApiRequestException(Exception ex) {
        ApiException apiException = new ApiException(
                ex.getMessage(),
                HttpStatus.BAD_REQUEST
        );

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

}

Ajax를 통해 서비스로 요청을 했고, 서버에서 예외가 발생해서 핸들러 메서드에 의해 오류 응답이 반환됐다면 프론트 콘솔에서 출력해보면, 실제로 JSON 형식의 응답 메시지가 반환된 것을 확인할 수 있다.

$.ajax({
    ...
    error: function (response) {
      console.log(response.responseJSON);
    }
});
{message: '해당되는 아이디(22)의 게시물이 없습니다.', httpStatus: 'BAD_REQUEST'}

테스트 도중에 예외가 발생해서 프론트에서 정상 출력이 안되는데 서버 콘솔에도 오류 내용이 찍히지 않았다. 확인해보니 예외처리 핸들러를 구현할때 핸들러를 적용할 클래스들 목록들 중에 NullPointerException.class 가 있었는데, 프로그램에서 예외처리를 하지 못한 부분에서 발생한 예외도 핸들러로 인터셉트되서 생기는 문제였다. 그래서 IllegalArgumentException을 상속받는 예외 클래스인 ApiRequestException을 하나 더 만들어서 우리가 직접적으로 발생시키는 예외에는 ApiRequestException 객체를 생성하도록 수정했다.

ApiRequestException 클래스

public class ApiRequestException extends IllegalArgumentException {
    public ApiRequestException(String message) {
        super(message);
    }

    public ApiRequestException(String message, Throwable cause) {
        super(message, cause);
    }
}

ApiExceptionHandler 클래스

@RestControllerAdvice
public class ApiExceptionHandler {
    @ExceptionHandler(value = { ApiRequestException.class })
    public ResponseEntity<Object> handleApiRequestException(Exception ex) {
        ApiException apiException = new ApiException(
                ex.getMessage(),
                HttpStatus.BAD_REQUEST
        );

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

예외처리 코드 작성 예시

Article article = articleRepository.findById(id).orElseThrow(
        () -> new ApiRequestException(String.format("해당되는 아이디(%d)의 게시물이 없습니다.", id))
);

참고자료

📌 Spring Framework 5.3.13 API. "ControllerAdvice".

📌 Carrey. "Spring Handle Exception", Carrey's 기술 블로그, 30 Aug 2018.*

📌 rin. "[Spring] ExceptionHandler", keep going, 08 May 2020.

📌 우드콕. "ResponseEntity는 왜 사용하는 것이며 @RestControllerAdvice는 무엇일까.", 02 May 2021.

post-custom-banner

0개의 댓글