본문 바로가기

백엔드 개발/SpringMVC

Spring에서 예외 처리하는 방법(2)

<개요>

저번 시간까지 정리한 내용으로 서버 처리하다가 예외가 발생했을 시 이를 대처하는 방법이 3가지가 있었다. 

(1) 예외가 발생할 수 있는 구문을 try - catch로 묶는다. 

      이는 예외가 발생할 수 있는 구문마다 try-catch 구문으로 묶어야 해서 아주 비효율적이었다.

(2)  컨트롤러 내부에 예외처리만 담당하는 매소드 만들기.

       예외처리 매소드의 어노테이션은 @ExceptionHandler 이다.

       작동원리는 다음과 같다.

       a.  해당 컨트롤러 내부의 어떤 매소드에서 예외가 발생하고, 그 매소드는 예외를 밖으로 던진다. (throws)

       b. @ExceptionHandler 어노테이션이 붙은 매소드 중 해당 예외를 처리할 수 있는 매소드가 있다면 catch 구문처럼 

       해당 예외를 객체로 받아서 처리한다.

      **@ExceptionHandler() 이 ()안에 적힌 예외가 해당 예외처리 매소드가 처리할 분야이다.

(3) 범클래스 예외처리 어노테이션 @ControllerAdvice 사용

     어떤 클래스에 @ControllerAdvice를 달아놓으면, 해당 어노테이션은 모든 패키지의 예외 처리를 담당할 수 있다.

     만약 특정 패키지에만 적용하고 싶다면, @ControllerAdivce()의 해당 ()안에 패키지의 경로를 쓰면 된다. 

     @ControllerAdivce와 예외 발생한 컨트롤러 내부에 @ExceptionHandler 중 우선순위는 후자가 높다.

      (예외 발생 지역에서 가까울수록 우선순위가 높다.)

      따라서 컨트롤러 내부 @ExceptionHandler에는 특정 매소드를 위한 맞춤 처리를, @ControllerAdvice에는 광범위하게 적용될 일반적인 처리를 적어놓는 것이 좋다. 

이번에는 이외에도 예외 처리하는 방법, 예외처리의 우선순위, 예외처리하는 과정, 상태코드 바꾸는 방법을 배워보겠다.

 

1. 상태코드 바꾸는 법

@ResponseStatus(HttpStatus.바꿀_상태코드의_이름)

이 어노테이션을 쓰면 요청에 대한 응답 메세지의 상태코드를 바꿀 수 있다.

해당 어노테이션이 쓰이는 경우는 크게 2가지가 있다. 

(1) 예외처리 매소드에게 쓰임.

a. 쓰여야 하는 이유 

위의 3가지 경우 (try-catch, @ExceptionHandler, @ControllerAdvice) 모두 예외를 객체로 받아 어떤 처리를 하고, 에러를 처리할 view를 반환 했다. 에러가 발생했음에도, 위의 과정들은 서버가 진행해야하는 절차들을 성공적으로 모두 진행한 것이다. 그래서 다른 설정을 하지 않는 경우, 응답 메세지에는 상태코드 200(정상)이 찍힌다.

이 경우 에러가 발생했음에도 상태코드만 봐서는 에러가 있었는지 없었는지 확인을 못한다. 

또 정상적인 서버 기능을 하지 못하고 예외처리만 했을 뿐인데, 상태코드가 200이 찍히는 것은 올바르지 않다.

그래서 예외처리 매소드로 예외처리 시 상태코드를 자신이 원하는 에러코드가 나오도록 설정 해야한다.. (400번대 - 클라이언트 잘못, 500번대 - 서버 잘못)

 

b. 예시 

	// 모든 예외 처리
    @ExceptionHandler(Exception.class)
	// 상태코드 변경
	@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 200 -> 500 
	public String catcher(Exception ex) {
		return "error";
	}

(2) 사용자 정의 예외 클래스에게 쓰임.

a. 써야하는 이유

예외마다 뜨는 상태코드는 각각 다르다. 하지만 사용자 정의 예외일 경우 프로그래머가 따로 설정하지 않는 경우 default로 상태코드500(서버-잘못)을 내뱉는다. 이는 클라이언트에게 문제가 무엇인지에 대해 잘못된 정보를 줄 수 있으므로 고쳐야 한다. 

 또한 밑에 다른 예외 처리 방법을 쓸 시에 상태코드가 무엇인지가 중요해지므로, 상태코드가 정확하게 찍히도록 @ResponseStatus로 미리 설정해 놓는 것이 중요하다.

 

b.예시

@ResponseStatus(HttpStatus.BAD_REQUEST) // 에러코드를 500(default) -> 400 
class MyException extends RuntimeException {
	MyException(String msg){
		super(msg);
	}
	
	MyException() { this("");}
}

 

 

2. 예외 처리하는 방법  2가지 추가

첫 번째 방법은 예외 종류 별로 Error view를 Mapping하는 방법이다. 해당 방법은 Servlet-context.xml에서 작업한다. 

예전에 배운 내용 중에, 클라이언트의 요청 별로 view Mapping 하는 것도 Servlet-context.xml에서 가능했었다. 

코드는 다음과 같다.

<view-controller path="/register/add" view-name="registerForm"/>

이는 "/register/add"로 Get Mapping이 오면, registerForm.jsp를 매핑하라는 뜻이다. 이는 요청을 받아서 뭔가 처리해줄 필요가 없는 경우, 그저 컨트롤러가 view 화면만 띄워도 되는 경우에 쓰인다. 

마찬가지로, 예외 발생 시 별 다른 처리 없이 Error view만 띄우는 경우에도 Servlet-context를 이용한 방법을 쓸 수 있다.

 

두 번째 방법은 상태 코드 별로 Error View를 Mapping 하는 방법이다. 해당 방법은 web.xml에서 작업한다. 

이는 예외를 처리하는 3가지 방법(try-catch로 예외코드 묶기, @ExceptionHandler, @ControllerAdivce)으로 처리가 안된 예외를 처리하기 위한 마지막 그물망이다. 예를 들어 A 예외를 처리하는 예외처리 매소드를 만들지 않아 A 예외가 발생해도 처리가 안될 수 있다. 이 경우에 상태코드만 보고 해당 상태코드일 때 뜨는 view를 보여줘서 예외 발생에 대응하는 것이다. 

(1) 예외 종류 별로 Error view(jsp) Mapping 하기

Spring의 Web 설정을 담당하는 Servlet-context.xml에서 작업해야한다. 

<!-- 예외 종류 별로 Error view mapping 하는 코드들
	여기서는 MyException 이라는 예외를 처리할 시 error400이라는 view를 찾아 열도록 설정해놓았다. -->
		<beans:bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
		<beans:property name="defaultErrorView" value="error"/>
    		<beans:property name="exceptionMappings">
      			<beans:props>
        			<beans:prop key="com.fastcampus.ch2.MyException">error400</beans:prop>
      			</beans:props>
    		</beans:property>
		<beans:property name="statusCodes">
			<beans:props>
        			<beans:prop key="error400">400</beans:prop>
			</beans:props>
		</beans:property>
  	</beans:bean>

이 코드를 추가하면 된다. 여기서 

<beans:props>
    <beans:prop key="com.fastcampus.ch2.MyException">error400</beans:prop>
 </beans:props>

이 부분은 예외의 종류와 해당 예외 발생 시 열어야할 Error View를 연결해준다.

여기서는 MyException 예외 발생 시 error400.jsp를 열라고 지시한다. 

<beans:props>
    <beans:prop key="error400">400</beans:prop>
</beans:props>

이건 error400.jsp view 띄울 때 상태 코드를 400으로 하라는 뜻이다. 

 

**참고** 

ㄱ.

우리가 Error View의 상태 코드를 400으로 해놨는데도 예외 발생 page 까보니 500으로 되어있는 경우가 있다! 

이는  Error View jsp 내부에 isErrorPage가 true로 되어있기 때문이다.

<%@ page contentType="text/html;charset=utf-8" isErrorPage="true"%>

해당 명령어는 예외 객체가 null이 아니면 상태코드를 500으로 바꾸라는 뜻이다. 

그래서 400으로 바꾸고 page를 열어도 다시 상태코드가 500이 되는 것이다. 

이를 없애려면 해당 isErrorPage = "false"로 바꾸자.

 

ㄴ.

여기서 여는 view는 webapp의 views 파일에 있는 녀석들이다. 원래 우리가 쓰던 파일이다. 

예외 종류 별 view mapping과 상태 코드 별 view mapping이 서로 다른 영역에 저장된 view를 씀으로 주의하자. 

상태 코드 별 view mapping은 WEB-INF라는 상위 파일에 존재하는 view를 쓴다.

(2) 상태 코드 별로 Error view(jsp) Mapping 하기 

이거는 web.xml 파일을 이용한다. 

	<error-page>
		<error-code>400</error-code>	
		<location>/error400.jsp</location>
	</error-page>

이걸 추가해주면 된다. 그러면 어떤 컨트롤러의 처리 결과로 400이라는 상태코드가 나왔을 경우 error400.jsp라는 view를 띄운다. 

web.xml은 WEB-INF 폴더에서 view를 찾기 때문에 거기서 만들어줘야한다.

3. Spring에서 예외처리하는 과정

(1) Controller에서 처리 중에 예외가 발생.

(2) 만약 try -catch 구문으로 예외 처리 했으면 컨트롤러 내부에서 예외처리가 전부 끝난다. 

(3) 그게 아니라면 컨트롤러가 예외 내용을 다시 DispatcherServlet에게 보낸다. 

(4) DispatcherServlet은 예외를 받고 Spring에서 제공하는 예외처리 기본전략에 따라 우선 순위대로 일을 처리 한다.

(4-1) 만약 ExceptionHandler로 어노테이션 된 예외처리 매소드 중에 해당 예외를 처리할 수 있는 녀석이 있으면, 그 녀석에게 예외를 보내서 예외처리 시킴. (동일 컨트롤러 내부에 있는 것, @ControllerAdvice에 있는 것 싹다 뒤짐)

(4-2) @ExceptionHandler 적힌 매소드 중 아무도 못하면, 다음과 같이 한다. 만약 예외 발생 시킨 클래스에 @ResponseStatus로 상태 코드를 설정해놓았다면, 상태코드가 뭔지 본다.   그리고 Web.xml에 상태코드에 맞게 띄울 Error view가 맵핑되어 있다면 그 View를 띄운다.

(4-3) 만약 @ResponseStaus 안 해놔서 예외발생 시 Spring의 default인 상태코드 500이 뜨는 경우, DispatcherServlet은 DefaultHandlerExceptionResolver를 찾아간다. 이 녀석은 본인만의 상태코드를 500에서 다른 코드로 바꾸는 메뉴얼을 가지고 있다. 그래서 DispatcherServlet이 예외를 들고 찾아오면, 그 예외에 맞게 상태코드를 400번대, 500번대 중 하나로 바꿔준다. 

<참고> 예외처리 view와 예외처리 매소드 사이에 model 없이 예외 객체를 주고 받는 법

<%@ page contentType="text/html;charset=utf-8" isErrorPage="true"%>

여기서 isErrorPage를 true로 해버리면, 예외처리 view와 예외처리 매소드 사이에 model 없이도 예외 객체를 주고 받을 수 있다. 

만약 모델 없이 직통으로 예외 객체를 받는다면 당연히 JSP 내의 EL 내용도 달라진다.

<!-- 모델을 사용하여 예외 객체를 받아올 때 EL 문법 -->
발생한 예외 : ${ex}<br>
예외 메시지 : ${ex.message}<br>

<!-- 위의 isErrorPage = "true" 삽입 후 모델 없이 에러 객체를 받을 경우 EL 문법 -->
발생한 예외 : ${pageContext.exception}<br>
예외 메시지 : ${pageContext.exception.message}<br>

Model을 안 쓰는 경우 JSP의 내부 저장소인 pageContext를 쓴다는 것을 알 수 있다. 여기서 예외 객체를 꺼내 쓰는 것이다. 

5. 스스로 해보기  

(1) @ResponseStatus 안 썼을 때 상태 코드, 썼을 때 상태코드 바꿔가며 확인

    -1 예외처리 메소드 앞에 쓰기

만약 @ResponseStatus를 하지 않으면 200이 나와야 한다. 서버가 예외를 잘 처리 했기 때문이다. 

그런데 내꺼는 500이 나왔다. 

그 이유는 isErrorPage가 true로 되어있어서이다. 상태코드가 뭘로 변환되어 오든, view에서 해당 요소가 true이면, 상태코드는 항상 500으로 다시 바뀐다. 해당 부분을 false로 해놓고, 다시 해보자.

---------------------------

jsp 해당 부분을 false로 돌리고 주석 처리한 상태에서 돌려보았다.

default 상태인 200 잘 떴다. 다시 @ResponseStatus 부분 주석을 빼니 

500 나왔다.

    -2 사용자 정의 예외 클래스 앞에 쓰기 

@ResponseStatus(HttpStatus.BAD_REQUEST) // 에러코드를 500(default) -> 400 
class MyException extends RuntimeException {
	MyException(String msg){
		super(msg);
	}
	
	MyException() { this("");}
}

내가 만든 MyException은 Default가 500이지만, @ResponseStatus를 통해 상태코드를 400으로 바꿨다. 

해당 컨트롤러 내부에는 예외처리 매소드가 없으므로 @ControllerAdvice 걸린 놈이 해결할 것이다. 

그렇다면 만약 @ControllerAdvice 걸린 놈도 @ResponseStatus 걸려 있으면 상태코드는 어떻게 뜰까? 

난 @ControllerAdivce를 만나면 상태코드가 다시 500이 되게끔 설정해보겠다.

예상은 500 -> 400 -> 500으로 변경되는 것이다.

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
	@ExceptionHandler(Exception.class)
	public String catcher2(Exception ex, Model m) {
		
		m.addAttribute("ex", ex);
		return "error";
	}

바뀌었다.

(2) servlet-context.xml과 web.xml 실행해보기 

servlet-context.xml은 spring >  appservlet > servlet-context.xml 경로로 가면 찾을 수 있다. 

MyException에 대해서 예외처리를 할 것인데, 다른 예외처리 매소드들이 일하지 못하도록 모두 주석처리 해주었다. 

그리고 web.xml도 주석처리 했다. 

이제 MyException 예외 발생 시 servlet-context가 발동하여 400Error라는 화면이 뜰 것이다. 

web.xml만 살리고 돌렸을 때도 같은 결과가 나왔다.