예외 복구 : 예외가 발생하면 예외 상황에 대해 알맞게 처리하여 복구한다. ex) try, catch
예외 회피 : 예외를 직접 처리하지 않고 예외를 상위 메소드에 위임한다. ex) throw
예외 전환 : 예외를 위임하되 발생한 예외를 그대로 위임하는 것이 아닌 적절한 예외로 전환하여 위임한다. ex) restTemplate.doExecute
이번에 살펴볼 내용은 예외 복구에 대한 내용이다.
이전에 설명 하였던 것 처럼 예외가 발생할 경우 try, catch를 사용하여 예외 상황을 대응할 수 있다. 그러나 우리가 실제로 개발하다 보면 상당히 많은 예외 복구 로직이 필요하고, Exception 성격에 따라서 비슷하게 처리되는 로직도 있을 것이다. 그러면서 우리는 동일한 복구 로직에 대한 보일러 플레이트 코드를 줄이고 싶은 상황이 발생할 수도 있고, 다양한 예외처리를 한곳에서 보고싶은 경우도 있을 것이다. 이런 다양한 개선점을 어떻게 해결할 수 있는지 살펴보자
예외 복구 범위를 정해보자
메소드 영역 : 메소드 영역은 종속된 복구 기능으로 단순히 try, catch 사용 하면 된다.
클래스 영역 : 클래스 내 공통 예외 복구는 @ExceptionHandler 사용할 수 있다.
전역 영역 : 여러 클래스의 공통 예외 복구는 @ControllerAdvice 사용할 수 있다.
컨트롤 내 예외처리
특정 컨트롤러 내에서 예외를 처리하고 싶을 경우에는 컨트롤러 내에서 @ExceptionHandler를 사용하면 해결할 수 있다. 이는 @Controller나 @RestController 빈 내에서 발생하는 특정 예외를 처리해주는 기능을 지원한다.
Controller에서 예외를 발생해보자
예제는 단순하다. /person/exception이라는 api를 호출하면 NullPointerException이 발생하지만 @ExceptionHanlder를 사용하여 NullPointerException을 catch하여 "nullPointerException Handle!!!" 라는 문자열을 리턴할 것이다.
정상적으로 date format으로 변경되는것을 확인할 수 있다. 이는 @InitBinder를 사용하는 단편적인 예로 @InitBinder를 사용하지 않고 Person 클래스의 registerDate필드에 @DateTimeFormat(pattern = "yyyy-MM-dd") 를 사용해도 date 포맷으로 변경 가능하다. 개인적으로는 이 어노테이션은 데이터 포맷 변경 보다는 검증 처리에 더 많이 사용하는 것 같다.
@ModelAttribute
@ModelAttribute는 두 가지 기능을 제공한다.
Controller에서 받아오는 외부 request 객체를 모델로 받을 수 있도록 매핑해준다.
Controller 내에서 공통으로 사용할 모델을 정의할 수 있으며, View에 전달할 모델을 자동으로 설정해준다.
이번 글에서는 첫번째 기능은 주제와 다른 이야기이므로 두번째 기능에 초점을 맞추도록 하겠다.
하나의 예제를 만들어보자. {url}/person/register이라는 api를 호출하면 hello라는 뷰 페이지를 호출하는 controller를 작성하였다. 코드를 살펴보면 hello 라는 뷰 페이지를 호출할 때, 아무런 데이터를 설정하지 않은것을 볼수 있다.
@Controller@RequestMapping("/person")publicclassPersonController { @ModelAttributepublicvoidaddAttributes(Model model) {model.addAttribute("msg","Welcome to the Incheol Blog!!!!!"); } @GetMapping("register")publicStringregister() {return"hello"; }}
그런 다음에 view 페이지를 작성해보자
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN""<http://www.w3.org/TR/html4/loose.dtd>"><html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8"><title></title></head><body>${msg}</body></html>
view 페이지에는 msg를 서버에서 받아와서 사용하도록 구성하였다. 그런 다음에 결과 페이지를 확인해보자.
msg 변수 값이 셋팅되어 있는 것을 확인할 수 있다. 이처럼 modelattribute를 사용하면 view 페이지에 전달해준 데이터가 자동으로 셋팅되도록 기능을 제공한다.
글로벌 예외처리
지금까지는 컨트롤러 내의 공통 기능을 적용할 수 있는 다양한 방법을 살펴보았다. 그러면 특정 컨트롤러가 아닌 여러 컨트롤러로 공통 기능을 확장하려면 어떻게 해야 할까?
컨트롤러 간의 공통된 로직 구현을 위해서는 @ControllerAdvice를 사용할 수 있다.
@ControllerAdvice
@ControllerAdvice는 컨트롤러를 보조해주는 기능을 제공한다는 것을 명시한다.
바로 예제를 통해서 알아보자. 우선 컨트롤러 공통으로 처리해 줄 수 있는 ControllerSupport 클래스를 생성해보자.
이전의 Exception 처리에 대한 예제를 다시 살펴보자. 이전에는 결과 메시지에만 집중하였지만 다시 살펴보니 http status code가 200으로 리턴해주는것을 확인할 수 있다. 이는 회사 내규에 따라 다르겠지만 Exception이 발생하였을 경우에 Response Body에만 표시해줄지 아니면 Http status code에도 표시할지에 따라서 다를 수 있다. 만약 status code에도 어떤 에러인지 명시적으로 내어주고 싶을 경우에 @ResponseStatus를 사용할 수 있다.
해당 코드를 보면 이유를 알 수 있을 것이다. Exception을 처리하는 로직이 한곳에 모여있으면 한눈에 보기 용이하다. 그러나 Exception에 따라 처리하는 로직이 다르고 결과값도 에러 페이지를 보여주는 경우도 있고 에러 메시지만 노출시키는 경우도 있다. 이러한 여러 분기처리를 한곳에서 사용한다고 해보자. 각각의 구현 코드는 메소드로 분리하여 코드 라인은 줄일 수 있지만 모든 분기처리를 하나의 메소드에서 해야 한다는 단점이 있다. 이는 객체지향 관점에서도 단일 책임 원칙을 위반하는 사례라고 할 수 있다. 예외에 대한 모든 책임을 가지다 보면 점차 유지보수하기 쉽지않고 변경에 취약한 코드가 되기 마련이다. 그러므로 Exception 처리는 Exception Handler를 사용하도록 하자