본문 바로가기
Spring

Spring - ExceptionHandler

by icblue21 2022. 11. 21.
728x90

이번 포스트에서는 스프링의 예외처리에 대해 정리해보았다.

스프링의 기본 예외처리 방식

다음과 같은 예시코드가 있다.

@Service
public class MemberService implements IMemberService {

    private MemberDAO memberDAO; // null

		public MemberService() {}

    //@Autowired
    //public MemberService(MemberDAO memberDAO) {
    //    this.memberDAO = memberDAO;
    //}
	
		...
}

원래라면 MemberDAO가 Autowired를 통해 외부에서 객체가 주입이 되어야 하는데 그러지 못하여 null인 상태이다. 이 상태에서 고객이 회원가입을 하면 내부에서는 데이터베이스에 접근하여 회원 가입 서비스를 처리해야하는데 데이터베이스 접근 객체가 null인 상태라 NullPointerException 예외가 발생한다.

[예외 발생 결과]

이 예외를 보면 컨트롤러 하위에서 예외가 발생했을 때 별도의 예외처리를 하지 않으면 WAS까지 에러를 전달한다는 것을 알 수 있다. 왜냐하면 이 에러 페이지는 Tomcat에 내장되어 있는 에러페이지 이기 때문이다.

@ResponseStatus

@ResponseStatus는 에러 HTTP Status 상태를 변경하도록 도와주는 어노테이션이다.
@ResponseStatus은 다음과 같이 사용할 수 있다.

@ResponseStatus(HttpStatus.BAD_REQUEST) 
// InputInvalidException -> 400 -> WAS -> erorr/400.jsp
public class InputInvalidException extends Exception {
    public InputInvalidException() {
        super();
    }

    public InputInvalidException(String message) {
        super(message);
    }
}

Exception 클래스에 어노테이션을 붙이면 ResponseStatusExceptionResolver가 @ResponseStatus를 처리한다. 해당 예외를 WAS까지 전달시키고 복잡한 WAS 에러 요청 전달이 진행된다.

  • 한계점
    • 별도의 응답 상태가 필요하면 예외 클래스를 추가해야 한다.
    • 예외 클래스와 강하게 결합되어있어 같은 예외는 같은 상태와 에러 메시지를 반환한다.
    • WAS까지 예외가 전달되고 WAS 에러 요청 전달이 진행된다.
    • 외부에서 정의한 예외 클래스는 @ResponseStatus 사용할 수 없다.

ResponseStatusException

스프링 5.0에서 @ResponseStatus의 프로그래밍 대안으로 손쉽게 에러를 반환할 수 있는 ResponseStatusException이 추가되었다. ResponseStatusException은 HttpStatus와 함께 선택적으로 reason과 cause를 추가할 수 있다. unchecked exception을 상속하고 있어 명시적으로 에러 처리하지 않아도 된다.

깨알 - checked exception과 unchecked exception ?

  • checked exception
    • RuntimeException의 하위 클래스가 아니면서 Exception 클래스의 하위 클래스들을 의미
    • 반드시 예외 처리를 해야함 (try/catch)
  • unchecked exception
    • RuntimeException의 하위 클래스들을 의미 (말 그대로 실행 중에(runtime) 발생할 수 있는 예외를 의미)
    • 예외 처리를 강제하지 않음

사용법

@RestController
@RequestMapping("")
public class SignupRestController {
    ...

    @PostMapping("/signup")
    public ResponseEntity<Status> doSignup(@ModelAttribute MemberDTO memberDTO, BindingResult bindingResult) throws InputEmptyException, DatabaseDuplicateException, InputInvalidException {
	      try {
						memberService.signup(memberDTO.getuId(), Password.of(memberDTO.getuPwStr()), memberDTO.getuEmail());
						return new ResponseEntity<>(Status.SUCCESS, HttpStatus.OK);
				} catch (NullPointerException e) {
						throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "BAD REQUEST");
				}
    }

		...

}

@ResponseStatus와 동일하게 예외 발생시 ResponseStatusExceptionResolver가 에러를 처리

장점

  • 기본적인 예외 처리를 빠르게 적용할 수 있으므로 손쉽게 프로토타이핑할 수 있음
  • HttpStatus를 직접 설정하여 예외 클래스와의 결합도를 낮출 수 있음
  • 불필요하게 많은 별도의 예외 클래스를 만들지 않아도 됨
  • 프로그래밍 방식으로 예외를 직접 생성하므로 예외를 더욱 잘 제어할 수 있음

한계점

  • 직접 예외 처리를 프로그래밍하므로 일관된 예외 처리가 어려움
  • 예외 처리 코드가 중복될 수 있음
  • Spring 내부의 예외를 처리하는 것이 어려움
  • 예외가 WAS까지 전달되고 WAS의 에러 요청 전달이 진행됨

ExceptionHandler

@ExceptionHandler는 어노테이션을 통해 에러를 매우 유연하고 쉽게 처리할 수 있게 도와준다.

사용법

  • @ControllerAdvice나 @RestControllerAdvice가 있는 클래스의 메소드에 사용한다.
@RestController
@RequestMapping("")
public class SignupRestController {
    ...

		// NullPointerException 발생할 수 있는 컨트롤러 함수
		...

		@ExceptionHandler(NullPointerException.class)
    public ResponseEntity<Status> handleNullPointerException(NullPointerException exception) {
				return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(exception.getMessage());
    }

		...

}

Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다. @ExceptionHandler 예외 클래스를 지정하지 않으면 파라미터에 설정된 에러 클래스를 처리하게 된다.
@ResponseStatus와 결합 가능하다 (ResponseEntity에서도 status를 지정하고, @ResponseStatus가 있다면 ResponseEntity가 우선이다.)

  • 장점
    • @ResponseStatus와 달리 에러 응답 (payload)을 자유롭게 다룰 수 있음
    • 응답을 다음과 같이 정의해서 내릴 수 있음
      • code - 어떠한 종류의 에러가 발생하는지에 대한 에러 코드
      • message - 왜 에러가 발생했는지에 대한 설명
      • errors - 어느 값이 잘못되어 @Valid에 의한 검증이 실패한 것인지를 위한 에러 목록

@ControllerAdvice, @RestControllerAdvice

스프링은 전역적으로 @ExceptionHandler를 적용할 수 있는 컨트롤러를 제공한다.

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler({ NullPointerException.class, InputEmptyException.class, InputInvalidException.class, DatabaseDuplicateException.class })
    public ModelAndView handleExceptions(HttpServletRequest request, Exception exception) {
        System.out.println(request.getRequestURI() + " raised " + exception);

        ModelAndView modelAndView = new ModelAndView("error/error");
        modelAndView.addObject("exception", exception);
        modelAndView.addObject("url", request.getRequestURL());
        return modelAndView;
    }

}

장점

  • 하나의 클래스로 모든 컨트롤러에 대해 전역적으로 예외처리 가능함
  • 직접 정의한 에러 응답을 일관성 있게 클라잉너트에게 내려줄 수 있음
  • 별도의 try-catch 구문이 없어 코드의 가독성이 높음

주의사항

  • 여러 ControllerAdvice가 있을 때 @Order로 순서를 지저하지 않으면 임의의 순서로 처리함
  • 일관된 예외 응답을 위해 한 프로젝트당 하나의 ControllerAdvice를 관리하는 것이 좋음
  • 여러개의 ControllerAdvice가 필요하면 basePackages나 annotations 등을 지정해야 함
  • 직접 구현한 예외 클래스는 한 공간에서 관리하는 것이 보편적임

스프링 예외처리 흐름

스프링에는 다음과 같은 예외처리 전략이 스프링 빈으로 등록되어 있다.

  • ExceptionHandlerExceptionResolver 스프링 디폴트 전략
    • 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
    • 컨트롤러 내부에서 발생하는 예외처리
  • ResponseStatusExceptionResolver 스프링 디폴트 전략
    • Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
    • 컨트롤러 내부에서 발생하는 예외처리
  • DefaultHandlerExceptionResolver 스프링 디폴트 전략
    • 스프링 내부의 기본 예외들을 처리함
    • 스프링에서 발생하는 주요 예외를 처리하는 표준 예외 처리기 (400, 404, 500)
    • 스프링 내부에서 발생하는 예외를 처리하기 위한 용도이므로 우리가 신경쓸 필요없음
    • 컨트롤러 내부에서 발생하는 예외처리
  • SimpleMappingExceptionResolver
    • web.xml에 error-page를 지정하여 특정 예외를 처리할 뷰를 매핑
    • exception 별로 error-page를 매핑할 수 있는 기능 제공
    • 실제로 사용하기 가장 편리함
      • 사용자에게 부담스러운 HTTP 상태코드와 예외 메시지를 던지지 않음
      • 친절하게 예외 페이지를 보여주는 것이 좋음
    • 컨트롤러 외부 발생하는 예외처리
      • 만약 Dispatcher Servlet 이전에 Filter에서 예외처리가 난다면 Web Application 레벨에서 에러를 처리해야 함
      • 예외 발생시 로그를 남기거나 관리자에게 통보하는 것이 어려움
      • 디폴트 전략이 아니므로 빈을 등록해야 함
      • 예시 코드
        @Configuration
        @EnableWebMvc
        public WebMvcConfig extends WebMvcConfigurerAdapter {
          /*
        				      * simple 예외 리졸버
        				    * */
        				   @Bean(name = "simpleMappingExceptionResolver")
        				   public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        					SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
        		
        					Properties mapping = new Properties();
        					mapping.setProperty("NullPointerException", "error/nullError");
        					mapping.setProperty("ArrayIndexOutOfBoundsException", "error/arrayBoundsError");
        					mapping.setProperty("ArithmeticException", "error/arithmeticError");
        					simpleMappingExceptionResolver.setExceptionMappings(mapping);
        		
        					simpleMappingExceptionResolver.setDefaultErrorView("error/error"); 
        					// 등록되지 않은 exception에 보여줄 뷰
        					return simpleMappingExceptionResolver;
        				    }
        }
      
      

예외처리 흐름

  1. ExceptionHandlerExceptionResolver 동작
    • 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사함
    • 컨트롤러의 @ExceptionHandler에서 처리가능하다면 처리하고, 그렇지 않으면 ControllerAdvice로 넘어감
    • ControllerAdvice안에 적합한 @ExceptionHandler가 있는지 검사하고 없으면 다음 처리기로 넘어감
  2. ResponseStatusExceptionResolver 동작
    • @ResponseStatus가 있는지 또는 ResponseStatusException인지 검사함
    • 맞으면 ServletResponse의 sendError()로 예외를 서블릿까지 전달되고, 서블릿이 BasicErrorController로 요청을 전달함
  3. DefaultHandlerExceptionResolver 동작
    • Spring 내부 예외인지 검사하여 맞으면 에러를 처리하고 아니면 넘어감

'Spring' 카테고리의 다른 글

Spring - ConnectionPool  (0) 2022.11.21
Spring - Log4j2  (0) 2022.11.21
Spring - JUnit+Mock 기반 스프링 단위테스트  (0) 2022.11.21
Spring - ResponseEntity, RequestEntity  (0) 2022.11.21
Spring - @RestController  (0) 2022.11.21

댓글