Spring Framework/Spring Boot

[Spring Boot] 5. Exception Handler

hyomee2 2024. 9. 9. 21:41

디렉토리 구조 및 공통 클래스

package org.example.exceptionhandler;

@SpringBootApplication
public class Chap04ExceptionHandlerApplication {

    public static void main(String[] args) {
        SpringApplication.run(Chap04ExceptionHandlerApplication.class, args);
    }
}
package org.example.exceptionhandler;

@Controller
public class MainController {

    @GetMapping(value = {"/", "/main"})
    public String main() { return "main"; }
}

Exception Handler

프로그램이 실행 중에 예기치 않은 오류가 발생했을 때 이를 처리하는 메커니즘으로,

Spring MVC에서는 예외 처리를 관리하는 기능을 여러가지 제공하고 있다.

1. HandlerExceptionResolver

Spring에서 예외를 처리하는 방법 중 하나로,

DispatcherServlet이 예외가 발생했을 때, HandlerExceptionResolver를 이용해 예외를 처리할 수 있다.

(1) 종류

1) SimpleMappingExceptionResolver: 예외 타입 별로 예외 처리 페이지를 매핑해준다.

2) DefaultHandlerExceptionResolver: 기본 예외 처리 방식을 제공한다.

3) ResponseStatusExceptionResolver: 예외 발생 시 HTTP 상태 코드를 반환한다.

4) ExceptionHandlerExceptionResolver: @ExceptionHandler 어노테이션을 이용해서 Controller 내부에서 예외를 처리할 수 있다.


Exception Handler 테스트

1. @ExceptionHandler

스프링 MVC에서 예외 처리를 담당하는 어노테이션으로,

Controller 클래스 내에서 예외가 발생했을 때 해당 예외를 처리할 메서드를 정의하는데 사용된다.

 

코드를 통해 알아보자.

 

(1) 기본 에러 화면

클라이언트에서 아래와 같은 버튼 클릭 이벤트를 통해 요청한다.

<!--/main/resources/templates/main.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>main</title>
</head>
<body>
    <h1>Exception Handler 사용하기</h1>

    <h2>@ExceptionHandler 어노테이션 사용하기</h2>
    <button onclick="location.href='controller-null'">NullPointerException 테스트</button>
    <button onclick="location.href='controller-user'">사용자 정의 Exception 테스트</button>
</body>
</html>

 

Controller의 핸들러 메소드에

의도적으로 NullPointerException과 사용자 정의 Exception인 MemberRegistException을 발생시키는 코드를 작성한다.

package org.example.exceptionhandler;

@Controller
public class ExceptionHandlerController {

    @GetMapping("/controller-null")
    public String nullPointerExceptionTest() {
        String str = null;
        System.out.println(str.charAt(0));  // 의도적으로 NullPointerException 발생 시키는 코드
        return "/";
    }

    @GetMapping("/controller-user")
    public String userExceptionTest() throws MemberRegistException {
        boolean check = true;
        if(check) throw new MemberRegistException("당신 같은 사람은 회원으로 받을 수 없습니다.");

        return "/";
    }
}

 

Exception Handling이 되어 있지 않아 기본 화면으로 오류 화면이 응답된다.

 

(2) Controller 레벨의 Exception 처리

ExceptionHandlerController(동일 클래스)에

아래와 같은 @ExceptionHandler 어노테이션이 붙은 메소드를 정의하고 응답할 뷰도 작성한다.

package org.example.exceptionhandler;

@Controller
public class ExceptionHandlerController {

    // 위에서 작성한 코드 생략

    @ExceptionHandler(NullPointerException.class)
    public String nullPointerExceptionHandler(NullPointerException e) {

        System.out.println("특정 Controller 범위의 Exception Handler 동작");
        System.out.println("message : " + e.getMessage());

        return "error/nullPointer";
    }

    @ExceptionHandler(MemberRegistException.class)
    public String memberRegistExceptionHandler(MemberRegistException e, Model model) {

        System.out.println("특정 Controller 범위의 Exception Handler 동작");
        model.addAttribute("exception", e);

        return "error/memberRegist";

    }
}
<!--main/resources/templates/error/nullPointer.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>nullPointer</title>
</head>
<body>
    <h1>널 위하여 NullPointerException 발생!</h1>
</body>
</html>
<!--/main/resources/templates/error/memberRegist.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>memberRegist</title>
</head>
<body>
    <h1 th:text="${exception.message}"></h1>
</body>
</html>

해당 Controller 범위에서 해당하는 Exception이 발생하면 그 Exception이 설정된 메소드가 실행된다.

이와 같이 @ExceptionHandler 어노테이션을 통해 Controller의 메소드에서 throw된 Exception에 대한 처리가 가능하다.

 

2. @ControllerAdvice

Spring에서 예외 처리를 담당하는 어노테이션으로, @ControllerAdvice 어노테이션이 붙은 클래스는 전역 예외 처리를 담당한다.

즉, 여러 개의 Controller에서 발생하는 예외를 일괄적으로 처리하기 위해 사용한다.

이 어노테이션을 사용하면 모든 Controller에서 발생하는 예외를 한 곳에서 처리할 수 있어 코드의 중복을 방지할 수 있다. 

하지만 그와 동시에, Controller에서 발생하는 모든 예외를 처리하게 되므로

예외 처리에 대한 로직이 많아지면 클래스의 크기가 커지기 때문에

이를 방지하기 위해 역할에 따라 클래스를 나눠야 한다.

 

코드로 알아보자.

(1) 기본 에러 화면

클라이언트에서 다음과 같은 버튼 클릭 이벤트를 통해 요청한다.

<!--/main/resources/templates/main.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>main</title>
</head>
<body>

    <!--앞에서 작성한 코드 생략-->

    <h1>@ControllerAdvice 어노테이션 사용하기(전역)</h1>
    <button onclick="location.href='other-controller-null'">NullPointerException 테스트</button>
    <button onclick="location.href='other-controller-user'">사용자 정의 Exception 테스트</button>
    <button onclick="location.href='other-controller-array'">ArrayException 테스트</button>

</body>
</html>
package org.example.exceptionhandler;

@Controller
public class OtherController {

    @GetMapping("/other-controller-null")
    public String nullPointerExceptionTest() {
        String str = null;
        System.out.println(str.charAt(0));  // 의도적으로 NullPointerException 발생 시키는 코드
        return "/";
    }

    @GetMapping("/other-controller-user")
    public String userExceptionTest() throws MemberRegistException {
        boolean check = true;
        if(check) throw new MemberRegistException("당신 같은 사람은 회원으로 받을 수 없습니다.");

        return "/";
    }
}

이전의 Controller 클래스에 작성했던 @ExceptionHandler 어노테이션이 붙은 메소드는 동작하지 않으므로 

기본 에러 응답 화면이 나온다.

 

(2) Global 레벨의 Exception 처리

별도의 클래스 GlobalExceptionHandler @ControllerAdvice 어노테이션을 붙이고

@ExceptionHandler 어노테이션을 붙인 메소드를 작성한다.

package org.example.exceptionhandler;

/* @ControllerAdvice : 해당 어노테이션이 적용 된 클래스의 @ExceptionHandler는 전역적으로 기능한다. */
@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(NullPointerException.class)
    public String nullPointerExceptionHandler(NullPointerException e) {

        System.out.println("전역 범위의 Exception Handler 동작");
        System.out.println("message : " + e.getMessage());

        return "error/nullPointer";
    }

    @ExceptionHandler(MemberRegistException.class)
    public String memberRegistExceptionHandler(MemberRegistException e, Model model) {

        System.out.println("전역 범위의 Exception Handler 동작");
        model.addAttribute("exception", e);

        return "error/memberRegist";

    }
}

실행을 해보면,

Controller 클래스 내에 Exception Handler 설정이 없을 때

@ControllerAdvice 클래스에서 전역으로 설정된 Exception Handler가 동작하는 것을 확인할 수 있다.

(3) 상위 타입으로 default 처리

클라이언트에서 다음과 같은 버튼 클릭 이벤트를 통해 요청한다.

<!--/main/resources/templates/main.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>main</title>
</head>
<body>

    <!--위에서 작성한 코드 생략-->

    <button onclick="location.href='other-controller-array'">ArrayException 테스트</button>

</body>
</html>

 

Controller의 핸들러 메소드에 의도적으로 ArrayIndexOutOfBoundException을 발생시키는 코드를 작성한다.

package org.example.exceptionhandler;

@Controller
public class OtherController {

    // 위에서 작성한 코드 생략

    @GetMapping("/other-controller-array")
    public String arrayExceptionTest()  {
        double[] array = new double[0];
        System.out.println(array[0]);

        return "/";
    }
}

 

그리고 @ControllerAdvice가 선언된 클래스에 Exception 타입을 트랩하는 Exception Handler method를 작성한다.

package org.example.exceptionhandler;

/* @ControllerAdvice : 해당 어노테이션이 적용 된 클래스의 @ExceptionHandler 는 전역적으로 기능한다. */
@ControllerAdvice
public class GlobalExceptionHandler {

    /* 상위 타입의 Exception을 통해 Handler를 작성하면 하위 타입의 모든 Exception을 처리할 수 있다. */
    @ExceptionHandler(Exception.class)
    public String defaultExceptionHandler(Exception e) {
        return "error/default";
    }
}

 

error view는 아래와 같이 작성했다.

<!--/main/resources/templates/error/default.html-->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>default</title>
</head>
<body>
    <h1>공통 에러 처리 화면</h1>
</body>
</html>

실행을 해보면,

상위 타입인 Exception을 이용하면

구체적으로 작성하지 않은 타입의 에러가 발생해도 처리가 가능하므로 default 처리 용도로 사용할 수 있다.

'Spring Framework > Spring Boot' 카테고리의 다른 글

[Spring Boot] 7. File Upload  (0) 2024.09.11
[Spring Boot] 6. Interceptor  (1) 2024.09.10
[Spring Boot] 4. View Resolver  (1) 2024.09.09
[Spring Boot] 3. Handler Method  (2) 2024.09.09
[Spring Boot] 2. Request Mapping  (0) 2024.09.08