Java | Spring

[Spring Boot] Bank App (3) Exception Handler 처리

@leem 2025. 2. 17. 14:43

1. @ControllerAdvice, @RestControllerAdvice란?

예외 처리의 필요성

웹 애플리케이션에서 예외(Exception)는 필연적으로 발생합니다. 하지만 이를 적절하게 관리하지 않으면 애플리케이션이 비정상적으로 종료되거나, 클라이언트가 원치 않는 에러 페이지를 보게 될 수 있습니다.

💡 Spring에서는 @ControllerAdvice와 @RestControllerAdvice를 사용하여 전역 예외 처리를 할 수 있습니다.

@ControllerAdvice 전통적인 웹 애플리케이션에서 사용되며, HTML 뷰를 반환하는 컨트롤러의 예외를 처리함
@RestControllerAdvice RESTful 웹 서비스에서 사용되며, JSON 또는 XML 응답을 반환하는 컨트롤러의 예외를 처리함

@ControllerAdvice 사용

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = ResourceNotFoundException.class)
    @ResponseBody
    public ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }
}
  1. @ControllerAdvice → 해당 클래스가 전역적으로 예외를 처리하는 역할을 함.
  2. @ExceptionHandler(ResourceNotFoundException.class) → 특정 예외 발생 시 실행될 메서드 지정.
  3. ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND) → 예외 메시지와 HTTP 상태 코드를 반환.
  4. @ResponseBody → 데이터 응답을 반환할 때 사용 (JSP 등 View 기반 프로젝트에서는 생략 가능).
 

@RestControllerAdvice 사용

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(value = ResourceNotFoundException.class)
    public ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex) {
        return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
    }
}

 

  • @RestControllerAdvice → @ControllerAdvice + @ResponseBody 역할을 수행하여 JSON 응답을 반환.
  • JSON 응답 예시
     
{
    "error": "Resource Not Found",
    "status": 404
}

 


2. 사용자 정의 예외 클래스 만들기

RuntimeException을 상속받아 인증 예외, 리다이렉트 예외, 데이터 예외를 정의할 수 있습니다.

1. UnAuthorizedException (인증 관련 예외)

package com.tenco.bank.handler.exception;

import org.springframework.http.HttpStatus;
import lombok.Getter;

@Getter
public class UnAuthorizedException extends RuntimeException {
    private HttpStatus status;

    public UnAuthorizedException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
}
  • 인증이 필요한 서비스에 비인증 사용자가 접근할 경우 발생하는 예외.
  • RuntimeException을 상속받아 필요 시 HttpStatus를 추가.

2. RedirectException (리다이렉트 예외)

package com.tenco.bank.handler.exception;

import org.springframework.http.HttpStatus;
import lombok.Getter;

@Getter
public class RedirectException extends RuntimeException {
    private HttpStatus status;

    public RedirectException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
}
  • 특정 상황에서 클라이언트를 다른 페이지로 리다이렉트해야 할 때 사용.
  • 예: 로그인 페이지로 이동, 잘못된 요청 시 에러 페이지로 이동.

3. DataDeliveryException (데이터 반환 예외)

package com.tenco.bank.handler.exception;

import org.springframework.http.HttpStatus;
import lombok.Getter;

@Getter
public class DataDeliveryException extends RuntimeException {
    private HttpStatus status;

    public DataDeliveryException(String message, HttpStatus status) {
        super(message);
        this.status = status;
    }
}

 

  • 데이터 응답 시 오류가 발생할 경우 클라이언트에게 직접 알림을 띄울 수 있도록 활용.
  • 예: "중복된 이메일입니다" 등의 메시지를 Alert 창으로 표시.

 


3. @ControllerAdvice 구현하기

package com.tenco.bank.handler;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;

import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;
import com.tenco.bank.handler.exception.UnAuthorizedException;

@ControllerAdvice
public class GlobalControllerAdvice {

    @ExceptionHandler(Exception.class)
    public void exception(Exception e) {
        System.out.println("-----------------------");
        System.out.println(e.getClass().getName());
        System.out.println(e.getMessage());
        System.out.println("-----------------------");
    }

    @ExceptionHandler(RedirectException.class)
    public ModelAndView redirectException(RedirectException e) {
        ModelAndView modelAndView = new ModelAndView("errorPage");
        modelAndView.addObject("statusCode", HttpStatus.NOT_FOUND.value());
        modelAndView.addObject("message", e.getMessage());
        return modelAndView;
    }
    
    @ResponseBody
    @ExceptionHandler(DataDeliveryException.class)
    public String dataDeliveryExceptionException(DataDeliveryException e) {
        return "<script>alert('" + e.getMessage() + "'); window.history.back();</script>";
    }
    
    @ResponseBody
    @ExceptionHandler(UnAuthorizedException.class)
    public String unAuthorizedException(UnAuthorizedException e) {
        return "<script>alert('" + e.getMessage() + "'); window.history.back();</script>";
    }
}

 

  • @ExceptionHandler(Exception.class)
    • 모든 예외를 처리하고 로그를 출력.
  • @ExceptionHandler(RedirectException.class)
    • 특정 예외 발생 시 에러 페이지(errorPage.jsp)로 이동.
  • @ExceptionHandler(DataDeliveryException.class)
    • JavaScript alert() 창을 띄우고 이전 페이지로 이동.
  • @ExceptionHandler(UnAuthorizedException.class)
    • 미인증 사용자가 접근할 경우, 경고 메시지 표시 후 뒤로 이동.

 


4. 에러 페이지 수정 (errorPage.jsp)

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>${statusCode} Error - Page Not Found</title>
  <link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
    }
    .error-message {
      text-align: center;
      margin-bottom: 20px;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="text-center">
      <h1 class="display-1">${statusCode}</h1>
      <p class="error-message">Page Not Found</p>
      <p>${message}</p>
      <a href="/" class="btn btn-primary">Go to Home Page</a>
    </div>
  </div>
</body>
</html>

5. 예외 발생 테스트 (MainController.java)

package com.tenco.bank.controller;

import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;

@Controller
public class MainController {

    @GetMapping({ "/main-page", "/index", "/" })
    public String mainPage() {
        return "mainPage";
    }
    
    @GetMapping("error-test1/{isError}")
    public String errorPage(@PathVariable boolean isError) {
        if (isError) {
            throw new RedirectException("잘못된 요청입니다.", HttpStatus.BAD_REQUEST);
        }
        return "mainPage";
    }
    
    @GetMapping("error-test2/{isError}")
    public String errorData2(@PathVariable boolean isError) {
        if (isError) {
            throw new DataDeliveryException("중복된 이메일을 사용할 수 없습니다", HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return "mainPage";
    }
    
    @GetMapping("error-test3/{isError}")
    public String errorData3(@PathVariable boolean isError) {
        if (isError) {
            throw new DataDeliveryException("로그인 먼저 해주세요", HttpStatus.UNAUTHORIZED);
        }
        return "mainPage";
    }
}

 

  • http://localhost:8080/error-test1/true → 에러 페이지 이동
  • http://localhost:8080/error-test2/true → 경고창 띄우기

6. HTTP 상태 코드

400 Bad Request 클라이언트 요청 오류
401 Unauthorized 인증 필요
403 Forbidden 접근 권한 없음
404 Not Found 리소스 없음
500 Internal Server Error 서버 내부 오류
502 Bad Gateway 게이트웨이 오류
503 Service Unavailable 서버 과부하 또는 유지보수

이제 Spring 프로젝트에서 전역 예외 처리(@ControllerAdvice)를 적용하여 유지보수하기 쉽고 일관된 예외 처리를 할 수 있습니다