
이 단원은
Controller에서 예외가 발생했을 때 서버 오류 화면으로 끝내지 않고, 정해진 처리 흐름으로 보내는 방법을 이해하는 구간이다.
오류 처리는 예외를 없애는 기능이 아니다.
예외가 발생했을 때 사용자에게 어떤 화면이나 응답을 보여 줄지 정리하는 기능이다.
웹 애플리케이션은 사용자의 요청을 받아 처리한다.
하지만 사용자가 항상 올바른 값만 보내지는 않는다.
숫자가 들어와야 하는 자리에 문자가 들어올 수도 있고, 존재하지 않는 데이터를 요청할 수도 있다.
이런 문제가 생기면Controller메서드는 정상적으로 화면을 반환하지 못한다.
이때 아무 처리도 하지 않으면 사용자는 복잡한 서버 오류 화면을 보게 된다.
그래서Spring MVC는 예외가 발생했을 때 예외를 처리할 메서드로 흐름을 넘길 수 있는 기능을 제공한다.
오류 처리의 핵심은 단순하다.
정상 흐름에서 문제가 생기면, 예외 처리 흐름으로 바꿔서 정해진 화면이나 응답을 반환하는 것이다.
웹 요청은 항상 정상적인 값으로 들어오지 않는다.
예를 들어 회원 번호는 숫자여야 한다.
그런데 사용자가 주소창에/member/detail/abc처럼 문자를 넣을 수 있다.
이 경우abc를 숫자로 바꾸는 과정에서 예외가 발생할 수 있다.
또 다른 경우도 있다.
사용자가/member/detail/10처럼 형식에 맞는 숫자를 보냈다고 해도, 실제로10번 회원이 없을 수 있다.
이 경우 요청값의 형식은 맞지만, 처리할 데이터가 없어서 정상 화면을 만들 수 없다.
이런 상황을 아무 처리 없이 두면 예외가 그대로 밖으로 퍼진다.
그러면 사용자는 개발자용 오류 메시지나 서버 오류 화면을 볼 수 있다.
이 화면은 사용자가 이해하기 어렵고, 서비스 입장에서도 좋지 않다.
사용자에게는 복잡한 오류 내용보다 “잘못된 요청입니다”, “해당 회원이 없습니다”처럼 이해할 수 있는 화면을 보여 주는 편이 더 자연스럽다.
그래서 오류 처리는 사용자에게 보여 줄 실패 화면을 정리하는 역할을 한다.
오류 처리는 프로그램이 실패하지 않게 만드는 기능이 아니라, 실패했을 때 어떻게 응답할지 정하는 기능이다.
예외가 생기지 않도록 미리 검사하는 것과, 이미 발생한 예외를 처리하는 것은 다르다.
입력값 검사는 예외를 줄이는 작업이고, 오류 처리는 발생한 예외를 정리하는 작업이다.
이 차이를 이해하면@ExceptionHandler와@ControllerAdvice도 훨씬 쉽게 읽힌다.
이 둘은 예외가 발생한 뒤, 그 예외를 어떤 메서드가 처리할지 정하는 도구이다.
일반적인
Spring MVC요청 흐름은 사용자의 요청을 받아 알맞은 컨트롤러 메서드를 실행하고, 그 메서드가 반환한 값으로 화면을 결정하는 방식이다.
사용자가 특정URL을 요청하면DispatcherServlet이 먼저 요청을 받는다.
DispatcherServlet은Spring MVC에서 들어온 요청을 가장 앞에서 받아서 알맞은Controller로 보내는 중심 역할을 한다.
이 구간에서 중요한 점은 정상 흐름과 예외 흐름을 구분해서 보는 것이다.
정상 흐름에서는 컨트롤러 메서드가 끝까지 실행되고return으로 화면이 결정된다.
하지만 예외가 발생하면 원래 실행되던 흐름이 멈추고, 예외를 처리하는 흐름으로 바뀐다.
정상 요청 흐름
정상 상황에서는 흐름이 비교적 단순하다.
사용자가/member/detail/10같은 주소를 요청하면DispatcherServlet이 이 요청을 처리할 수 있는Controller메서드를 찾는다.
그리고 메서드를 실행하기 위해 필요한 값을 준비한다.
예를 들어 컨트롤러 메서드에int id같은 매개변수가 있다면,Spring MVC는 요청 주소나 요청 파라미터에서 값을 꺼내id에 넣을 수 있는 형태로 바꾼다.
이 준비가 끝나야 컨트롤러 메서드의 본문이 실행된다.
여기서 본문은 메서드의{ }안에 작성한 실제 처리 코드를 의미한다.
컨트롤러 메서드가 끝까지 실행되고return "memberDetail"처럼 뷰 이름을 반환하면,Spring MVC는 그 이름에 맞는 화면 파일을 찾아 사용자에게 응답한다.
여기서return은 단순히 문자열을 돌려주는 것이 아니라, 어떤 화면을 보여 줄지 알려 주는 역할을 한다.
정상 흐름을 단계로 보면 이렇다.
- 사용자가 특정
URL을 요청한다.DispatcherServlet이 요청을 받는다.- 요청 주소와 연결된
Controller메서드를 찾는다.- 컨트롤러 메서드 실행에 필요한 매개변수 값을 준비한다.
- 매개변수 준비가 끝나면
Controller메서드 본문이 실행된다.- 메서드가
String,ModelAndView같은 값을 반환한다.- 반환값을 기준으로 보여 줄
View를 찾는다.- 최종 화면이 사용자에게 응답된다.
즉, 정상 흐름의 핵심은 매개변수 준비가 성공하고, 컨트롤러 메서드가 끝까지 실행되고, 정상적으로
return을 한다는 점이다.
정상 요청은 요청 → 매개변수 준비 → 컨트롤러 실행 → 뷰 반환 → 화면 응답 순서로 진행된다.
예외가 발생했을 때의 흐름
예외가 발생하면 정상 흐름이 그대로 진행되지 않는다.
예외는 정상적인 실행 흐름을 끊는 신호이다.
컨트롤러 메서드 안에서 예외가 발생하면, 그 아래에 있던 코드는 더 이상 실행되지 않는다.
메서드가 원래 반환하려던return도 실행되지 못한다.
아래 예제처럼 요청 처리 중간에 예외가 발생한다고 생각하면 된다.// ErrorFlowExample.java @GetMapping("/error-flow") public String errorFlow() { // 요청 처리 중 문제가 발생한다 throw new IllegalArgumentException("잘못된 요청입니다."); // 위에서 예외가 발생했으므로 이 코드는 실행되지 않는다 // return "normalView"; }
throw는 예외를 직접 발생시키는 문법이다.
이 코드에서는IllegalArgumentException이 발생하는 순간 메서드의 정상 흐름이 끊긴다.
그래서return "normalView"로 정상 화면을 반환하는 흐름까지 가지 못한다.
예외가 발생하면 원래 컨트롤러 메서드의 정상return은 실행되지 않고,Spring MVC가 예외를 처리할 수 있는 다른 흐름을 찾는다.
이때 찾는 대상이 바로@ExceptionHandler가 붙은 예외 처리 메서드이다.
여기서 중요한 점은 예외 처리 메서드를 개발자가 직접 호출하지 않는다는 것이다.
개발자가handleException()같은 메서드를 코드에서 직접 실행하는 구조가 아니다.
컨트롤러 실행 전이나 실행 중에 예외가 발생하면Spring MVC가 현재 발생한 예외의 종류를 확인하고, 그 예외를 처리할 수 있는 메서드가 있는지 자동으로 찾는다.
예외 처리 흐름을 단계로 보기
예외 처리 흐름을 단계로 보면 이렇다.
- 사용자가 특정
URL을 요청한다.DispatcherServlet이 요청을 받는다.- 요청 주소와 연결된
Controller메서드를 찾는다.- 컨트롤러 메서드 실행에 필요한 매개변수 값을 준비한다.
- 요청값을 매개변수 타입으로 바꾸는 과정에서 예외가 발생할 수 있다.
- 매개변수 준비가 성공하면
Controller메서드 본문이 실행된다.- 메서드 본문 실행 중에도 예외가 발생할 수 있다.
- 예외가 발생하면 원래 컨트롤러 메서드의 정상
return흐름이 멈춘다.Spring MVC가 해당 예외를 처리할 수 있는@ExceptionHandler메서드를 찾는다.- 처리 가능한 메서드가 있으면 그 메서드가 대신 실행된다.
- 예외 처리 메서드가 반환한 화면 이름이나 응답값을 기준으로 사용자에게 오류 화면을 보여 준다.
이 흐름에서 핵심은 예외가 발생한 뒤에 새로운 요청이 다시 들어오는 것이 아니라, 기존 요청 흐름 안에서 예외 처리 흐름으로 방향이 바뀐다는 점이다.
사용자는 한 번 요청했지만, 서버 내부에서는 정상 처리 흐름이 실패하고 예외 처리 흐름으로 넘어간다.
정상 흐름과 예외 흐름을 비교하면 차이가 더 분명하다.
- 정상 흐름: 요청 → 매개변수 준비 성공 → 컨트롤러 실행 → 정상
return→ 화면 응답- 예외 흐름: 요청 → 매개변수 준비 실패 또는 실행 중 예외 발생 → 예외 처리 메서드 실행 → 오류 화면 응답
따라서 오류 처리를 이해할 때는 “예외가 발생하면 기존 메서드가 계속 실행되는가?”를 먼저 봐야 한다.
정답은 아니다.
예외가 발생하면 기존 메서드의 정상 흐름은 멈추고, 예외 처리 흐름으로 넘어간다.
예외가 발생하는 위치
예외는 크게 두 위치에서 발생할 수 있다.
첫 번째는 컨트롤러 메서드 본문이 실행되기 전이다.
두 번째는 컨트롤러 메서드 본문이 실행되는 중이다.
이 차이를 알아야 뒤에서 나오는num예제가 이해된다.
컨트롤러 메서드 본문이 실행되기 전에도 예외가 발생할 수 있다.
예를 들어 컨트롤러 메서드가int num을 받는다고 생각해 보자.
int는 숫자만 담을 수 있는 타입이다.
그런데 사용자가/exceptionTest?num=abc처럼 문자를 보내면Spring MVC는abc를 숫자로 바꾸려고 시도한다.
이 변환은 메서드 본문이 실행되기 전에 일어난다.
왜냐하면detail(int num, Model model)메서드를 실행하려면 먼저num자리에 넣을 값을 준비해야 하기 때문이다.
그런데abc는int로 바꿀 수 없으므로, 메서드 안쪽 코드가 실행되기 전에 예외가 발생한다.// TypeMismatchFlowExample.java @GetMapping("/exceptionTest") public String detail(int num, Model model) { // num 값이 int로 변환된 뒤에야 이 코드가 실행될 수 있다 model.addAttribute("num", num); return "resultView"; }
/exceptionTest?num=10처럼 숫자를 보내면num에10이 들어가고 메서드 본문이 실행된다.
하지만/exceptionTest?num=abc처럼 문자를 보내면abc를int로 바꾸는 과정에서 실패한다.
그러면detail()메서드 안쪽 코드는 실행되지 않고, 바로 예외 처리 흐름으로 넘어간다.
두 번째는 컨트롤러 메서드 본문이 실행되는 중에 예외가 발생하는 경우이다.
이 경우에는 요청값 변환까지는 성공했지만, 메서드 내부 로직을 처리하다가 문제가 생긴 것이다.
예를 들어num=1은 숫자이므로int num에 들어갈 수 있다.
하지만 그 번호에 해당하는 데이터가 없으면 컨트롤러 내부에서 직접 예외를 발생시킬 수 있다.// NotFoundFlowExample.java @GetMapping("/exceptionTest") public String detail(int num, Model model) throws FriendNotFoundException { FriendDTO friend = friendService.get(num); // num으로 친구 정보를 조회한다 if (friend == null) { // 조회 결과가 없으면 직접 만든 예외를 발생시킨다 throw new FriendNotFoundException(); } model.addAttribute("friend", friend); // 친구 정보를 화면으로 넘긴다 return "friendView"; // 정상 화면 이름을 반환한다 }이 흐름에서는
num을 숫자로 받는 데는 성공했다.
그래서 메서드 본문은 실행된다.
하지만friendService.get(num)결과가null이면 정상 화면을 만들 수 없다.
이때FriendNotFoundException을 발생시키면, 그 순간부터 정상 화면인friendView로 가는 흐름은 멈춘다.
그리고 이 예외를 처리할 수 있는@ExceptionHandler를 찾는다.
정리하면 예외 발생 위치에 따라 흐름은 이렇게 나뉜다.
- 요청값을 컨트롤러 매개변수로 바꾸는 과정에서 실패하면, 컨트롤러 메서드 본문이 실행되기 전에 예외 처리 흐름으로 넘어간다.
- 컨트롤러 메서드 본문이 실행된 뒤 로직 처리 중 문제가 생기면, 실행 중이던 메서드가 멈추고 예외 처리 흐름으로 넘어간다.
- 두 경우 모두 최종적으로는 해당 예외를 처리할 수 있는
@ExceptionHandler메서드를 찾는다.그래서 오류 처리를 볼 때는 “예외가 어디서 발생했는가”를 먼저 봐야 한다.
요청값 변환 단계에서 발생한 예외인지, 컨트롤러 내부 로직에서 직접 발생시킨 예외인지에 따라 원인 설명이 달라진다.
예외 처리 메서드가 응답을 만드는 방식
예외 처리 메서드가 발견되면 그 메서드가 대신 응답을 만든다.
예외 처리 메서드도 일반 컨트롤러 메서드처럼String으로 화면 이름을 반환할 수 있다.
또는ModelAndView를 사용해서 화면 이름과 오류 메시지를 함께 반환할 수도 있다.
예를 들어@ExceptionHandler메서드가"errorPage"를 반환하면 정상 화면 대신errorPage.html이 열린다.
ModelAndView에msg값을 담아 반환하면 오류 화면에서 그 메시지를 출력할 수 있다.
즉, 예외가 발생해도 사용자는 서버의 복잡한 오류 화면이 아니라 개발자가 준비한 오류 화면을 보게 된다.
예외 처리 흐름은 실패를 없애는 흐름이 아니라, 실패했을 때 사용자에게 어떤 응답을 보여 줄지 바꾸는 흐름이다.
정상 요청은 정상 화면으로 가고, 문제가 생긴 요청은 예외 처리 메서드를 거쳐 오류 화면으로 간다.
이 차이를 이해하면@ExceptionHandler와@ControllerAdvice가 왜 필요한지 자연스럽게 이어진다.
핵심 정리
이 구간의 핵심을 다시 정리하면 이렇다.
- 정상 흐름에서는 매개변수 준비가 성공한 뒤 컨트롤러 메서드가 끝까지 실행되고,
return으로 화면을 결정한다.- 예외가 발생하면 원래 메서드의 정상
return흐름은 멈춘다.Spring MVC는 발생한 예외를 처리할 수 있는@ExceptionHandler메서드를 찾는다.- 예외 처리 메서드는 개발자가 직접 호출하지 않고, 예외가 발생했을 때
Spring MVC가 자동으로 실행한다.- 예외는 요청값 변환 단계에서 발생할 수도 있고, 컨트롤러 내부 로직에서 발생할 수도 있다.
- 예외 처리 메서드가 반환한 값이 최종 오류 화면이나 오류 응답이 된다.
따라서 예외가 발생했을 때의 흐름은 요청 → 컨트롤러 실행 → 정상 화면 반환이 아니다.
예외가 발생한 순간부터 요청 → 매개변수 준비 실패 또는 실행 중 예외 발생 → 예외 처리 메서드 실행 → 오류 화면 반환 흐름으로 바뀐다.
@ExceptionHandler는 특정Controller안에서 발생한 예외를 처리하는 메서드에 붙이는 어노테이션이다.
쉽게 말하면, 같은 컨트롤러 안에서 문제가 생겼을 때 대신 실행될 예외 처리 메서드를 지정하는 표시이다.
앞에서 예외가 발생하면 원래 컨트롤러 메서드의 정상return흐름이 멈춘다고 정리했다.
그다음Spring MVC는 발생한 예외를 처리할 수 있는 메서드를 찾는다.
이때 같은 컨트롤러 안에@ExceptionHandler가 붙은 메서드가 있으면, 그 메서드가 예외를 대신 처리한다.
@ExceptionHandler의 역할
@ExceptionHandler는 일반 요청 주소를 처리하는 어노테이션이 아니다.
@GetMapping,@PostMapping,@RequestMapping처럼 사용자가 직접 요청한URL과 연결되는 용도가 아니다.
@ExceptionHandler는 예외가 발생했을 때 실행될 메서드를 지정한다.
즉, 사용자가 직접/handle-error같은 주소로 호출하는 메서드가 아니라, 컨트롤러 처리 중 예외가 발생했을 때Spring MVC가 자동으로 호출하는 메서드이다.
예를 들어 아래처럼 작성할 수 있다.// ExceptionHandlerBasicExample.java @ExceptionHandler(IllegalArgumentException.class) public String handleIllegalArgumentException() { // IllegalArgumentException이 발생했을 때 실행된다 return "errorView"; // 오류 화면 이름을 반환한다 }
@ExceptionHandler(IllegalArgumentException.class)는IllegalArgumentException이 발생하면 이 메서드가 처리하겠다는 뜻이다.
IllegalArgumentException은 잘못된 인자값이 들어왔을 때 사용할 수 있는 예외 클래스이다.
여기서 중요한 점은 괄호 안의 예외 클래스이다.
@ExceptionHandler는 괄호 안에 적은 예외 종류와 실제 발생한 예외 종류를 비교한다.
그리고 처리할 수 있는 예외라고 판단되면 해당 메서드를 실행한다.
@ExceptionHandler의 핵심은 예외가 발생했을 때 정상 화면 대신 실행될 예외 처리 메서드를 연결하는 것이다.
그래서 예외 처리 메서드는 요청을 처음부터 처리하는 메서드가 아니라, 실패한 요청 흐름을 이어받아 오류 응답을 만드는 메서드라고 이해하면 된다.
같은 컨트롤러 안에서 처리된다는 의미
@ExceptionHandler를Controller클래스 안에 작성하면 기본적으로 그 컨트롤러 안에서 발생한 예외를 처리한다.
이것을 지역 예외 처리라고 생각하면 쉽다.
지역 예외 처리는 여러 컨트롤러 전체가 아니라, 해당 컨트롤러 내부에 한정해서 예외를 처리하는 방식이다.
예를 들어MemberController안에@ExceptionHandler가 있다면,MemberController안에서 발생한 예외를 처리하는 흐름으로 보면 된다.
반대로 다른 컨트롤러에서 발생한 예외까지 공통으로 처리하고 싶다면 뒤에서 보는@ControllerAdvice를 사용한다.
흐름을 단계로 보면 이렇다.
- 사용자가 특정
URL을 요청한다.- 요청과 연결된
Controller메서드가 실행된다.- 메서드 실행 전이나 실행 중 예외가 발생한다.
Spring MVC가 같은 컨트롤러 안에서 해당 예외를 처리할@ExceptionHandler메서드를 찾는다.- 처리 가능한 메서드가 있으면 그 메서드가 실행된다.
- 예외 처리 메서드가 반환한 값으로 오류 화면이나 오류 응답이 만들어진다.
즉,
@ExceptionHandler는 같은 컨트롤러 안에서 발생한 문제를 그 컨트롤러 내부에서 정리하는 방식이다.
이 방식은 특정 컨트롤러에서만 다르게 처리해야 하는 예외가 있을 때 사용하기 좋다.
예외 클래스와 메서드가 연결되는 방식
@ExceptionHandler는 예외 클래스와 예외 처리 메서드를 연결한다.
예외 클래스는 어떤 종류의 예외인지 나타내는 클래스이다.
예를 들어IllegalArgumentException,RuntimeException,MethodArgumentTypeMismatchException같은 것들이 예외 클래스이다.
아래 코드는IllegalArgumentException을 처리하는 예외 처리 메서드이다.// IllegalArgumentExceptionHandlerExample.java @ExceptionHandler(IllegalArgumentException.class) public String handleIllegalArgumentException() { // IllegalArgumentException이 발생하면 이 메서드가 실행된다 return "errorView"; // errorView.html 화면으로 이동한다 }이 코드는
IllegalArgumentException이 발생했을 때만 실행되는 처리 메서드이다.
예외가 발생하지 않으면 실행되지 않는다.
또한 다른 종류의 예외가 발생했는데 이 메서드가 처리 대상이 아니라면 실행되지 않는다.
예외 처리 메서드는 발생한 예외 객체를 매개변수로 받을 수도 있다.
매개변수는 메서드가 실행될 때 외부에서 전달받는 값이다.
여기서는 발생한 예외 객체가 매개변수로 들어온다.// ExceptionMessageHandlerExample.java @ExceptionHandler(IllegalArgumentException.class) public String handleIllegalArgumentException(IllegalArgumentException ex, Model model) { // 발생한 예외 객체를 ex로 받는다 model.addAttribute("message", ex.getMessage()); // 예외 메시지를 화면으로 넘긴다 return "errorView"; // 오류 화면 이름을 반환한다 }
ex에는 실제로 발생한IllegalArgumentException객체가 들어온다.
그래서ex.getMessage()를 사용하면 예외를 발생시킬 때 넣은 메시지를 꺼낼 수 있다.
Model은 컨트롤러에서 화면으로 데이터를 넘길 때 사용하는 객체이다.
여기서는 예외 메시지를message라는 이름으로 화면에 전달한다.
그러면errorView.html에서${message}같은 방식으로 값을 출력할 수 있다.
즉,@ExceptionHandler메서드는 단순히 오류 화면만 반환하는 것이 아니다.
필요하면 발생한 예외 정보를 꺼내서 화면에 전달할 수도 있다.
기본 개념예제
아래 예제는
/error-test요청이 들어오면 일부러 예외를 발생시키고, 같은Controller안의@ExceptionHandler메서드가 그 예외를 처리하는 흐름이다.// ErrorBasicController.java package com.example.springedu.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.GetMapping; @Controller public class ErrorBasicController { @GetMapping("/error-test") public String errorTest() { // 일반 요청 처리 메서드가 실행된다 throw new IllegalArgumentException("잘못된 요청입니다."); // 예외를 일부러 발생시킨다 } @ExceptionHandler(IllegalArgumentException.class) public String handleIllegalArgumentException(IllegalArgumentException ex, Model model) { // IllegalArgumentException이 발생하면 이 메서드가 자동으로 실행된다 model.addAttribute("message", ex.getMessage()); // 예외 메시지를 화면으로 넘긴다 return "errorView"; // 오류 화면 이름을 반환한다 } }이 코드에서
/error-test를 요청하면 먼저errorTest()가 실행된다.
errorTest()는 정상 화면 이름을 반환하지 않는다.
대신throw new IllegalArgumentException("잘못된 요청입니다.")를 실행해서 예외를 발생시킨다.
예외가 발생하는 순간errorTest()의 정상 흐름은 끝난다.
그래서errorTest()가 직접 화면 이름을 반환하는 것이 아니다.
그다음Spring MVC가 같은 컨트롤러 안에서IllegalArgumentException을 처리할 수 있는 메서드를 찾는다.
같은 클래스 안에@ExceptionHandler(IllegalArgumentException.class)가 붙은handleIllegalArgumentException()이 있다.
그래서 이 메서드가 자동으로 실행된다.
여기서 자동으로 실행된다는 말은 개발자가 코드에서handleIllegalArgumentException()을 직접 호출하지 않는다는 뜻이다.
handleIllegalArgumentException()은 발생한 예외 객체를ex로 받는다.
그리고ex.getMessage()로"잘못된 요청입니다."라는 메시지를 꺼낸다.
그 값을model.addAttribute("message", ex.getMessage())로 화면에 넘긴다.
마지막으로"errorView"를 반환해서 오류 화면으로 이동한다.
오류 화면은 아래처럼 작성할 수 있다.// errorView.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>errorView</title> </head> <body> <h1>오류가 발생했습니다.</h1> <h2 th:text="${message}"></h2> <!-- 컨트롤러에서 넘긴 예외 메시지를 출력한다 --> </body> </html>이 화면은 컨트롤러가
message라는 이름으로 넘긴 예외 메시지를 출력한다.
th:text="${message}"는Thymeleaf에서message값을 화면에 출력하는 표현이다.
실행 결과는 아래처럼 볼 수 있다.// 출력결과 // /error-test 요청 // 오류가 발생했습니다. // 잘못된 요청입니다.이 예제의 요청 흐름을 다시 정리하면 이렇다.
- 사용자가
/error-test를 요청한다.errorTest()가 실행된다.IllegalArgumentException이 발생한다.errorTest()는 정상return을 하지 못한다.Spring MVC가@ExceptionHandler(IllegalArgumentException.class)메서드를 찾는다.handleIllegalArgumentException()이 자동으로 실행된다.- 예외 메시지가
Model에 담긴다."errorView"가 반환된다.errorView.html에 오류 메시지가 출력된다.이 예제의 핵심은 예외가 발생했을 때 일반 요청 처리 메서드가 멈추고, 같은 컨트롤러 안의 예외 처리 메서드로 흐름이 바뀐다는 점이다.
String 반환과 Model 사용 흐름
@ExceptionHandler메서드도 일반 컨트롤러 메서드처럼String을 반환할 수 있다.
여기서String은 오류 화면의 이름을 의미한다.
예를 들어"errorView"를 반환하면errorView.html화면으로 이동한다.
오류 화면에 단순 문구만 보여 줄 수도 있지만, 실제 예외 메시지를 보여 주고 싶을 때도 있다.
이때Model을 사용한다.
Model에 값을 담으면 오류 화면에서 그 값을 꺼내 출력할 수 있다.
흐름을 값 기준으로 보면 이렇다.// ExceptionMessageFlow.java throw new IllegalArgumentException("잘못된 요청입니다."); ex.getMessage(); // 결과: "잘못된 요청입니다." model.addAttribute("message", ex.getMessage()); // message라는 이름으로 오류 메시지를 화면에 전달한다 return "errorView"; // errorView.html이 열린다이 흐름에서
throw에 들어간 메시지가ex.getMessage()로 꺼내진다.
그리고 그 메시지가Model에 담겨errorView.html로 전달된다.
즉, 예외 처리 메서드는 오류 화면으로 보내는 역할만 하는 것이 아니다.
오류 화면에 보여 줄 메시지나 필요한 정보를 함께 넘기는 역할도 한다.
적용 범위 정리
@ExceptionHandler를 사용할 때 꼭 기억해야 할 점은 적용 범위이다.
컨트롤러 안에 작성한@ExceptionHandler는 기본적으로 그 컨트롤러 안에서 발생한 예외를 처리한다.
그래서 특정 컨트롤러에서만 특별한 오류 화면을 보여 주고 싶다면 컨트롤러 내부에@ExceptionHandler를 작성하면 된다.
예를 들어 회원 컨트롤러에서는 회원 오류 화면을 보여 주고, 게시글 컨트롤러에서는 게시글 오류 화면을 보여 주고 싶을 수 있다.
이런 경우에는 각 컨트롤러 안에 지역 예외 처리 메서드를 따로 둘 수 있다.
반대로 여러 컨트롤러에서 같은 예외 처리를 반복해서 사용해야 한다면@ControllerAdvice를 사용하는 편이 더 적합하다.
@ControllerAdvice는 여러 컨트롤러에 공통으로 적용되는 예외 처리 클래스를 만들 때 사용한다.
정리하면 이렇게 볼 수 있다.
- 특정 컨트롤러 안에서만 처리할 예외라면
@ExceptionHandler를 컨트롤러 내부에 작성한다.- 여러 컨트롤러에서 공통으로 처리할 예외라면
@ControllerAdvice안에@ExceptionHandler를 작성한다.@ExceptionHandler는 예외가 발생했을 때 실행될 메서드를 지정한다.- 예외 처리 메서드는 개발자가 직접 호출하지 않고
Spring MVC가 자동으로 호출한다.결국
@ExceptionHandler는 예외가 발생했을 때 정상 흐름을 오류 처리 흐름으로 바꾸는 가장 기본적인 도구이다.
이 개념을 이해하면 다음에 나오는@ControllerAdvice도 쉽게 연결된다.
@ControllerAdvice는 여러Controller에서 발생하는 예외를 공통으로 처리할 때 사용하는 어노테이션이다.
앞에서 본@ExceptionHandler는 특정 예외를 처리할 메서드를 지정하는 역할을 한다.
그런데@ExceptionHandler를 각 컨트롤러 안에 계속 작성하면 같은 예외 처리 코드가 여러 곳에 반복될 수 있다.
예를 들어 회원 컨트롤러, 게시글 컨트롤러, 댓글 컨트롤러에서 모두RuntimeException계열 오류를 같은 오류 화면으로 보내고 싶다고 생각해 보자.
이때 각 컨트롤러마다 똑같은@ExceptionHandler메서드를 작성하면 코드가 불필요하게 반복된다.
이런 반복을 줄이기 위해 공통 예외 처리 클래스를 만들 수 있다.
그 공통 예외 처리 클래스에 붙이는 어노테이션이@ControllerAdvice이다.
@ControllerAdvice가 필요한 이유
@ExceptionHandler를 컨트롤러 내부에 작성하면 해당 컨트롤러 안에서 발생한 예외를 처리할 수 있다.
이 방식은 특정 컨트롤러에서만 다른 오류 화면을 보여 주고 싶을 때 좋다.
하지만 여러 컨트롤러에서 같은 방식으로 예외를 처리해야 한다면 중복이 생긴다.
예를 들어 아래와 같은 상황을 생각하면 된다.
- 회원 조회 중 예외가 발생하면 공통 오류 화면으로 보낸다.
- 게시글 조회 중 예외가 발생해도 공통 오류 화면으로 보낸다.
- 댓글 처리 중 예외가 발생해도 공통 오류 화면으로 보낸다.
이 세 컨트롤러가 모두 같은 오류 화면을 사용한다면, 각 컨트롤러 안에 같은 예외 처리 메서드를 반복해서 둘 필요가 없다.
공통 예외 처리 클래스를 하나 만들고, 그 안에서 예외를 처리하면 된다.
@ControllerAdvice는 여러 컨트롤러에서 반복될 수 있는 예외 처리 코드를 한 곳으로 모아 관리하기 위한 구조이다.
그래서 특정 컨트롤러 전용 예외 처리는 컨트롤러 내부의@ExceptionHandler가 어울리고, 여러 컨트롤러에 공통으로 적용할 예외 처리는@ControllerAdvice가 어울린다.
@ControllerAdvice와 @ExceptionHandler의 관계
@ControllerAdvice만 붙인다고 예외가 자동으로 처리되는 것은 아니다.
@ControllerAdvice는 공통 예외 처리 클래스를 만들겠다는 표시이다.
실제로 어떤 예외를 처리할지는 그 클래스 안에 작성한@ExceptionHandler가 정한다.
즉, 둘의 역할은 다르다.
@ControllerAdvice는 여러 컨트롤러에 공통으로 적용되는 예외 처리 클래스를 만든다.@ExceptionHandler는 그 안에서 어떤 예외를 어떤 메서드가 처리할지 정한다.둘은 서로 대체 관계가 아니다.
@ControllerAdvice는 공통 처리 범위를 만들고,@ExceptionHandler는 실제 처리할 예외 종류를 지정한다.
아래처럼 이해하면 쉽다.// CommonExceptionHandler.java @ControllerAdvice public class CommonExceptionHandler { @ExceptionHandler(RuntimeException.class) public String handleRuntimeException() { // RuntimeException 계열 예외를 공통으로 처리한다 return "commonError"; } }
@ControllerAdvice가 붙은CommonExceptionHandler는 여러 컨트롤러에서 발생한 예외를 처리할 수 있는 공통 클래스가 된다.
그 안의@ExceptionHandler(RuntimeException.class)는RuntimeException계열 예외가 발생했을 때 실행될 메서드를 지정한다.
정리하면@ControllerAdvice는 “어디에 공통으로 적용할 것인가”에 가깝고,@ExceptionHandler는 “어떤 예외를 처리할 것인가”에 가깝다.
RuntimeException 계열 예외 처리
RuntimeException은 실행 중에 발생할 수 있는 예외의 큰 종류이다.
여기서 “큰 종류”라는 말은RuntimeException아래에 여러 하위 예외가 있다는 뜻이다.
예를 들어IllegalStateException은RuntimeException의 하위 예외이다.
그래서@ExceptionHandler(RuntimeException.class)로 작성한 메서드는RuntimeException뿐만 아니라IllegalStateException같은 하위 예외도 함께 처리할 수 있다.
흐름을 간단히 보면 이렇다.
IllegalStateException이 발생한다.IllegalStateException은RuntimeException의 하위 예외이다.@ExceptionHandler(RuntimeException.class)가 처리할 수 있는 예외 범위에 포함된다.- 그래서 공통 예외 처리 메서드가 실행될 수 있다.
이 구조를 이해하면 응용예제에서
IllegalStateException지역 핸들러를 막았을 때 공통 핸들러가 대신 처리하는 이유를 이해할 수 있다.
IllegalStateException자체를 직접 지정하지 않아도, 상위 타입인RuntimeException을 처리하는 메서드가 있기 때문이다.
다만 공통으로 너무 넓은 예외를 잡으면 어떤 컨트롤러에서 어떤 문제가 발생했는지 구분하기 어려울 수 있다.
그래서 실제로는 예외 종류에 따라 필요한 만큼 나누어 처리하는 것이 좋다.
이 글에서는 공통 처리 흐름을 이해하기 위해RuntimeException을 기준으로 본다.
기본 개념예제
아래 예제는 여러 컨트롤러에서 발생할 수 있는
RuntimeException계열 예외를 공통 예외 처리 클래스에서 처리하는 흐름이다.
먼저 예외가 발생하는 컨트롤러를 본다.// CommonErrorTestController.java package com.example.springedu.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class CommonErrorTestController { @GetMapping("/common-error1") public String commonError1() { // 첫 번째 요청에서 예외가 발생한다 throw new RuntimeException("첫 번째 컨트롤러 오류입니다."); // 공통 처리 대상 예외 } @GetMapping("/common-error2") public String commonError2() { // 두 번째 요청에서도 예외가 발생한다 throw new IllegalStateException("두 번째 컨트롤러 오류입니다."); // RuntimeException의 하위 예외 } }이 컨트롤러에는 예외 처리 메서드가 없다.
/common-error1을 요청하면RuntimeException이 발생한다.
/common-error2를 요청하면IllegalStateException이 발생한다.
하지만CommonErrorTestController안에는@ExceptionHandler가 없다.
따라서 이 컨트롤러 내부에서 예외를 직접 처리하지 않는다.
이때 공통 예외 처리 클래스가 있으면, 그 클래스가 예외를 대신 처리할 수 있다.
공통 예외 처리 클래스는 아래처럼 작성한다.// CommonExceptionHandler.java package com.example.springedu.controller; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; @ControllerAdvice public class CommonExceptionHandler { @ExceptionHandler(RuntimeException.class) public ModelAndView handleRuntimeException(RuntimeException ex) { // RuntimeException 계열 예외가 발생하면 이 메서드가 실행된다 ModelAndView mav = new ModelAndView(); // 화면 이름과 데이터를 함께 담을 객체를 만든다 mav.setViewName("commonError"); // 공통 오류 화면 이름을 지정한다 mav.addObject("exception", ex); // 예외 객체를 화면으로 전달한다 return mav; // 화면 이름과 예외 정보를 함께 반환한다 } }이 클래스에는
@ControllerAdvice가 붙어 있다.
그래서 특정 컨트롤러 하나에만 묶이지 않고, 여러 컨트롤러에서 발생하는 예외를 공통으로 처리할 수 있다.
handleRuntimeException()에는@ExceptionHandler(RuntimeException.class)가 붙어 있다.
따라서RuntimeException또는 그 하위 예외가 발생했을 때 이 메서드가 실행된다.
/common-error1에서 발생한RuntimeException도 처리할 수 있고,/common-error2에서 발생한IllegalStateException도 처리할 수 있다.
ModelAndView는 모델 데이터와 뷰 이름을 한 객체에 함께 담는 방식이다.
여기서는"commonError"라는 오류 화면 이름과exception이라는 예외 객체를 함께 담는다.
즉, “어떤 오류 화면을 보여 줄지”와 “화면에 어떤 예외 정보를 넘길지”를 한 번에 반환하는 구조이다.
오류 화면은 아래처럼 작성할 수 있다.// commonError.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>commonError</title> </head> <body> <h1>공통 오류 화면입니다.</h1> <h2 th:text="${exception.message}"></h2> <!-- 전달받은 예외 객체의 메시지를 출력한다 --> </body> </html>이 화면은 공통 예외 처리 클래스에서 넘긴
exception객체의 메시지를 출력한다.
exception.message는 예외 객체 안에 들어 있는 메시지를 의미한다.
실행 결과는 아래처럼 볼 수 있다.// 출력결과 // /common-error1 요청 // 공통 오류 화면입니다. // 첫 번째 컨트롤러 오류입니다. // /common-error2 요청 // 공통 오류 화면입니다. // 두 번째 컨트롤러 오류입니다.
/common-error1은RuntimeException을 직접 발생시킨다.
그래서@ExceptionHandler(RuntimeException.class)가 처리한다.
/common-error2는IllegalStateException을 발생시킨다.
IllegalStateException은RuntimeException의 하위 예외이므로, 같은 공통 예외 처리 메서드가 처리할 수 있다.
이 예제의 핵심은 예외 처리 코드가 컨트롤러 안에 없어도,@ControllerAdvice가 붙은 공통 클래스에서 여러 컨트롤러의 예외를 처리할 수 있다는 점이다.
지역 예외 처리와 공통 예외 처리의 관계
@ControllerAdvice가 있다고 해서 컨트롤러 내부의@ExceptionHandler가 필요 없어지는 것은 아니다.
둘은 사용 목적이 다르다.
컨트롤러 내부의@ExceptionHandler는 특정 컨트롤러에서만 다르게 처리해야 하는 예외에 적합하다.
반면@ControllerAdvice는 여러 컨트롤러에서 같은 방식으로 처리할 예외에 적합하다.
예를 들어 회원 컨트롤러에서만 회원 없음 화면을 보여 주고 싶다면, 회원 컨트롤러 안에@ExceptionHandler를 둘 수 있다.
하지만 여러 컨트롤러에서 공통적으로 발생하는 실행 오류를 같은 오류 화면으로 보내고 싶다면,@ControllerAdvice를 사용하는 것이 더 자연스럽다.
흐름을 정리하면 이렇다.
- 특정 컨트롤러만의 오류 처리라면 컨트롤러 내부의
@ExceptionHandler를 사용한다.- 여러 컨트롤러에서 반복되는 오류 처리라면
@ControllerAdvice를 사용한다.@ControllerAdvice안에서도 실제 예외를 잡는 역할은@ExceptionHandler가 한다.- 지역 예외 처리는 개별 컨트롤러의 특수한 상황을 처리하기 좋다.
- 공통 예외 처리는 여러 컨트롤러의 반복되는 예외 처리 코드를 한 곳으로 모으기 좋다.
이 관계를 알아야 뒤의 응용예제에서 지역 핸들러와 공통 핸들러의 차이를 이해할 수 있다.
컨트롤러 안에 해당 예외를 처리하는 메서드가 있으면 지역 예외 처리로 볼 수 있고, 공통 클래스에서 처리하면 공통 예외 처리로 볼 수 있다.
핵심 정리
@ControllerAdvice는 여러 컨트롤러에 공통으로 적용되는 예외 처리 클래스를 만들 때 사용한다.
@ExceptionHandler는 그 클래스 안에서 어떤 예외를 처리할지 지정한다.
정리하면 이렇게 볼 수 있다.
@ControllerAdvice는 공통 예외 처리 클래스를 만든다.@ControllerAdvice만으로 예외가 처리되는 것은 아니고, 내부에@ExceptionHandler가 필요하다.@ExceptionHandler(RuntimeException.class)는RuntimeException과 그 하위 예외를 처리할 수 있다.ModelAndView를 사용하면 오류 화면 이름과 예외 정보를 함께 담아 반환할 수 있다.- 지역 예외 처리는 특정 컨트롤러 전용 처리에 적합하다.
- 공통 예외 처리는 여러 컨트롤러에서 반복되는 예외 처리에 적합하다.
결국
@ControllerAdvice는 예외 처리 코드를 한 곳으로 모으기 위한 구조이다.
예외가 발생하는 위치는 여러 컨트롤러로 흩어져 있어도, 처리 방식이 같다면 공통 예외 처리 클래스로 모아서 관리할 수 있다.
@ExceptionHandler와@ControllerAdvice는 둘 다 예외 처리를 위해 사용한다.
하지만 두 어노테이션은 같은 일을 하는 것이 아니다.
@ExceptionHandler는 어떤 예외를 어떤 메서드가 처리할지 정하는 역할을 한다.
@ControllerAdvice는 그런 예외 처리 메서드를 여러Controller에 공통으로 적용할 수 있게 만드는 역할을 한다.
즉, 둘은 대체 관계가 아니다.
@ControllerAdvice를 쓰면@ExceptionHandler가 필요 없어지는 것이 아니다.
공통 예외 처리 클래스를 만들 때도 실제 예외를 잡는 메서드에는@ExceptionHandler가 필요하다.
역할의 차이
@ExceptionHandler는 예외 처리 메서드에 붙인다.
이 어노테이션은 “이 예외가 발생하면 이 메서드가 처리한다”는 연결 규칙을 만든다.
예를 들어@ExceptionHandler(IllegalArgumentException.class)라고 작성하면IllegalArgumentException이 발생했을 때 해당 메서드가 실행된다.
반면@ControllerAdvice는 클래스에 붙인다.
이 어노테이션은 “이 클래스 안의 예외 처리 메서드들을 여러 컨트롤러에 공통으로 적용한다”는 범위를 만든다.
따라서@ControllerAdvice가 붙은 클래스 안에도 실제 예외를 처리할@ExceptionHandler메서드가 있어야 한다.
둘의 역할을 짧게 나누면 이렇다.
@ExceptionHandler: 어떤 예외를 어떤 메서드가 처리할지 정한다.@ControllerAdvice: 예외 처리 메서드를 여러 컨트롤러에 공통으로 적용할 범위를 만든다.따라서
@ExceptionHandler는 예외와 메서드를 연결하는 어노테이션이고,@ControllerAdvice는 공통 예외 처리 클래스를 만드는 어노테이션이라고 이해하면 된다.
적용 범위의 차이
가장 쉬운 구분은 적용 범위이다.
@ExceptionHandler를 특정Controller안에 작성하면 그 컨트롤러에서 발생한 예외를 처리한다.
이런 방식은 지역 예외 처리라고 볼 수 있다.
지역 예외 처리는 특정 컨트롤러에서만 다른 오류 화면이나 다른 메시지를 보여 주고 싶을 때 사용하기 좋다.
예를 들어 회원 컨트롤러에서 발생한 회원 없음 오류는 회원 전용 오류 화면으로 보내고 싶을 수 있다.
이런 경우에는 회원 컨트롤러 안에@ExceptionHandler를 두면 된다.
반면@ControllerAdvice가 붙은 클래스 안에@ExceptionHandler를 작성하면 여러 컨트롤러에서 발생한 예외를 공통으로 처리할 수 있다.
이 방식은 공통 예외 처리라고 볼 수 있다.
여러 컨트롤러에서 같은 오류 화면이나 같은 오류 메시지를 사용해야 할 때 적합하다.
적용 범위를 비교하면 이렇다.
- 컨트롤러 내부의
@ExceptionHandler: 해당 컨트롤러 중심으로 동작한다.@ControllerAdvice안의@ExceptionHandler: 여러 컨트롤러에 공통으로 동작한다.초보자는 먼저 이렇게 기억하면 된다.
특정 컨트롤러 안에서만 처리하면@ExceptionHandler, 여러 컨트롤러에 공통으로 적용하면@ControllerAdvice이다.
둘을 함께 쓰는 구조
@ControllerAdvice만 붙인다고 예외가 자동으로 처리되는 것은 아니다.
공통 처리 클래스 안에도 실제 예외를 잡을@ExceptionHandler메서드가 필요하다.
아래처럼 구조를 보면 더 쉽다.// CommonExceptionHandler.java @ControllerAdvice public class CommonExceptionHandler { @ExceptionHandler(RuntimeException.class) public String handleRuntimeException() { // RuntimeException 계열 예외가 발생하면 이 메서드가 실행된다 return "commonError"; } }이 코드에서
@ControllerAdvice는CommonExceptionHandler클래스를 공통 예외 처리 클래스로 만든다.
그리고@ExceptionHandler(RuntimeException.class)는RuntimeException계열 예외가 발생했을 때handleRuntimeException()메서드가 실행되도록 연결한다.
즉,@ControllerAdvice는 공통 처리 범위를 만들고,@ExceptionHandler는 실제 처리할 예외 종류를 지정한다.
둘 중 하나만으로 전체 공통 예외 처리 흐름이 완성되는 것이 아니라, 두 어노테이션이 함께 사용되면서 공통 예외 처리 구조가 만들어진다.
지역 처리와 공통 처리 중 무엇이 먼저인가
예외 처리를 볼 때는 지역 처리와 공통 처리를 함께 생각해야 한다.
특정 컨트롤러 안에 해당 예외를 처리하는@ExceptionHandler가 있으면, 그 컨트롤러 내부에서 예외가 처리되는 흐름으로 볼 수 있다.
이것이 지역 예외 처리이다.
반대로 컨트롤러 내부에 해당 예외를 처리할 메서드가 없거나, 해당 예외를 잡는 지역 핸들러가 없다면@ControllerAdvice가 붙은 공통 예외 처리 클래스가 처리할 수 있다.
뒤의 응용예제에서도IllegalStateException을 처리하는 지역 핸들러를 막으면,RuntimeException을 처리하는 공통 핸들러가 대신 실행된다.
흐름을 정리하면 이렇다.
- 예외가 발생한 컨트롤러 안에 해당 예외를 처리할
@ExceptionHandler가 있으면 지역 처리로 해결된다.- 해당 컨트롤러 안에 처리할 메서드가 없으면 공통 예외 처리 클래스를 확인한다.
- 공통 클래스의
@ExceptionHandler가 해당 예외를 처리할 수 있으면 공통 처리로 해결된다.이 흐름을 이해하면 응용예제에서 지역 핸들러를 주석으로 막았을 때 왜
CommonExceptionHandler가 대신 실행되는지 자연스럽게 이해할 수 있다.
지역 예외 처리는 개별 컨트롤러의 특수한 오류를 처리하고, 공통 예외 처리는 여러 컨트롤러에 반복되는 오류를 한 곳에서 처리한다.
반환 방식의 차이
예외 처리 메서드는 일반 컨트롤러 메서드처럼 값을 반환한다.
반환값은 사용자에게 어떤 오류 화면이나 오류 응답을 보여 줄지 결정한다.
가장 단순한 방식은String을 반환하는 것이다.
String은 오류 화면 이름만 반환할 때 사용한다.
예를 들어"noFriend"를 반환하면noFriend.html화면으로 이동한다.// StringReturnExceptionHandlerExample.java @ExceptionHandler(FriendNotFoundException.class) public String handleNotFoundException() { // 화면에 추가로 넘길 데이터가 없으면 화면 이름만 반환한다 return "noFriend"; }이 방식은 오류 화면에 따로 전달할 데이터가 없을 때 단순하고 편하다.
즉, 화면 이름만 정하면 되는 경우에 사용한다.
반대로 오류 화면 이름과 함께 메시지나 예외 객체 같은 데이터를 넘겨야 할 때는ModelAndView를 사용할 수 있다.
ModelAndView는 모델 데이터와 뷰 이름을 한 객체에 함께 담는 방식이다.// ModelAndViewExceptionHandlerExample.java @ExceptionHandler(RuntimeException.class) public ModelAndView handleRuntimeException(RuntimeException ex) { // 화면 이름과 예외 정보를 함께 담는다 ModelAndView mav = new ModelAndView(); mav.setViewName("commonErrorPage"); // 오류 화면 이름을 지정한다 mav.addObject("exceptionInfo", ex); // 예외 객체를 화면으로 넘긴다 return mav; }이 방식은 오류 화면에서 실제 예외 정보나 오류 메시지를 출력해야 할 때 사용한다.
예를 들어exceptionInfo라는 이름으로 예외 객체를 넘기면 화면에서 그 값을 출력할 수 있다.
반환 방식을 비교하면 이렇다.
String: 오류 화면 이름만 반환할 때 사용한다.ModelAndView: 오류 화면 이름과 화면에 보여 줄 데이터를 함께 반환할 때 사용한다.즉, 화면 이름만 필요하면
String, 화면 이름과 데이터가 함께 필요하면ModelAndView라고 구분하면 된다.
핵심 정리
@ExceptionHandler와@ControllerAdvice는 둘 다 예외 처리와 관련되어 있지만, 역할과 적용 범위가 다르다.
@ExceptionHandler는 실제로 예외를 처리할 메서드를 지정하고,@ControllerAdvice는 그 예외 처리 메서드를 여러 컨트롤러에 공통으로 적용할 수 있는 클래스를 만든다.
정리하면 이렇다.
@ExceptionHandler는 특정 예외를 처리할 메서드를 지정한다.- 컨트롤러 내부의
@ExceptionHandler는 해당 컨트롤러 중심의 지역 예외 처리로 동작한다.@ControllerAdvice는 공통 예외 처리 클래스를 만든다.@ControllerAdvice안의@ExceptionHandler는 여러 컨트롤러의 예외를 공통으로 처리한다.- 지역 핸들러가 있으면 해당 컨트롤러 안에서 먼저 처리되는 흐름으로 볼 수 있다.
- 지역 핸들러가 없거나 해당 예외를 처리하지 않으면 공통 핸들러가 처리할 수 있다.
- 화면 이름만 반환하면
String, 화면 이름과 데이터를 함께 반환하면ModelAndView를 사용한다.오류 처리의 핵심은 예외가 발생한 뒤에도 사용자에게 보여 줄 응답 흐름을 개발자가 직접 정리할 수 있다는 점이다.
@ExceptionHandler는 개별 컨트롤러 예외 처리에 적합하고,@ControllerAdvice는 공통 예외 처리에 적합하다.
이 차이를 알면 이후 응용예제에서MethodArgumentTypeMismatchException, 직접 만든 예외, 공통 오류 화면이 어떻게 연결되는지도 자연스럽게 이해할 수 있다.
1. 지역 예외 처리와 공통 예외 처리 (ExceptionLocalController.java, CommonExceptionHandler.java, FriendService.java, FriendDTO.java, FriendNotFoundException.java, errorPage.html, friendView.html, noFriend.html, commonErrorPage.html)
앞에서는
@ExceptionHandler가 특정Controller안에서 발생한 예외를 처리하고,@ControllerAdvice가 여러Controller에서 발생하는 예외를 공통으로 처리한다고 정리했다.
그런데 이 예제는 단순히 두 어노테이션의 차이만 보여 주는 예제가 아니다.
실제로는 요청 파라미터 타입 변환 실패, 직접 만든 예외 발생,Service조회 결과에 따른 정상 화면 반환,ModelAndView를 이용한 오류 메시지 전달, 지역 예외 처리와 공통 예외 처리의 우선 흐름까지 함께 보여 준다.
즉, 이 묶음은 하나의/exceptionTest요청에서num값에 따라 정상 처리, 지역 예외 처리, 공통 예외 처리가 어떻게 갈라지는지 확인하는 응용예제이다.
먼저 요청 흐름만 간단히 보면 이렇다.
/exceptionTest처럼num을 전달하지 않으면 이 예제 실행 결과에서는IllegalStateException처리 핸들러가 실행된다./exceptionTest?num=이언덕처럼 숫자 자리에 문자를 전달하면MethodArgumentTypeMismatchException이 발생한다./exceptionTest?num=10처럼 정상 숫자를 전달하고 해당 친구 정보가 있으면friendView.html이 출력된다./exceptionTest?num=1처럼 숫자 형식은 맞지만 친구 정보가 없으면 직접 만든FriendNotFoundException이 발생한다.IllegalStateException을 처리하는 지역 핸들러를 막으면CommonExceptionHandler가 공통으로 예외를 처리한다.
이 예제가 같이 보여주는 개념
이 예제의 중심은
ExceptionLocalController.java이다.
이 컨트롤러는/exceptionTest요청을 받고, 요청 파라미터num을int타입으로 받는다.
int는 숫자만 받을 수 있는 기본형 타입이다.
그래서num이 없거나 숫자로 바꿀 수 없는 값이 들어오면 정상적으로 메서드를 실행하기 어렵다.// ExceptionLocalController.java package com.example.springedu.controller; import java.io.IOException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; import org.springframework.web.servlet.ModelAndView; import com.example.springedu.service.FriendNotFoundException; import com.example.springedu.service.FriendService; import com.example.springedu.domain.FriendDTO; @Controller public class ExceptionLocalController { @Autowired FriendService ms; // 친구 정보를 조회하는 서비스 객체를 주입받는다 @RequestMapping("/exceptionTest") public String detail(int num, Model model) throws FriendNotFoundException { FriendDTO vo = ms.get(num); // num 값으로 친구 정보를 조회한다 if (vo == null) { throw new FriendNotFoundException(); // 친구 정보가 없으면 직접 만든 예외를 발생시킨다 } model.addAttribute("friend", vo); // 조회된 친구 정보를 화면으로 넘긴다 return "friendView"; // 정상 조회 화면으로 이동한다 } @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ModelAndView handleTypeMismatchException(MethodArgumentTypeMismatchException ex) { System.out.println("TypeMismatchException 발생시 처리하는 핸들러가 오류 처리합니다."); ModelAndView mav = new ModelAndView(); // 화면 이름과 데이터를 함께 담을 객체를 만든다 mav.addObject("msg", "타입을 맞춰주세용!!"); // 오류 화면에 보여 줄 메시지를 담는다 mav.setViewName("errorPage"); // errorPage.html로 이동한다 return mav; } @ExceptionHandler(FriendNotFoundException.class) public String handleNotFoundException() throws IOException { System.out.println("FriendNotFoundException 발생시 처리하는 핸들러가 오류 처리합니다."); return "noFriend"; // noFriend.html로 이동한다 } @ExceptionHandler(IllegalStateException.class) // 주석으로 막을시 CommonExceptionHandler가 처리함 public ModelAndView handleIllegalStateException() throws IOException { System.out.println("IllegalStateException 발생시 처리하는 핸들러가 오류 처리합니다."); ModelAndView mav = new ModelAndView(); // 화면 이름과 데이터를 함께 담을 객체를 만든다 mav.addObject("msg", "num=숫자 형식의 쿼리를 전달하세요!!"); // 오류 메시지를 담는다 mav.setViewName("errorPage"); // errorPage.html로 이동한다 return mav; } }이 코드에서 가장 먼저 봐야 할 부분은
detail(int num, Model model)이다.
/exceptionTest?num=10처럼 요청하면num에는10이 들어간다.
그리고FriendService의get(num)을 호출해서 친구 정보를 조회한다.
여기서Model은 컨트롤러에서 화면으로 데이터를 넘길 때 사용하는 객체이다.
정상적으로 친구 정보를 찾으면model.addAttribute("friend", vo)로friend라는 이름에 친구 정보를 담는다.
이렇게 담긴 값은friendView.html에서[[${ friend.name }]]처럼 꺼내 쓸 수 있다.
반대로FriendService가 친구 정보를 찾지 못하면vo는null이 된다.
이때 컨트롤러는 직접 만든FriendNotFoundException을 발생시킨다.
즉, 이 예제는 값이 없을 때 단순히 화면을 반환하는 것이 아니라, 직접 만든 예외를 발생시키고 그 예외를 지역 핸들러에서 처리하는 흐름을 보여 준다.
FriendService.java는 실제 친구 정보를 조회하는 역할을 한다.
여기서는num이10일 때만 친구 정보를 만들어 반환한다.
그 외 숫자는 전부null을 반환한다.// FriendService.java package com.example.springedu.service; import org.springframework.stereotype.Service; import com.example.springedu.domain.FriendDTO; @Service public class FriendService { public FriendDTO get(int num) { FriendDTO vo = null; // 처음에는 친구 정보가 없다고 둔다 if (num == 10) { vo = new FriendDTO(); // num이 10이면 친구 정보 객체를 만든다 vo.setPhoneNum("010-1111-2222"); // 전화번호를 저장한다 vo.setName("Dooly"); // 이름을 저장한다 } return vo; // 친구 정보가 있으면 객체를, 없으면 null을 반환한다 } }
@Service는 이 클래스가 비즈니스 로직을 처리하는Spring Bean이라는 표시이다.
비즈니스 로직은 실제 서비스에서 필요한 처리 규칙을 의미한다.
이 예제에서는 “num이10이면 친구 정보를 반환하고, 아니면 없다고 처리한다”가 비즈니스 로직이다.
FriendDTO.java는 친구 정보를 담는 객체이다.
DTO는 데이터를 옮기기 위해 사용하는 객체라고 이해하면 된다.
이 예제에서는 전화번호와 이름을 화면으로 넘기기 위해 사용한다.// FriendDTO.java package com.example.springedu.domain; import lombok.Getter; import lombok.Setter; @Getter @Setter public class FriendDTO { private String phoneNum; // 친구 전화번호 private String name; // 친구 이름 }
@Getter와@Setter는Lombok이 제공하는 어노테이션이다.
직접getPhoneNum(),setPhoneNum(),getName(),setName()메서드를 쓰지 않아도 자동으로 만들어 준다.
그래서friendView.html에서는friend.phoneNum,friend.name처럼 값을 꺼낼 수 있다.
친구 정보가 없을 때 사용할 예외는 직접 만든다.
FriendNotFoundException은Exception을 상속받는다.
이렇게 직접 만든 예외를 사용하면 “친구가 없는 상황”을 코드에서 더 명확하게 표현할 수 있다.// FriendNotFoundException.java package com.example.springedu.service; public class FriendNotFoundException extends Exception { private static final long serialVersionUID = 1L; // 예외 클래스 직렬화 버전 값 public FriendNotFoundException() { } public FriendNotFoundException(String message) { super(message); // 부모 예외 클래스에 메시지를 전달한다 } }이 예외는
Exception을 상속받았기 때문에 일반적인 실행 예외인RuntimeException과는 다르다.
그래서detail()메서드 선언부에throws FriendNotFoundException을 적어, 이 메서드에서 해당 예외가 발생할 수 있음을 표시한다.
오류 메시지를 보여 주는 화면은errorPage.html이다.
ModelAndView에msg라는 이름으로 담은 메시지를 화면에서 출력한다.// errorPage.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>[[${ msg }]]</h1> </body> </html>
[[${ msg }]]는Thymeleaf표현식이다.
컨트롤러에서mav.addObject("msg", "...")로 담은 값을 화면에 출력한다.
즉, 어떤 예외가 발생했는지에 따라 같은errorPage.html을 사용하더라도 다른 메시지를 보여 줄 수 있다.
정상적으로 친구 정보가 조회되면friendView.html이 사용된다.
이 화면은 컨트롤러가friend라는 이름으로 넘긴FriendDTO객체를 출력한다.// friendView.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h2>친구 정보</h2> <hr> <h3>전화 번호 : [[${ friend.phoneNum }]]</h3> <h3>이름 : [[${ friend.name }]]</h3> </body> </html>
friend.phoneNum은FriendDTO의 전화번호 값이고,friend.name은 이름 값이다.
num=10일 때만FriendService가 이 값을 만들어 주기 때문에 정상 화면은num=10요청에서만 볼 수 있다.
친구 정보가 없으면noFriend.html이 사용된다.
이 화면은 별도의 데이터를 받지 않고, 단순히 친구가 없다는 메시지만 출력한다.// noFriend.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1>친구가 없당!!</h1> </body> </html>여기서
ExceptionLocalController의handleNotFoundException()은String으로"noFriend"를 반환한다.
String으로 뷰 이름만 반환해도 되는 이유는 화면에 따로 넘길 데이터가 없기 때문이다.
마지막으로 공통 예외 처리 클래스인CommonExceptionHandler.java를 봐야 한다.
이 클래스에는@ControllerAdvice가 붙어 있다.
따라서 특정 컨트롤러 하나에만 묶이지 않고, 여러 컨트롤러에서 발생하는 예외를 공통으로 처리할 수 있다.// CommonExceptionHandler.java package com.example.springedu.service; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.servlet.ModelAndView; @ControllerAdvice public class CommonExceptionHandler { @ExceptionHandler(RuntimeException.class) private ModelAndView errorModelAndView(Exception ex) { ModelAndView modelAndView = new ModelAndView(); // 화면 이름과 데이터를 함께 담을 객체를 만든다 modelAndView.setViewName("commonErrorPage"); // 공통 오류 화면 이름을 지정한다 modelAndView.addObject("msg", "RuntimeException은 내가 다 잡는다!!!"); // 공통 오류 메시지를 담는다 modelAndView.addObject("exceptionInfo", ex); // 발생한 예외 객체를 화면으로 넘긴다 return modelAndView; } }이 메서드는
Exception ex로 예외 객체를 받는다.
하지만@ExceptionHandler(RuntimeException.class)로 지정되어 있기 때문에, 실제로는RuntimeException계열 예외가 들어온다.
즉, 매개변수 타입은 넓게 받을 수 있지만, 어떤 예외를 처리할지는@ExceptionHandler의 괄호 안에 적은 예외 클래스가 기준이 된다.
예제에서는 예외 처리 메서드가private으로 작성되어 있다.
하지만 여기서 중요한 것은 접근 제한자가 아니라,@ControllerAdvice가 붙은 클래스 안에@ExceptionHandler(RuntimeException.class)가 선언되어 있다는 점이다.
즉, 이 메서드는RuntimeException계열 예외가 발생했을 때 공통 예외 처리 메서드로 사용된다.
공통 오류 화면은commonErrorPage.html이다.
CommonExceptionHandler에서msg와exceptionInfo를 화면으로 넘기기 때문에, 이 화면에서는 공통 오류 메시지와 실제 예외 정보를 함께 출력한다.// commonErrorPage.html <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Insert title here</title> </head> <body> <h1 style="color:red" th:text="${ msg }"></h1> <hr> <h3 th:text="${ exceptionInfo }"></h3> </body> </html>
th:text="${ msg }"는 공통 예외 처리 클래스에서 넘긴 오류 메시지를 출력한다.
th:text="${ exceptionInfo }"는 실제 발생한 예외 객체 정보를 출력한다.
즉, 이 화면은 단순히 “오류가 났다”만 보여 주는 것이 아니라, 공통 핸들러가 어떤 메시지와 어떤 예외 정보를 화면에 넘겼는지 확인하는 화면이다.
이 코드는RuntimeException계열 예외를 공통으로 처리한다.
IllegalStateException은RuntimeException의 하위 예외이다.
그래서ExceptionLocalController안에서IllegalStateException을 처리하는 지역 핸들러를 주석으로 막으면, 이 공통 핸들러가 대신 처리할 수 있다.
이 흐름에서 중요한 점은 지역 예외 처리와 공통 예외 처리의 관계이다.
특정 컨트롤러 안에 해당 예외를 처리하는@ExceptionHandler가 있으면 그 컨트롤러 내부에서 처리된다.
하지만 해당 예외를 처리할 지역 핸들러가 없거나 해당 예외를 처리하지 않으면@ControllerAdvice가 붙은 공통 예외 처리 클래스가 처리할 수 있다.
그래서 결과가 이렇게 나온다
첫 번째는
/exceptionTest처럼num을 아예 전달하지 않은 경우이다.
컨트롤러 메서드는int num을 필요로 한다.
그런데 요청에num이 없으면int에 넣을 값이 없다.
int는 기본형 타입이라null을 담을 수 없기 때문에 이 예제 실행 결과에서는IllegalStateException처리 핸들러가 실행된다.
이 예제에서는ExceptionLocalController안에@ExceptionHandler(IllegalStateException.class)가 있다.
그래서 지역 예외 처리 메서드인handleIllegalStateException()이 실행된다.
그 결과errorPage.html에num=숫자 형식의 쿼리를 전달하세요!!메시지가 출력된다.
콘솔에는
IllegalStateException을 처리하는 핸들러가 오류를 처리했다는 로그가 출력된다.
화면에는
errorPage.html이 열리고,ModelAndView에 담긴msg값이 출력된다.
즉,num이 없어서 예외가 발생했지만 서버 오류 화면이 아니라 준비한 오류 화면으로 응답한다.
두 번째는/exceptionTest?num=이언덕처럼num에 문자를 전달한 경우이다.
컨트롤러 메서드는int num을 기대한다.
그런데이언덕은 숫자로 바꿀 수 없는 문자열이다.
그래서Spring MVC가 요청값을int로 변환하는 과정에서MethodArgumentTypeMismatchException이 발생한다.
이 예외는@ExceptionHandler(MethodArgumentTypeMismatchException.class)가 붙은handleTypeMismatchException()에서 처리한다.
그 결과errorPage.html에타입을 맞춰주세용!!메시지가 출력된다.
화면에는 타입이 맞지 않는다는 메시지가 출력된다.
요청 주소의 형태는 맞지만,num에 들어온 값의 타입이 맞지 않기 때문이다.
콘솔에는
TypeMismatchException을 처리하는 핸들러가 오류를 처리했다는 로그가 출력된다.
이 흐름을 통해 요청 파라미터 타입 변환 실패도 지역 예외 처리 메서드로 잡을 수 있다는 점을 확인할 수 있다.
세 번째는/exceptionTest?num=10처럼 정상 숫자를 전달하고, 해당 친구 정보가 존재하는 경우이다.
num에는 숫자10이 들어간다.
FriendService의get(10)은FriendDTO객체를 만들어 반환한다.
컨트롤러는 반환받은FriendDTO를model.addAttribute("friend", vo)로 화면에 넘긴다.
그리고"friendView"를 반환하므로friendView.html이 열린다.
화면에는 친구 정보가 출력된다.
전화번호는010-1111-2222이고, 이름은Dooly이다.
이 경우는 예외가 발생하지 않았기 때문에@ExceptionHandler가 실행되지 않는다.
즉, 정상 요청 흐름 그대로 컨트롤러가 뷰를 반환한 결과이다.
네 번째는/exceptionTest?num=1처럼 숫자 형식은 맞지만 친구 정보가 없는 경우이다.
num은 숫자이므로 타입 변환 문제는 없다.
하지만FriendService의get(1)은 친구 정보를 만들지 않고null을 반환한다.
컨트롤러는vo == null을 확인하고 직접FriendNotFoundException을 발생시킨다.
이 예외는@ExceptionHandler(FriendNotFoundException.class)가 붙은handleNotFoundException()에서 처리한다.
그 결과"noFriend"가 반환되고noFriend.html이 열린다.
화면에는
친구가 없당!!이 출력된다.
이 결과는 요청값 자체가 잘못된 것이 아니라, 요청한 번호에 해당하는 데이터가 없어서 나온 결과이다.
콘솔에는
FriendNotFoundException을 처리하는 핸들러가 오류를 처리했다는 로그가 출력된다.
이 흐름을 통해 직접 만든 예외도@ExceptionHandler로 처리할 수 있다는 것을 확인할 수 있다.
다섯 번째는ExceptionLocalController안의IllegalStateException지역 핸들러를 주석으로 막은 뒤/exceptionTest를 요청한 경우이다.
원래는num이 없어서 발생한 예외를 컨트롤러 내부 핸들러가 처리했다.
하지만 그 핸들러를 막으면 컨트롤러 내부에는 해당 예외를 처리할 메서드가 없어진다.
이때IllegalStateException은RuntimeException의 하위 예외이므로,@ControllerAdvice가 붙은CommonExceptionHandler의@ExceptionHandler(RuntimeException.class)가 대신 처리한다.
그 결과 공통 오류 화면인commonErrorPage가 열린다.
화면에는
RuntimeException은 내가 다 잡는다!!!라는 메시지가 출력된다.
그리고 아래에는 실제 발생한 예외 정보가 함께 출력된다.
이 결과가 중요한 이유는 지역 예외 처리와 공통 예외 처리의 차이가 눈에 보이기 때문이다.
컨트롤러 안에 처리할 지역 핸들러가 있으면 지역 핸들러가 처리하고, 지역 핸들러가 없거나 해당 예외를 처리하지 않으면 공통 핸들러가 처리할 수 있다.
이 예제의 핵심을 다시 정리하면 이렇다.
num이 없으면 이 예제 실행 결과에서는IllegalStateException처리 핸들러가 실행된다.num에 문자가 들어오면MethodArgumentTypeMismatchException이 발생한다.num=10이면 정상적으로 친구 정보가 출력된다.num=1이면 직접 만든FriendNotFoundException이 발생한다.- 컨트롤러 내부
@ExceptionHandler는 해당 컨트롤러의 예외를 지역적으로 처리한다.@ControllerAdvice는 지역 핸들러가 없거나 해당 예외를 처리하지 않을 때 공통으로 처리할 수 있다.ModelAndView는 오류 화면 이름과 화면에 보여 줄 데이터를 함께 담을 때 사용한다.String반환은 화면 이름만 반환해도 충분할 때 사용한다.결국 이 응용예제는 오류 처리 개념을 실제 요청 결과로 확인하는 예제이다.
단순히 예외가 발생했다는 사실만 보는 것이 아니라, 어떤 예외가 발생했고, 그 예외를 지역 핸들러가 처리했는지 공통 핸들러가 처리했는지까지 구분해서 보는 것이 핵심이다.