예외처리 제대로 하고 계신가요?

손재민·2023년 11월 25일
1

Spring

목록 보기
1/1
post-thumbnail

안녕하세요. 저는 백엔드 공부를 하고 있는 학생입니다. 프레임워크는 주로 스프링을 이용해 개발하고 있습니다.
프로젝트를 진행하면 주로 Custom Exception을 이용해 예외처리하고 있습니다. 하지만 실제 Spring 내부에서 발생하는 일이나 효율적인 예외처리에 대해 고민을 해본적은 없었던 것 같습니다. 그래서 저는 이 글을 작성하게 되었습니다.
여기에서부터는 문장의 간결함을 위해서 높임말은 생략하겠습니다.

1. 필터(Filter)와 인터셉터(Interceptor)의 차이는?

디스패처 서블릿(Dispatcher Servlet)

  • 서블릿 컨테이너의 가장 앞단에서 HTTP 프로토콜로 들어오는 모든 요청을 먼저 받아 적합한 컨트롤러에 위임해주는 프론트 컨트롤러이다.
  • 클라이언트로부터 요청이 들어오면 모든 요청을 디스패처 서블릿이 가장 먼저 받고, 공통적인 작업을 먼저 처리한 후에 해당 요청을 처리해야 하는 컨트롤러를 찾아서 작업을 위임한다.

→ 이전에는 컨트롤러마다 공통 로직을 복붙 형식으로 다시 작성하여 사용했다.

필터(Filter)

  • J2EE(Java EE) 표준 스펙 기능이다. (Spring의 지원을 받을 수 없다.)
  • 디스패처 서블릿에 요청이 전달되기 전/후에 URL 패턴에 맞는 모든 요청에 대해 부가작업을 처리할 수 있는 기능을 제공한다.
  • 스프링 컨테이너가 아닌 톰캣과 같은 웹 컨테이너(서블릿 컨테이너)에 의해 관리된다.(Spring Bean 등록은 된다.)
  • 스프링과 무관하게 전역적으로 처리해야 하는 작업들을 처리한다.
  • 보안 공통 작업, 이미지나 데이터의 압축이나 문자열 인코딩과 같이 웹 애플리케이션에 전반적으로 사용되는 기능을 구현하는데 사용한다.

인터셉터(Interceptor)

  • Spring이 제공하는 기술이다.
  • 디스패처 서블릿이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있다.
  • 스프링 컨테이너 내에서 동작하므로 필터를 거쳐 프론트 컨트롤러인 디스패처 서블릿이 요청을 받은 이후에 동작한다.
  • 클라이언트의 요청과 관련되어 전역적으로 처리해야 하는 작업들을 처리한다.
  • 컨트롤러로 넘겨주기 위한 정보를 가공하는데 사용한다.

2. 스프링의 기본적인 예외처리 방법

스프링부트 예외가 발생하면 기본적으로 /error 로 에러 요청을 다시 전달하도록 WAS 설정을 해두었다.

그래서 별도의 설정이 없다면 예외 발생시 BasicErrorController로 에러 처리 요청이 전달된다.

일반적인 요청 흐름

WAS(톰캣)필터디스패처 서블릿인터셉터컨트롤러

컨트롤러 하위에서 예외가 발생하고 별도의 예외처리를 하지 않았을 경우 WAS까지 에러가 전달된다.

WAS는 애플리케이션이 처리할 수 없는 예외가 올라왔다고 판단하고 대응 작업을 진행한다.

컨트롤러(예외발생)인터셉터디스패처 서블릿필터WAS(톰캣)

WAS는 스프링 부트가 등록한 에러 설정(/error)에 맞게 요청을 전달한다.

WAS(톰캣)필터디스패처 서블릿인터셉터컨트롤러
컨트롤러(예외발생)인터셉터디스패처 서블릿필터WAS(톰캣)
WAS(톰캣)필터디스패처 서블릿인터셉터컨트롤러(BasicErrorController)

기본적인 예외처리는 에러 컨트롤러를 한번 더 호출한다.

기본 설정으로 받는 에러 응답은 나름 잘 갖추어져 있지만 클라이언트 입장에서 유용하지 못하다는 단점이 있다.(설정으로 변경할 수 있지만 유의미한 에러 응답을 전달하지 못한다.)

에러가 처리되지 않고 WAS에서 에러를 전달받아서 status가 500이 뜬다.

← 필터에서 Exception이 던져졌다면 에러가 처리되지 않고 서블릿까지 전달된다. 서블릿은 예외가 핸들링 되기를 기대했지만, 예외가 그대로 올라와서 예상치 못한 Exception을 만나 내부에 문제가 있다고 판단하여 status가 500이 뜬다.


3. 스프링이 제공하는 예외처리

Spring은 예외 처리 전략을 추상화한 HandlerExceptionResolver 인터페이스를 만들었다. (전략 패턴이 사용된 것이다.)

→ 에러 처리라는 공통 관심사를 메인 로직으로부터 분리하였다.

HandlerExceptionResolver는 발생한 Exception을 catch하고 HTTP 상태, 응답 메시지 등을 설정한다.

WAS는 해당 요청이 정상적인 응답인 것으로 인식이 되어 위에서 설명한 WAS의 에러 전달이 진행되지 않는다.

public interface HandlerExceptionResolver {
		//Object 타입의 handler는 예외가 발생한 컨트롤러 객체이다.
    ModelAndView resolveException(HttpServletRequest request, 
            HttpServletResponse response, Object handler, Exception ex);
}

예외가 던져지면 디스패처 서블릿까지 전달되고 적합한 예외처리를 위해 HandlerExceptionResolver 구현체들을 빈으로 등록해서 관리한다. 그리고 적용 가능한 구현체를 찾아 예외처리를 한다.

  • DefaultErrorAttributes: 에러 속성을 저장하며 직접 예외를 처리하지는 않는다.
  • ExceptionHandlerExceptionResolver: 에러 응답을 위한 Controller나 ControllerAdvice에 있는 ExceptionHandler를 처리함
  • ResponseStatusExceptionResolver: Http 상태 코드를 지정하는 @ResponseStatus 또는 ResponseStatusException를 처리함
  • DefaultHandlerExceptionResolver:  스프링 내부의 기본 예외들을 처리한다.

직접 예외를 처리하지 않고 속성만 관리하는 DefaultErrorAttributes 를 제외하고 직접 에러를 처리하는 ExceptionResolver들을 HandlerExceptionResolverComposite로 모아서 관리한다.(컴포지트 패턴이 적용됐다.)


4. ExceptionResolver를 동작시키는 도구들

1. ResponseStatus

에러 HTTP 상태를 변경하도록 도와주는 어노테이션이다.

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class NoSuchElementFoundException extends RuntimeException {
  ...
}

@ResponseStatus를 사용하는 경우

  • Exception 클래스 자체
  • 메소드에 @ExceptionHandler와 함께
  • 클래스에 @RestControllerAdvice와 함께

@ResponseStatus의 한계

  • 에러 응답의 내용을 수정할 수 없다.

DefaultErrorAttributes를 수정하면 가능하긴 하다.

  • WAS까지 예외가 전달되고 WAS의 에러 요청 전달이 진행된다.
  • 예외 클래스와 강하게 결합되어 있어 같은 예외는 같은 상태와 에러 메시지를 반환한다.
  • 외부에서 정의한 Exception 클래스에는 @ResponseStatus를 붙일 수 없다.

프로퍼티 설정이나 에러 응답 커스터마이징를 이용해 해결할 수 있지만 개발자가 원하는대로 에러를 처리하는 것은 어렵다.

2. ResponseStatusException

@ResponseStatus의 프로그래밍적 대안으로써 손쉽게 에러를 반환할 수 있다.

HttpStatus와 함께 선택적으로 reason과 cause를 추가할 수 있다.

언체크 예외를 상속받고 있어서 명시적으로 예외를 처리하지 않아도 된다.

@GetMapping("/{id}")
public ResponseEntity<Post> getPost(@PathVariable String id) {
    try {
        return ResponseEntity.ok(postService.getPost(id));
    } catch (NoSuchElementFoundException e) {
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Post Not Found");
    }
}

ResponseStatusException의 장점

  • 기본적인 예외처리를 빠르고 쉽게 적용할 수 있다.
  • HttpStatus를 직접 설정해 예외 클래스와의 결합도를 낮출 수 있다.

→ 예외 클래스를 사용하지 않는다.

  • 불필요한 예외 클래스들을 만들지 않아도 된다.

ResponseStatusException ****의 한계

  • 예외 처리 코드가 중복될 수 있다.
  • Spring 내부의 예외를 처리하기 어렵다.
  • 예외가 WAS까지 전달되고 WAS의 에러 요청 전달이 진행된다.

3. ExceptionHandler

유연하게 에러처리 할 수 있는 방법이다.

@RestController
@RequiredArgsConstructor
public class ProductController {

  private final PostService postService;

  @GetMapping("/{id}")
  public Response getPost(@PathVariable String id){
    return postServcie.getPost(id);
  }

  @ExceptionHandler(NoSuchElementFoundException.class)
  public ResponseEntity<String> handleNoSuchElementFoundException(NoSuchElementFoundException exception) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND).body(exception.getMessage());
  }
}

Exception 클래스들을 속성으로 받아 처리할 예외를 지정할 수 있다.

ExceptionHandler 어노테이션에 예외 클래스를 지정하지 않으면 파라미터에 설정된 에러 클래스를 처리한다.

@ResponseStatus와 다르게 에러 응답을 자유롭게 할 수 있다.

@ExceptionHandler 사용시 주의할 점 & 단점

  • @ExceptionHandler에 등록된 예외 클래스와 파라미터로 받는 예외 클래스가 동일해야 한다.

→ 값이 다르면 런타입 에러가 발생한다.

  • 특정 컨트롤러에서만 발생하는 예외만 처리한다.

→ 에러 처리 코드가 중복될 수 있다.

4. @ControllerAdvice@RestControllerAdvice

전역적으로 @ExceptionHandler를 적용할 수 있다.

둘의 차이점은 @ResponseBody 유무로 응답을 Json으로 내려준다는 점(RestControllerAdvice인 경우)이 다르다.(ex. Controller vs RestController)

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NoSuchElementFoundException.class)
    protected ResponseEntity<?> handleIllegalArgumentException(NoSuchElementFoundException e) {
        final ErrorResponse errorResponse = ErrorResponse.builder()
                .code("Item Not Found")
                .message(e.getMessage()).build();

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

ControllerAdvice의 장점

  • 하나의 클래스로 모든 컨트롤러에 대한 전역 예외처리가 가능하다.
  • 직접 정의한 에러 응답을 일관성있게 클라이언트에게 응답할 수 있다.

ControllerAdvice 사용시 주의할 점

  • 한 프로젝트당 하나의 ControllerAdvice만 관리하는 것이 좋다.

→ 여러 개 있을 경우 @Order를 이용해서 순서를 지정해야한다.(하지 않으면 임의의 순서로 에러를 처리한다.)

  • 직접 구현한 Exception 클래스는 한 곳에서 관리한다.

5. Spring 예외 처리 흐름

예외가 발생하면

  1. ExceptionHandlerExceptionResolver

→ 예외가 발생한 컨트롤러 안에 적합한 @ExceptionHandler가 있는지 검사한다.

→ 컨트롤러의 @ExceptionHandler에서 처리가능하다면 처리하고, 그렇지 않으면 ControllerAdvice로 넘어간다.

ControllerAdvice안에 적합한 @ExceptionHandler가 있는지 검사하고 없으면 다음 처리기로 넘어간다.

  1. ResponseStatusExceptionResolver

@ResponseStatus가 있는지 또는 ResponseStatusException인지 검사한다.

→ 맞으면 ServletResponsesendError()로 예외를 디스패처 서블릿까지 전달되고, 디스패처 서블릿이 BasicErrorController로 요청을 전달한다.

  1. DefaultHandlerExceptionResolver

→ Spring의 내부 예외인지 검사하여 맞으면 에러를 처리하고 아니면 넘어간다.

  1. 적합한 ExceptionResolver가 없으므로 예외가 서블릿까지 전달된다.

→ 서블릿은 SpringBoot가 진행한 자동 설정에 맞게 BasicErrorController로 요청을 다시 전달한다.

@ResponseStatus, ResponseStatusException 등과 같이 직접 에러 응답을 반환하지 않는 경우에는 BasicErrorController를 거쳐 에러가 처리된다.

→ 내부에서 2번 컨트롤러 요청이 전달된다.

→ ExceptionHandler나 ControllerAdvice처럼 직접 에러를 반환하는 경우에는 BasicErrorController를 거치지 않는다.


참고자료

[Spring] 디스패처 서블릿이란? (Dispatcher Servlet)

[Spring] 필터(Filter) vs 인터셉터(Interceptor) 차이 및 용도 - (1)

[Spring] 스프링의 다양한 예외 처리 방법(ExceptionHandler, ControllerAdvice 등) 완벽하게 이해하기 - (1/2)

profile
대덕SW마이스터고 백엔드 개발자

0개의 댓글