[Spring boot] Bank App 만들어 보기 - 12. Exception Handler 처리(HTTP 상태 코드가 뭘까?)
💡 학습 목표
1. @ControllerAdvice, @RestControllerAdvice 는 뭘까?
2. @ControllerAdivce 와 @RestControllerAdivce 에 차이점을 이해 하자.
3. 사용자 정의 예외 클래스를 만들기
4. @ControllerAdvice 구현해 보기
5. 에러 페이지 코드 수정 (errorPage.jsp)
6. 직접 예외 발생해보기
1. @ControllerAdvice, @RestControllerAdvice 는 뭘까?
HTTP 통신을 통해 예외 상황을 클라이언트에게 알려주는 방법은 여러 가지가 있으며, 이들을 적절히 사용하는 것이 중요합니다.
**@ControllerAdvice**와 **@RestControllerAdvice**는 Spring Framework에서 제공하는 어노테이션들로, 애플리케이션 전역에 걸쳐 발생하는 예외를 효과적으로 관리하고 처리하는 데 사용됩니다.
이들은 일종의 "예외 처리의 중앙 집중화"를 가능하게 해주며, 애플리케이션 내 여러 컨트롤러나 서비스에서 공통적으로 발생할 수 있는 예외를 한 곳에서 관리할 수 있게 해줍니다.
2. @ControllerAdivce 와 @RestControllerAdivce 에 차이점을 이해 하자.
@ControllerAdvice와 @RestControllerAdvice 차이점
- @ControllerAdvice: 이 어노테이션은 주로 @Controller 또는 @RequestMapping 어노테이션이 적용된 클래스(즉, MVC 컨트롤러)에서 발생하는 예외를 처리하기 위해 사용됩니다. HTML 뷰를 반환하는 전통적인 웹 애플리케이션에서 주로 사용됩니다.
- @RestControllerAdvice: **@ControllerAdvice**와 유사한 기능을 제공하지만, **@RestController**에서 발생하는 예외를 처리하는 데 특화되어 있습니다. 즉, RESTful 웹 서비스에서 JSON이나 XML 같은 응답을 반환할 때 사용됩니다. 사실상, **@RestControllerAdvice**는 **@ControllerAdvice**에 **@ResponseBody**를 추가한 것과 동일한 효과를 제공하여, 응답 본문에 직접 데이터를 매핑할 수 있습니다.
예시 코드 확인 - 1
ControllerAdvice 에서 데이터를 반환
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = ResourceNotFoundException.class)
@ResponseBody
public ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex) {
// 에러 메시지와 함께 404 상태 코드를 반환
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
예시 코드 확인 - 2
RestControllerAdvice 에서 반환
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(value = ResourceNotFoundException.class)
// @ResponseBody 사용 안해도 됨(데이터를 반환 처리 함)
public ResponseEntity<Object> handleResourceNotFoundException(ResourceNotFoundException ex) {
// 에러 메시지와 함께 404 상태 코드를 반환
return new ResponseEntity<>(ex.getMessage(), HttpStatus.NOT_FOUND);
}
}
3. 사용자 정의 예외 클래스를 만들기
RuntimeException은 프로그램의 실행 도중 발생하는 예외를 나타냅니다. 컴파일 시 예외가 아닌 실행 시 예외로 분류되며, 따로 try-catch 블록으로 처리하지 않아도 컴파일러가 오류로 인식하지 않습니다.
예시로는 NullPointerException, ArrayIndexOutOfBoundsException, IllegalArgumentException 등이 있습니다. 여기서는 RuntimeException 확장해서 사용자 정의 예외 클래스를 만들어 봅니다.
위 그림처럼 패키지와 자바 파일을 만들어 주세요 (handler/GlobalControllerAdivce.java 파일 생성)
- handler/GlobalControllerAdivce.java 파일 생성
- handler/exception 패키지 생성
- handler/exception/UnAuthorizedException 자바 파일 생성
: 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;
}
}
: 인증관련 사용자 정의 예외 클래스 생성
RedirectException 사용자 정의 예외 클래스 생성 - 에러 페이지로 이동 시킬 때 사용할 예정
package com.tenco.bank.handler.exception;
import org.springframework.http.HttpStatus;
import lombok.Getter;
// 404.. 에러 페이지동 이동 시킬 때 사용하는 예외 클래스
@Getter
public class RedirectException extends RuntimeException {
private HttpStatus status;
public RedirectException(String message, HttpStatus status) {
super(message);
this.status = status;
}
}
DataDeliveryException 사용자 정의 예외 클래스 생성 - Data로 예외를 내려 줄 때 사용할 예정
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;
}
}
4. @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;
/**
* View 렌더링을 위해 ModelView 객체를 반환 하도록 설정되어 있음 예외처리 Page를 Return 할 때 사용
*
* @ResponseBody 를 붙이면 Data를 내려 줄 수 있음
*/
@ControllerAdvice
public class GlobalControllerAdvice {
/**
* 모든 예외 클래스를 알 수 없기 때문에 로깅으로 확인 할 수 있도록 설정
* 참고 동기적 방식에 로깅은 지양(@slf4j 권장)
*/
@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("-----------------------");
}
/**
* @return 에러 페이지로 이동 처리
*/
@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; // 페이지 반환 + 데이터 내려줌
}
/**
* Data 로 예외를 내려 주기 위해
* ResponseBody 활용
* 브라우저에서 스크립트 코드로 동작
*/
@ResponseBody
@ExceptionHandler(DataDeliveryException.class)
public String dataDeliveryExceptionException(DataDeliveryException e) {
StringBuffer sb = new StringBuffer();
sb.append("<script>");
sb.append("alert('" + e.getMessage() + "');");
sb.append("window.history.back();");
sb.append("</script>");
return sb.toString();
}
@ResponseBody
@ExceptionHandler(UnAuthorizedException.class)
public String unAuthorizedException(UnAuthorizedException e) {
StringBuffer sb = new StringBuffer();
sb.append("<script>");
sb.append("alert('" + e.getMessage() + "');");
// Todo - 로그인 페이지로 수정 예정
sb.append("window.history.back();");
sb.append("</script>");
return sb.toString();
}
}
5. 에러 페이지 코드 수정 (errorPage.jsp)
샘플 화면
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>${statusCode} Error - Page Not Found</title>
<!-- 부트스트랩 4 CSS -->
<link href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css" rel="stylesheet">
<!-- 커스텀 CSS -->
<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>
6. 직업 예외 발생해보기
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 // IoC 대상(싱글톤으로 관리 됨)
public class MainController {
// 주소 설계 하기
// http:localhost:8080/main-page
@GetMapping({ "/main-page", "/index", "/" })
public String mainPage() {
System.out.println("동기적 방식으로 동작 --> mainPage() 호출 확인");
// [[ JSP 파일 찾기 - yml 파일 설정 부분 ]]
// prefix: /WEB-INF/view/
// mainPage <- (String 값으로 메서드에 직접 작성 하는 방식)
// suffix: .jsp
// /WEB-INF/view/mainPage.jsp - 완성된 문자열
return "mainPage";
}
// todo - 삭제 예정
// 주소 설계
// http:localhost:8080/error-test1/true
@GetMapping("error-test1/{isError}")
public String errorPage(@PathVariable boolean isError) {
if(isError) {
throw new RedirectException("잘못된 요청 입니다.", HttpStatus.BAD_REQUEST);
}
return "mainPage";
}
// todo - 삭제 예정
// 주소 설계
// http:localhost:8080/error-test2/true
@GetMapping("error-test2/{isError}")
public String errorData2(@PathVariable boolean isError) {
if(isError) {
throw new DataDeliveryException("중복된 이메일을 사용할 수 없습니다", HttpStatus.INTERNAL_SERVER_ERROR);
}
return "mainPage";
}
// todo - 삭제 예정
// 주소 설계
// http:localhost:8080/error-test3/true
@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
http:localhost:8080/error-test3/true
💡 HTTP 상태 코드란?
HTTP(Hypertext Transfer Protocol)는 웹 서버와 웹 클라이언트 사이에서 데이터를 주고받기 위해 사용하는 통신 방식으로 TCP/IP 프로토콜 위에서 동작합니다. 즉, 우리가 웹을 이용하려면 웹 서버와 웹 클라이언트는 각각 TCP/IP 동작에 필수적인 IP 주소를 가져야 한다는 의미입니다.
HTTP란 이름대로라면 하이퍼텍스트(Hypertext)만 전송할 수 있어 보이지만, 실제로는 HTML이나 XML과 같은 하이퍼텍스트뿐 아니라 이미지, 음성, 동영상, Javascript, PDF와 각종 문서 파일 등 컴퓨터에서 다룰 수 있는 데이터라면 무엇이든 전송할 수 있습니다.
우리가 웹 브라우저의 주소창에 https://www.naver.com을 입력하고 Enter 를 누르면 웹 클라이언트와 웹 서버 사이에 HTTP 연결이 맺어지고 웹 클라이언트는 웹 서버에 HTTP 요청 메시지를 보냅니다. 웹 서버는 요청에 따른 처리를 진행한 후에 그 결과를 웹 클라이언트에 HTTP 응답 메시지로 보냅니다.
100~500번 대까지 상태 코드 정의
- 1XX: Informational(정보 제공)
- 임시 응답으로 현재 클라이언트의 요청까지는 처리되었으니 계속 진행하라는 의미입니다. HTTP 1.1 버전부터 추가되었습니다.
- 2XX: Success(성공)
- 클라이언트의 요청이 서버에서 성공적으로 처리되었다는 의미입니다.
- 3XX: Redirection(리다이렉션)
- 완전한 처리를 위해서 추가 동작이 필요한 경우입니다. 주로 서버의 주소 또는 요청한 URI의 웹 문서가 이동되었으니 그 주소로 다시 시도하라는 의미입니다.
- 4XX: Client Error(클라이언트 에러)
- 없는 페이지를 요청하는 등 클라이언트의 요청 메시지 내용이 잘못된 경우를 의미합니다.
- 5XX: Server Error(서버 에러)
- 서버 사정으로 메시지 처리에 문제가 발생한 경우입니다. 서버의 부하, DB 처리 과정 오류, 서버에서 익셉션이 발생하는 경우를 의미합니다.
주요 4xx 상태코드 확인 (클라이언트 에러) 클라이언트의 요청에 오류가 있다.
상태 코드 | 상태 텍스트 | 한국어 | 서버 측면에서의 의미 |
400 | Bad Request | 잘못된 요청 | 요청 구문, 유효하지 않은 요청 메시지 또는 디코딩할 수 없는 내용으로 인해 서버가 요청을 이해할 수 없음. |
401 | Unauthorized | 권한 없음 | 요청이 인증을 필요로 하며, 클라이언트가 해당 인증을 제공하지 않았음. WWW-Authenticate 헤더를 통해 인증 방식을 지정. |
403 | Forbidden | 금지됨 | 서버가 요청을 이해했으나, 권한 없음 또는 기타 사유로 인해 요청을 수행 거부. |
404 | Not Found | 찾을 수 없음 | 서버가 요청한 리소스를 찾을 수 없음. 주소 오타 또는 리소스 이동/삭제 등이 원인일 수 있음. |
주요 5xx 상태코드 확인 (서버 에러) - 클라이언트의 요청은 유효한데 서버가 처리에 실패하였다.
상태 코드 | 상태 텍스트 | 한국어 | 서버 측면에서의 의미 |
500 | Internal Server Error | 내부 서버 오류 | 서버 내부에 오류가 발생하여 요청을 처리할 수 없음. 일반적으로 서버 측에서 예상치 못한 상황이 발생했을 때 반환됨. |
501 | Not Implemented | 구현되지 않음 | 서버가 요청된 메소드를 지원하지 않거나 구현하지 않음. 예를 들어, 서버가 PUT 메소드를 지원하지 않는 경우에 반환됨. |
502 | Bad Gateway | 불량 게이트웨이 | 게이트웨이나 프록시 역할을 하는 서버가 뒤쪽 서버로부터 잘못된 응답을 받았음. 대개 다른 서버로의 요청 중 문제가 발생했을 때 사용됨. |
503 | Service Unavailable | 서비스 제공불가 | 서버가 일시적으로 요청을 처리할 수 없는 상태임. 일반적으로 서버 과부하나 유지 보수로 인해 일시적으로 서비스를 제공할 수 없을 때 반환됨. |
package com.tenco.bank.handler.exception;
// 사용자 정의 예외 클래스 만들기
import org.springframework.http.HttpStatus;
public class DataDeliveryException extends RuntimeException {
private HttpStatus status;
// 예외 발생했을 때 --> Http 상태코드를 알려 준다.
// 메세지 (어떤 예외 발생)
public DataDeliveryException(String message, HttpStatus status) {
super(message);
this.status = status;
}
}
package com.tenco.bank.handler.exception;
// 사용자 정의 예외 클래스 만들기
import lombok.Data;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
public class RedirectException extends RuntimeException {
private HttpStatus status;
// 예외 발생했을 때 --> Http 상태코드를 알려 준다.
// 메세지 (어떤 예외 발생)
public RedirectException(String message, HttpStatus status) {
super(message);
this.status = status;
}
}
package com.tenco.bank.handler.exception;
// 사용자 정의 예외 클래스 만들기
import org.springframework.http.HttpStatus;
public class UnAuthorizedException extends RuntimeException {
private HttpStatus status;
// 예외 발생했을 때 --> Http 상태코드를 알려 준다.
// 메세지 (어떤 예외 발생)
public UnAuthorizedException(String message, HttpStatus status) {
super(message);
this.status = status;
}
}
package com.tenco.bank.handler;
import com.tenco.bank.handler.exception.DataDeliveryException;
import com.tenco.bank.handler.exception.RedirectException;
import com.tenco.bank.handler.exception.UnAuthorizedException;
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.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;
// 중앙에서 관리할 예외처리 핸들러 만들어 보기
@ControllerAdvice // Ioc 대상(싱글톤) - HTML 렌더링시 예외에서 많이 사용
// @RestControllerAdvice RESET API 활용이 많이 됨.
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("----------------");
}
// 데이터로 클라이언트에게 전달
@ResponseBody // 뷰 리졸버 안탐
@ExceptionHandler(DataDeliveryException.class)
public String dataDeliveryException(DataDeliveryException e) {
StringBuffer sb = new StringBuffer();
sb.append(" <script>");
sb.append(" alert(' "+ e.getMessage() + " ' );");
sb.append(" window.history.back();");
sb.append(" </script>");
return sb.toString();
}
@ResponseBody // 뷰 리졸버 안탐
@ExceptionHandler(UnAuthorizedException.class)
public String unAuthorizedException(UnAuthorizedException e) {
StringBuffer sb = new StringBuffer();
sb.append(" <script>");
sb.append(" alert(' "+ e.getMessage() + " ' );");
sb.append(" window.history.back();");
sb.append(" </script>");
return sb.toString();
}
/**
* 에러 페이지로 이동 처리 하기
* JSP로 이동시 데이터를 담아서 보내는 방법
*/
@ExceptionHandler(RedirectException.class)
public ModelAndView redirectException(RedirectException e) {
ModelAndView modelAndView = new ModelAndView("/errorPage");
modelAndView.addObject("statusCode", e.getStatus().value());
modelAndView.addObject("message", e.getMessage());
return modelAndView;
}
}