Framework/Spring

[Annotation]@RestControllerAdivce / 스프링 예외 처리

SHXL2 2021. 2. 6. 12:38
반응형

스프링에서는 예외 처리를 전역으로 할 수 있는 기능을 제공합니다.

오늘 포스팅할 내용은 스프링 어노테이션 중 하나인 @RestContollerAdvice를 이용한 예외 처리에 관한 것입니다.

 

※ 테스트 환경
  • JDK 1.8
  • Spring 4.3.14
  • Tomcat 9.0

@RestControllerAdvice

@RestControllerAdvice는 @ControllerAdvice와 @ResponseBody가 더해진 어노테이션이다.

@ControllerAdvice는 예외 처리를 View로 응답하는 경우 사용할 수 있고 REST 요청에 대한 처리가 필요한 경우(일반적으로 JSON 형식의 데이터) @ResponseBody가 더해진 @RestControllerAdvice를 사용하면 된다.

 

예외를 잡아내기 위해서 @RestControllerAdivce 내에 @ExceptionHandler라는 어노테이션을 선언한 메소드를 사용한다. @ExceptionHandler@Controller, @RestController가 적용된 Bean에서 발생한 예외를 잡아 하나의 메소드에서 처리하는 역할을 한다. @Service에서의 예외는 잡지 못한다.

 

이제 테스트를 해보자.

두 가지 상황을 테스트해봤다. 첫 번째는 Get 방식의 Model을 리턴하고 두 번째는 Post 방식의 JSON 형식의 데이터를 리턴하는 상황이다.

 

1. Get 방식

테스트 페이지를 하나 만들고 Y 입력시에 예외가 발생하도록 했다.

 

ex01.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<title>Exception 예제 01. @Restcontrolleradvice(Get방식)</title>
<%@ include file="/WEB-INF/views/include/source.jsp" %>
<script>
function sendMsg() {
	var msg = $("#inputMsg").val();
	
	location.href="/exception/sqlExceptionGet.do?msg=" + msg;
}

$(document).ready(function() {
	
});
</script>
</head>
<body>
<form id="testForm">
	<div class="container">
		<div class="row border-bottom mb-3">
			<div class="col-12">
				<p class="h1">EX01. @RestControllerAdvice - Get</p>
			</div>
		</div>
		
		<div class="row">
			<div class="col-4">
				<div class="row">			
					<div class="col-2">
		  				<label for="inputLabel" class="visually-hidden"></label>
		  				<input type="text" readonly class="form-control-plaintext" id=inputLabel value="메세지">
					</div>
					<div class="col-auto">
		  				<label for="inputMsg" class="visually-hidden"></label>
		  				<input type="text" class="form-control" id="inputMsg" placeholder="Y 입력시 예외 발생">
					</div>
					<div class="col-auto">
		  				<button type="button" class="btn btn-primary mb-3" onclick="sendMsg()">전송</button>
					</div>
				</div>
			</div>
		</div>		
	</div>
	
	<%@ include file="/WEB-INF/views/common/bottom.jsp" %>
</form>
</body>
</html>

 

먼저 공통으로 예외를 핸들링할 클래스 CusomExceptionHandler 클래스를 만들었다.

@RestControllerAdvice(annotations = ExceptionController.class)

annotations 속성에 예외 처리를 하고 싶은 클래스를 따로 지정할 수 있는데 지정하지 않는다면 전역적으로 작동된다.

 

CustomExceptionHandler.java

package com.study.exception;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.json.MappingJackson2JsonView;

@RestControllerAdvice
public class CustomExceptionHandler {

	private static final Logger logger = LoggerFactory.getLogger(CustomExceptionHandler.class);

	@ExceptionHandler(CustomSqlException.class)
	private ModelAndView customSqlException(HttpServletRequest request, CustomSqlException ex) {				
		String msg = ex.getMessage();
		
		logger.error("### CustomSqlException");
		logger.error("### 메세지 : " + msg);				
				
		if(isAjaxRequest(request)) {
			return createJsonView(msg); 
		}
		
		ModelAndView mav = new ModelAndView();		
		
		mav.addObject("msg", msg);
		mav.setViewName("/common/message");
		
		return mav;	
	}
	
	private boolean isAjaxRequest(HttpServletRequest request) {		
		String header = request.getHeader("x-requested-with");		
		
		logger.info("### Request Header : " + request.getHeader("x-requested-with"));
		
		if("XMLHttpRequest".equals(header)) { 
			return true;
		}
		
		return false;
	}
	
	private ModelAndView createJsonView(String msg) { 
		ModelAndView mav = new ModelAndView("jsonView"); 
		
		mav.addObject("msg", msg);
		
		return mav; 
	}
	
	@Bean 
	public MappingJackson2JsonView jsonView() {
		return new MappingJackson2JsonView(); 
	}
}

CustomSqlException 예외가 발생하는 시점에 @ExceptionHandler가 등록된 메소드가 작동한다.

 

25번 라인에 isAjaxRequest 메소드는 두 번째 상황에서 Post 방식 ajax를 사용할 계획이었기에 요청이 ajax인지 구분하기 위해 만들었다.

ajax 요청시에는 Request 헤더에 "XMLHttpRequest" 값이 들어오는 것을 이용했다. 그리고 Model에 저장된 객체를 JSON 형식으로 변환해주는 MappingJackson2JsonView 클래스를 사용해 JSON으로 리턴하게 된다.(CreateJsonView)

 

MappingJackson2JsonView 클래스를 사용하기 위해서는 Maven에 Jackson 라이브러리가 등록되어야 하고 Bean을 생성해야 한다.

 

pom.xml

<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-core</artifactId>
	<version>2.8.8</version>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-annotations</artifactId>
	<version>2.8.8</version>
</dependency>
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.8.8</version>
</dependency>

 

servlet-context.xml

<beans:bean class="org.springframework.web.servlet.view.json.MappingJackson2JsonView" id="jsonView">
	<beans:property name="contentType" value="application/json;charset=UTF-8"/>
</beans:bean>

 

예외 발생시 원하는 메세지를 받을 수 있게 CustomSqlException이라는 클래스를 만들었다.

 

CustomSqlException.java

package com.study.exception;

@SuppressWarnings("serial")
public class CustomSqlException extends Exception {
	
	public CustomSqlException(String msg) {
        super(msg);
    }
}

super함수를 통해 msg를 전달하면 Exception 클래스의 내부 생성자를 통해 입력받은 메세지로 새로운 예외를 생성하게 된다. Exception 클래스는 Throwable 클래스를 상속받는다.

Exception.class

    /**
     * Constructs a new exception with the specified detail message.  The
     * cause is not initialized, and may subsequently be initialized by
     * a call to {@link #initCause}.
     *
     * @param   message   the detail message. The detail message is saved for
     *          later retrieval by the {@link #getMessage()} method.
     */
    public Exception(String message) {
        super(message);
    }

 

그리고 컨트롤러에서 Y를 입력받을 때 "예외 발생"이라는 메세지를 보내고 예외를 발생시켰다.

 

ExceptionController.java

@RequestMapping(value = "/exception/sqlExceptionGet.do", method = { RequestMethod.GET })
public ModelAndView sqlExceptionGet(HttpServletRequest reqest) throws Exception {	
	ModelAndView mav = new ModelAndView();				
	
	String msg = reqest.getParameter("msg");
		
	if("Y".equals(msg)) {
		throw new CustomSqlException("예외 발생");
	} else {
		msg = "통과";
	}
	
	mav.addObject("msg", msg);
	mav.setViewName("/common/message");
	
	return mav;	
}

 

[결과]

(좌) N 입력 / (우) Y 입력

 

2. Post 방식

Post 방식도 Y 입력시 예외가 발생하도록 했다. Get 방식과 다른 것은 ajax 비동기 방식이다.

 

ex02.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<!DOCTYPE html>
<html>
<head>
<title>Exception 예제 02 : @Restcontrolleradvice(Post방식)</title>
<%@ include file="/WEB-INF/views/include/source.jsp" %>
<script>
function sendMsg() {
	var params = {
		msg : $("#inputMsg").val()
	};

	$.ajax({        	
        type       : "POST",    
        async      : false,
        url        : "/exception/sqlExceptionPost.do",
        cache      : false,  
        dataType   : "json",
        data       : JSON.stringify(params),            
        contentType: "application/json; charset=utf-8",
        success: function(data) {
        	alert(data.msg);
        },	
		error : function(request, status, error) {
			alert("code : " + request.status + "\n" + "message : " + request.responseText + "\n" + "error : " + error);
		}
	});
		
}

$(document).ready(function() {
	
});
</script>
</head>
<body>
<form id="testForm">
	<div class="container">
		<div class="row border-bottom mb-3">
			<div class="col-12">
				<p class="h1">EX02. @RestControllerAdvice - Post</p>
			</div>
		</div>
		
		<div class="row">
			<div class="col-4">
				<div class="row">			
					<div class="col-2">
		  				<label for="inputLabel" class="visually-hidden"></label>
		  				<input type="text" readonly class="form-control-plaintext" id=inputLabel value="메세지">
					</div>
					<div class="col-auto">
		  				<label for="inputMsg" class="visually-hidden"></label>
		  				<input type="text" class="form-control" id="inputMsg" placeholder="Y 입력시 예외 발생">
					</div>
					<div class="col-auto">
		  				<button type="button" class="btn btn-primary mb-3" onclick="sendMsg()">전송</button>
					</div>
				</div>
			</div>
		</div>		
	</div>
	
	<%@ include file="/WEB-INF/views/common/bottom.jsp" %>
</form>
</body>
</html>

 

ExceptionController.java

@RequestMapping(value = "/exception/sqlExceptionPost.do", method = { RequestMethod.POST })
public @ResponseBody Map<String, Object> sqlExceptionPost(HttpServletRequest reqest, @RequestBody String jsonParam) throws Exception {
		JSONObject result = new JSONObject(); 
		
		try {
			JSONObject json = (JSONObject) JSONValue.parse(jsonParam);
			
			String msg = (String) json.get("msg");
			
			if("Y".equals(msg)) {
				throw new Exception();
			} else {
				result.put("msg", "통과");			
			}						
		} catch (Exception e) {
			// TODO: handle exception
			throw new CustomSqlException("예외 발생");
		}				
		
		return result;	
}

11번 라인에서 Y 데이터를 받으면 예외를 발생시켰다.

디버깅을 걸어 순서를 따라가보면 예외가 발생하고 catch 문으로 빠져 CustomSqlException이 실행되면서 "예외 발생"이라는 메세지가 전달된다.

 

[결과]

Y 입력
N 입력

 

참고자료
 

Spring boot ModelAndView를 jsonView로 return하기

Web개발을 할 때 ajax를 사용해서 통신하는 경우가 매우 많다. 클라이언트에서 Spring Controller로 ajax 등의 요청을 했을 때, json형식으로 return 받기 위해서는 여러 방법이 있을 수 있다. 그 중 두 가지

www.leafcats.com

 

[Spring] Annotation 정리

Annotation(@)은 사전적 의미로는 주석이라는 뜻이다. 자바에서 사용될 때의 Annotation은 코드 사이에 주석처럼 쓰여서 특별한 의미, 기능을 수행하도록 하는 기술이다.

velog.io

 

[Network] REST란? REST API란? RESTful이란? - Heee's Development Blog

Step by step goes a long way.

gmlwjd9405.github.io


예외 처리를 분리하여 관리하는 이점은 비즈니스 로직에만 집중할 수 있도록 하는 것입니다. 여러분들은 어떻게 예외 처리를 하고 계신가요? 전체 소스는 GitHub에서 확인하실 수 있습니다.

+피드백은 언제나 환영입니다 :)

반응형