예외 처리exception handling
이란, 프로그램 실행 시 발생할 수 있는 예기치 못한 문제들을 예상하여 이를 대비한 코드를 작성하는 것이다.
예외 처리를 한다고 하여, 프로그램의 모든 예외 상황 자체를 막을 수 있는 것은 아니다. 그러나 이를 다른 기능으로 우회시키거나, 회피하는 식으로 처리를 하여 프로그램이 정상적인 실행상태를 지속적으로 유지하도록 설계하는 것이 예외 처리의 중요한 맥락이라고 할 수 있다.
자바의 경우, Error
와 Exception
으로 나누어, 예외를 처리한다.
Error (오류) : 시스템이 종료될 수준의 심각한 문제로, 대부분 개발자의 제어 범위를 벗어난 상황에서 발생한다. 메모리 부족, 스택 오버 플로우 등 코드로 해결하기 어려운 문제들을 나타낸다.
Exception (예외) : 개발자가 구현한 코드 내에서 발생하는 문제로, 예외는 처리 가능하고 복구가 가능한 문제를 의미한다. 자바에서는 크게 checked exception
과 unchecked exception
으로 예외를 구분한다.
unchecked exception
에 대한 적절한 처리를 대비해야한다. 흔히 프로젝트에서 이야기하는 예외처리 로직은 대부분 unchecked exception
에 해당하는 예외들이다.try-catch
와 throw
그리고 CustomException
등을 통해 예외를 해결한다.
try-catch : try
문에 예외가 발생할 수 있는 코드를 위치하고 만일 코드에 예외가 발생한다면, 해당 예외에 적합한 catch
문으로 이동하여, 해당 블럭 내의 코드를 실행한다.
throw, throws : 예외를 강제로 발생시키는 키워드로 주로 예외 클래스에 대한 인스턴스를 직접적으로 생성하는 경우 함께 사용한다. 위의 try
문에서 강제적으로 throw
를 사용하는 경우, 강제로 발생된 예외에 알맞은 catch
문으로 이동하게 된다. throws
와 함께 활용하여 해당 예외를 자신을 호출한 상위 요소로 전파시켜 예외처리를 진행하기도 한다.
CustomException : Java
에서 제공하는 예외 클래스를 상속하여 설계하는 프로젝트에 적합한 CustomException
을 만들어 사용할 수 있다. 일반적인 예외 클래스와 동일하게 try-catch
, throw, throws
를 적용하여 예외처리가 가능하다.
반복적인 try-catch
구문의 코드 작성은 코드의 중복성 및 가독성을 저하시켰다. 더불어 다수의 throw, throws
의 경우에는 서비스 설계가 커지면 커질수록 전파량이 많아지면서, 임의의 예외가 발생했을 때 어디에서 이 문제를 처리하는지 파악하기 어려웠다.
자바의 예외 처리에서 나타났던 어려움 개선하기 위해, Spring
진영에서는 자주 일어나는 예외에 대한 예외 클래스를 추가적으로 만들었고, 예외 처리라는 관심사를 메인 로직으로 부터 분리하여 하나의 장소에서 공통적으로 처리할 수 있도록 설계했다. HandlerExceptionResolver
크게 DispatcherServlet
이후 발생하는 예외와 DispatcherServlet
이전 발생하는 예외로 나눌 수 있다.
DispatcherServlet
에 도달하기전에 발생하는 예외로 설정 클래스나, Filter
에서 발생하는 예외이다. 일반적으로 Spring
이 기본적으로 처리하는 예외 과정들은 대부분 DispatcherServlet
에 의해 처리되기 때문에, Spring
기본 예외처리 기능이 관리하지 못하는 영역이다.이러한 문제를 해결하기 위한 방법으로
Spring Security
가 있다.Spring Security
는 보안을 위한 주요 필터를 제공하며, 해당 필터에서 예외가 발생하는 경우, 해당 예외처리를 위한EntryPoint
인터페이스를 제공하여,filter
내 특정 예외에 대한 처리를 가능하도록 도와준다.
DispatcherServlet
에 도달한 이후 발생하는 예외로, 주로 Controller
, Repository
, Service
등 개발자가 구현해야하는 로직 내에서 발견되는 예외이다. Spring
이 기본적으로 제공하는 예외 전략은 대부분의 DispatcherServlet
접근 이후 예외를 다룬다.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"
}
Spring은 예외 처리라는 예외 처리라는 관심사를 메인 로직으로 부터 분리하여 하나의 장소에서 공통적으로 처리할 수 있도록 다양한 방안을 고안했고, 이러한 전략들을 추상화한 인터페이스가 HandlerExceptionResolver
이다. 기존 BasicErrorController
의 경우, WAS
가 잘못된 응답을 감지하여 /error
요청을 보내는 것이었다면, HandlerExceptionResolver
를 적용시키면, DispatcherServlet
접근 이후 나타나는 예외 상황을 감지하여, 해당 예외를 처리한 후 적절히 응답을 보낼 수 있다.
일반적으로 WAS
가 감지하여 나타나는 에러는 무조건 500
에러로 간주되지만, HandlerExceptionResolver
가 감지하여 처리한 예외 들은 개발자가 알맞은 상태코드와 메시지를 적용시킬 수 있다.
HandlerExceptionResolver
가 처리한 예외 응답의 경우,WAS
는 정상 동작으로 간주한다.
@Controller
및 @ControllerAdvice
클래스의 @ExceptionHandler
가 적용된 예외사항을 처리한다.HTTP Status Code
를 지정하는 @ResponseStatus
혹은 ResponseStatusException
을 처리한다.Controller
메서드에서 발생한 예외에 대한 응답 상태 코드와 선택적으로 상태 코드에 대한 이유를 정의하기 위한 어노테이션이다.
메서드 레벨에서 적용하면 해당 메서드에서 발생한 예외에 대해 특정 응답 코드를 설정할 수 있으며, 클래스 레벨에서 적용하여 클래스 내부에서 발생하는 모든 예외에 대한 기본 상태 코드를 설정하는 것도 가능하다.
@Controller
@ResponseStatus(HttpStatus.NOT_FOUND)
public class MyController {
// 예외에 대한 추가 로직 구현
@GetMapping("/example")
@ResponseStatus(HttpStatus.OK)
public String example() {
return "OK";
}
}
Spring 5
부터 도입된 예외 클래스로, 인스턴스의 매개변수로 HTTP Status Code
와 메시지를 담아, 예외를 발생시키는 것이 가능하다.
@ResponseStatus
의 경우 기본적으로 Controller
메서드에서 발생하는 예외를 처리하기 위한 어노테이션으로, 외부에서 만들어진 예외 클래스에는 적용이 불가능한데, ResponseStatusException
의 경우, 이를 적용하여 Controller
외의 영역에서 예외를 생성하는 것이 가능하다.
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
조합을 선호한다.
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를 통한 예외처리