[Spring] 예외 처리 - Spring에서의 예외 처리 전략과 특징

·2024년 2월 25일
0

Spring

목록 보기
4/6
post-thumbnail

예외 처리

예외 처리exception handling 이란, 프로그램 실행 시 발생할 수 있는 예기치 못한 문제들을 예상하여 이를 대비한 코드를 작성하는 것이다.

예외 처리를 한다고 하여, 프로그램의 모든 예외 상황 자체를 막을 수 있는 것은 아니다. 그러나 이를 다른 기능으로 우회시키거나, 회피하는 식으로 처리를 하여 프로그램이 정상적인 실행상태를 지속적으로 유지하도록 설계하는 것이 예외 처리의 중요한 맥락이라고 할 수 있다.

Error vs Exception

자바의 경우, ErrorException 으로 나누어, 예외를 처리한다.

  • Error (오류) : 시스템이 종료될 수준의 심각한 문제로, 대부분 개발자의 제어 범위를 벗어난 상황에서 발생한다. 메모리 부족, 스택 오버 플로우 등 코드로 해결하기 어려운 문제들을 나타낸다.

  • Exception (예외) : 개발자가 구현한 코드 내에서 발생하는 문제로, 예외는 처리 가능하고 복구가 가능한 문제를 의미한다. 자바에서는 크게 checked exceptionunchecked exception 으로 예외를 구분한다.

    • checked exception (컴파일 에러) : 컴파일러가 예외를 감지하여 강제로 예외 처리를 요구하는 예외이며, 처리하지 않는 경우 컴파일 과정이 실행이 되지 않거나, 컴파일 단계 도중 예외를 발생시킨다.
    • unchecked exception (런타임 에러) : 컴파일러가 감지하지 못하는 예외를 의미하며, 프로그램 실행 시에만 예외를 확인할 수 있기 때문에, 코드를 구현하는 개발자가 주의가 필요한 예외이다. 개발자는 해당 예외가 발생할 수 있는 부분을 사전에 확인해야하며 unchecked exception 에 대한 적절한 처리를 대비해야한다. 흔히 프로젝트에서 이야기하는 예외처리 로직은 대부분 unchecked exception 에 해당하는 예외들이다.

자바의 예외 처리

try-catchthrow 그리고 CustomException 등을 통해 예외를 해결한다.

  • try-catch : try 문에 예외가 발생할 수 있는 코드를 위치하고 만일 코드에 예외가 발생한다면, 해당 예외에 적합한 catch 문으로 이동하여, 해당 블럭 내의 코드를 실행한다.

  • throw, throws : 예외를 강제로 발생시키는 키워드로 주로 예외 클래스에 대한 인스턴스를 직접적으로 생성하는 경우 함께 사용한다. 위의 try 문에서 강제적으로 throw 를 사용하는 경우, 강제로 발생된 예외에 알맞은 catch 문으로 이동하게 된다. throws 와 함께 활용하여 해당 예외를 자신을 호출한 상위 요소로 전파시켜 예외처리를 진행하기도 한다.

  • CustomException : Java 에서 제공하는 예외 클래스를 상속하여 설계하는 프로젝트에 적합한 CustomException 을 만들어 사용할 수 있다. 일반적인 예외 클래스와 동일하게 try-catch , throw, throws 를 적용하여 예외처리가 가능하다.

자바 예외 처리의 어려움

반복적인 try-catch 구문의 코드 작성은 코드의 중복성 및 가독성을 저하시켰다. 더불어 다수의 throw, throws 의 경우에는 서비스 설계가 커지면 커질수록 전파량이 많아지면서, 임의의 예외가 발생했을 때 어디에서 이 문제를 처리하는지 파악하기 어려웠다.

스프링 예외 처리

자바의 예외 처리에서 나타났던 어려움 개선하기 위해, Spring 진영에서는 자주 일어나는 예외에 대한 예외 클래스를 추가적으로 만들었고, 예외 처리라는 관심사를 메인 로직으로 부터 분리하여 하나의 장소에서 공통적으로 처리할 수 있도록 설계했다. HandlerExceptionResolver

Spring 예외 분류

크게 DispatcherServlet 이후 발생하는 예외와 DispatcherServlet 이전 발생하는 예외로 나눌 수 있다.

  • DispatcherServlet 접근 이전 예외 : DispatcherServlet 에 도달하기전에 발생하는 예외로 설정 클래스나, Filter 에서 발생하는 예외이다. 일반적으로 Spring 이 기본적으로 처리하는 예외 과정들은 대부분 DispatcherServlet 에 의해 처리되기 때문에, Spring 기본 예외처리 기능이 관리하지 못하는 영역이다.

    이러한 문제를 해결하기 위한 방법으로 Spring Security 가 있다. Spring Security 는 보안을 위한 주요 필터를 제공하며, 해당 필터에서 예외가 발생하는 경우, 해당 예외처리를 위한 EntryPoint 인터페이스를 제공하여, filter 내 특정 예외에 대한 처리를 가능하도록 도와준다.

  • DispatcherServlet 접근 이후 예외 : DispatcherServlet 에 도달한 이후 발생하는 예외로, 주로 Controller , Repository , Service 등 개발자가 구현해야하는 로직 내에서 발견되는 예외이다. Spring 이 기본적으로 제공하는 예외 전략은 대부분의 DispatcherServlet 접근 이후 예외를 다룬다.

BasicErrorController

Spring 은 기본적인 예외 페이지 및 예외 응답을 제공하는 Controller 를 구현해두었다. 만약 Spring 프로젝트에 예외처리 설정이 없다면, WAS 측에서 /error 요청을 다시 보내는 것으로, 이를 BasicErrorControler 가 받아 관련 에러 응답을 반환한다. Spring 프레임워크를 사용하는 경우 Postman 등의 API 설계 플랫폼을 사용했을 때, 예상치 못한 예외가 발생하면, 예외 처리를 하지 않았음에도, statusCode 나, 예외 페이지 등을 나타나는 이유 역시, BasicErrorController 기능이라 할 수 있다.

{
    "timestamp": "2021-12-31T03:35:44.675+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "path": "/product/5000"
}

HandlerExceptionResolver

Spring은 예외 처리라는 예외 처리라는 관심사를 메인 로직으로 부터 분리하여 하나의 장소에서 공통적으로 처리할 수 있도록 다양한 방안을 고안했고, 이러한 전략들을 추상화한 인터페이스가 HandlerExceptionResolver 이다. 기존 BasicErrorController 의 경우, WAS 가 잘못된 응답을 감지하여 /error 요청을 보내는 것이었다면, HandlerExceptionResolver 를 적용시키면, DispatcherServlet 접근 이후 나타나는 예외 상황을 감지하여, 해당 예외를 처리한 후 적절히 응답을 보낼 수 있다.

일반적으로 WAS 가 감지하여 나타나는 에러는 무조건 500 에러로 간주되지만, HandlerExceptionResolver 가 감지하여 처리한 예외 들은 개발자가 알맞은 상태코드와 메시지를 적용시킬 수 있다.

HandlerExceptionResolver 가 처리한 예외 응답의 경우, WAS 는 정상 동작으로 간주한다.

Spring이 제공하는 HandlerExceptionResolver 구현체

  • ExceptionHandlerExceptionResolver : @Controller@ControllerAdvice 클래스의 @ExceptionHandler 가 적용된 예외사항을 처리한다.
  • ResponseStatusExceptionResolver : HTTP Status Code 를 지정하는 @ResponseStatus 혹은 ResponseStatusException 을 처리한다.
  • DefaultHandlerExceptionResolver : 스프링이 제공하는 기본 예외들을 처리한다.

Spring 예외 처리 도구

@ResponseStatus

Controller 메서드에서 발생한 예외에 대한 응답 상태 코드와 선택적으로 상태 코드에 대한 이유를 정의하기 위한 어노테이션이다.

메서드 레벨에서 적용하면 해당 메서드에서 발생한 예외에 대해 특정 응답 코드를 설정할 수 있으며, 클래스 레벨에서 적용하여 클래스 내부에서 발생하는 모든 예외에 대한 기본 상태 코드를 설정하는 것도 가능하다.

@Controller
@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyController {
    // 예외에 대한 추가 로직 구현

		@GetMapping("/example")
		@ResponseStatus(HttpStatus.OK)
		public String example() {
		    return "OK";
		}
}

ResponseStatusException

Spring 5 부터 도입된 예외 클래스로, 인스턴스의 매개변수로 HTTP Status Code 와 메시지를 담아, 예외를 발생시키는 것이 가능하다.

@ResponseStatus 의 경우 기본적으로 Controller 메서드에서 발생하는 예외를 처리하기 위한 어노테이션으로, 외부에서 만들어진 예외 클래스에는 적용이 불가능한데, ResponseStatusException 의 경우, 이를 적용하여 Controller 외의 영역에서 예외를 생성하는 것이 가능하다.

@ExceptionHandler

Controller 에서 발생하는 예외를 감지하여, 메서드로 처리해주는 기능이다. @ResponseStatus 의 경우, 임의의 결과에 대한 상태코드와 메시지를 형성하는 기능이지만, @ExceptionHandler 의 경우, throw 된 특정 예외를 감지하여, 필요한 메서드를 실행할 수 있도록 한다.

@ExceptionHandler 역시 메서드 레벨, 클래스 레벨 모두 적용이 가능하며, 단일 및 다중 예외 처리가 가능하다. 더불어 @ControllerAdvice 와 함께 적용시켜 Controller 전역에 대한 예외 처리도 가능하다.

@ExceptionHandler({ FirstException.class, SecondException.class })
public ResponseEntity<String> handleMultipleExceptions(Exception ex) {
    // 예외 처리 로직
    return new ResponseEntity<>("Multiple Exceptions Handled", HttpStatus.INTERNAL_SERVER_ERROR);
}

ResponseEntity가 등장하게 되면서, 상태코드와 메시지, body 값을 설정하는 것이 가능하기 때문에, @ResponseStatus 는 잘 사용하지 않게 되었다. 실제 @ResponStatus , ResponseEntity 모두 적용하는 경우, ResponseEntity 가 응답 우선순위가 더 높기 때문에, 실무에서는 ResponseEntity + @ExceptionHandler 조합을 선호한다.

@ControllerAdvice

Spring 프로젝트 내의 여러 컨트롤러에서 발생한 예외를 한 곳에서 전역적으로 처리하도록 도와준다. @ExceptionHandler 를 포함하여 위의 3가지 예외처리 도구 모두, Controller 단일 개체에서 발생하는 예외를 감지하는 기능이므로, Controller 마다 중복적인 코드 구현을 피할 수 없었다.

@ControllerAdvice 가 등장한 이후 부터는 모든 Controller 에서의 예외 사항이 @ControllerAdvice 를 거쳐가기 때문에, 서로 다른 Controller 에서 공통적으로 나타날 수 있는 예외처리를 한번에 처리하는 것이 가능했다.

@ControllerAdvice(assignableTypes = { UserController.class, ProductController.class })
public class SpecificControllerExceptionHandler {

    @ExceptionHandler(SpecificException.class)
    public ResponseEntity<String> handleSpecificException(SpecificException ex) {
        // 예외 처리 로직
        return new ResponseEntity<>("Specific Exception Handled", HttpStatus.BAD_REQUEST);
    }
}

@RestController 를 위한 @RestControllerAdvice 도 존재하는데, 해당 어노테이션을 적용하면 예외 처리에 대한 응답을 @ResponseBody 를 통해, HTTP Response Body 의 형태로 구성하여 제공해준다.

@Controller@ControllerAdvice 모두 @ExceptionHandler 를 적용하여 예외 처리가 가능하다. 만약 임의의 예외에 대해 @Controller@ControllerAdvice 모두 @ExceptionHandler 로 예외 처리를 하는 코드가 작성되어 있다면, @Controller 쪽에서 예외가 처리된다.
이는 @ControllerAdvice 를 통해 공통적인 예외 처리를 진행하고, 특정 메서드에서 대하여 @Controller 를 통해 개별적인 예외처리가 가능하다.

@ControllerAdvice 를 사용해보면, 프로젝트 예외 처리를 바라보는 관점이 달라진다. 마치 React 에서 props 를 통해 상태를 제공하다가, Redux 를 만난 느낌과 유사한데, 예외 처리가 정말 편해지므로, Spring 을 사용한다면 @ControllerAdvice 는 필수적으로 사용하자.

참고
[java30강] throw throws (예외발생 및 예외처리)
☕ 자바 예외 처리(try catch) 문법 & 응용 정리
신예진 - 스프링의 예외처리 | 백엔드 데브코스 4기 | 20230824
스크랩 Exception 방법과 흐름
[Spring] API 예외 처리(HandlerExceptionResolver)
[스프링부트] @ExceptionHandler를 통한 예외처리

profile
새로운 것에 관심이 많고, 프로젝트 설계 및 최적화를 좋아합니다.

0개의 댓글