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);
}
}
- @ControllerAdvice → 해당 클래스가 전역적으로 예외를 처리하는 역할을 함.
- @ExceptionHandler(ResourceNotFoundException.class) → 특정 예외 발생 시 실행될 메서드 지정.
- ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND) → 예외 메시지와 HTTP 상태 코드를 반환.
- @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)를 적용하여 유지보수하기 쉽고 일관된 예외 처리를 할 수 있습니다