예외 처리하기

 

- @ExceptionHandler

 

컨트롤러 안에 다음과 같이 에러를 처리하는 메서드를 만들 수 있다.

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

일반 컨트롤러처럼 Model에 예외 객체를 저장하여 view에 전달할 수 있다.

 

ExceptionHandler는 컨트롤러 내의 메서드에 대해서만 에러처리가 가능하다.

 

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

JSP 파일에 다음과 같이 isErrorPage=true로 하면 Model에 예외 객체를 저장하지 않아도

 ${pageContext.exception} 와 같이 기본객체와 EL을 사용해서 예외 객체를 활용할 수 있다.

 

 

- @ControllerAdvice

 

다음과 같이 패키지 내에서 발생하는 에러를 처리하는 클래스를 만들 수 있다.

(@ControllerAdvice는 꼭 전역 예외처리 클래스를 만드는 용도로만 사용하는 애너테이션은 아님)

@ControllerAdvice("com.fastcampus.ch2")
//@ControllerAdvice // 모든 패키지에 적용
public class GlobalCatcher {
	@ExceptionHandler({NullPointerException.class, FileNotFoundException.class}) // 여러 예외를 처리할때는 배열로
	public String catcher2(Exception ex, Model m) {
		System.out.println("catcher2() in GlobalCatcher");
		m.addAttribute("ex", ex);
		return "error";
	}
	
	
	@ExceptionHandler(Exception.class)
	public String catcher(Exception ex, Model m) {
		System.out.println("catcher() in GlobalCatcher");
		m.addAttribute("ex", ex);
		return "error";
	}
}

애너테이션에 패키지 이름을 따로 적으면 해당 패키지의 메서드에만 적용된다.

 

 

만약 같은 컨트롤러 내에 에러를 처리하는 메서드가 있으면 그 메서드가 에러를 처리하고

컨트롤러 내에 에러를 처리하는 메서드가 없다면 GlobalCatcher에 등록된 메서드가 에러를 처리하게 된다.

 

 

- @ResponseStatus 

응답 메세지의 상태 코드를 변경할 때 사용한다.

 

@ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED) // 405 Method Not Allowed.
@ExceptionHandler(Exception.class)
public String catcher(Exception ex, Model m) {
    m.addAttribute("ex", ex);
    return "error";
}

error.jsp를 반환하게 되면 200번대의 요청처리 성공 상태코드가 표시된다.

error가 발생했는데 요청처리 성공 코드가 나타나는 것은 바람직하지 않은 상태이기 때문에

400번대나 500번대, 즉 클라이언트 에러나 서버 에러로 나타내기 위해서

@ResponseStatus 애너테이션을 붙여서 원하는 상태코드로 나타낼 수 있다.

 

 

- 사용자정의 예외 클래스

 

사용자정의로 만든 에러 클래스는 에러가 발생했을 때

디폴트 상태코드인 500번 Internal ServerError 를 나타내는데,

이 500번 대신 원하는 상태 코드를 나타내고 싶을 때

클래스에 @ResponseStatus 애너테이션을 활용하면 된다.

 

 

- 상태 코드별 뷰 맵핑

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

해당 상태코드가 나타낼 view를 다음과 같이 지정해주는 코드를 web.xml에 추가할 수 있다.

(src/main/webapp 에 있는 jsp파일을 사용 // view에 있는 jsp 파일을 사용하지 않음. 경로 주의!!)

 

 

- SimpleMappingExceptionResolver

 

예외 종류별 뷰 맵핑에 사용. servlet-context.xml에 등록

 

<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">404</beans:prop>
			</beans:props>
		</beans:property>
  	</beans:bean>

다음과 같은 코드를 추가하여 MyException(특정 예외 종류)이 발생했을 경우 보여줄 뷰를 error400.jsp로 지정했다.

 

그리고 error400.jsp가 보여진다면 그 상태코드는 404번이 되도록 지정하였다. (statusCodes 아래 부분)

 

- ExceptionResolver

DispatcherServlet은 예외를 처리하려고 handlerExceptionResolvers를 확인함

 

1) 적절한 @ExceptionHandler가 있으면 처리

2) @ResponseStatus가 설정한 상태코드로 바꿔주고, 해당하는 view를 보여줌

3) 스프링에 정의된 예외의 상태코드를 적절한 코드로 바꿔주는 역할을 함

 

 

- 스프링에서의 예외 처리

 

- 컨트롤러 메서드 내에서 try-catch로 처리

- 컨트롤러에 @ExceptionHandler메소드가 처리

- @ControllerAdvice클래스의 @ExceptionHandler메소드가 처리

- 예외 종류별로 에러뷰를 지정 - SimpleMappingExceptionResolver

- 응답 상태코드별로 에러뷰를 지정 - <error-page> (web.xml에서)

 

- DispatcherServlet

Spring MVC의 요청 처리 과정

 

1) 클라이언트에게 DispatcherServlet가 요청을 받음

 

2) HandlerMapping으로부터 요청 URL에 대응되는 메서드가 뭔지 전달받아서,

처리할 수 있는 HandlerAdapter에게 다시 전달함

(HandlerMapping에는 URL - 메소드가 Map의 형태로 저장되어 있음)

 

3) HandlerAdapter에서 해당 URL과 메서드가 있는 Controller를 호출하고

작업을 통해 얻은 Model과 view 이름을 DispatcherServlet에 전달함

 

4) DispatcherServlet은 InternalResourceViewResolver를 통해 해당 이름을 가진 view파일의 위치와 확장자를 얻음.

JstlView(인터페이스)를 거쳐 해당 jsp 파일에 Model을 전달하고 jsp 파일이 Model을 이용하여 응답결과를 만들어 클라이언트에게 응답함.

 

 

 

데이터의 변환과 검증

클라이언트에서 넘어오는 데이터들이 적절한 타입의 값으로 변환되도록 해야 하고,

또 그 값 자체 또한 적절한지 검증을 해줘야 한다.

 

- WebDataBinder

 

WebDataBinder가 하는 일은 2가지가 있다.

 

1. 타입을 적절하게 변환한다.

2. 데이터 검증의 결과나 에러를 BindingResult에 저장한다.

 

만약 잘못된 값이 들어오고, 타입이 적절하게 변환되지 못하면 예외가 발생하여 에러페이지가 나타남.

 

메서드의 특정 매개변수 바로 뒤에 BindingResult 타입의 매개변수를 추가해주면

잘못된 값이 매개변수에 들어와도 에러페이지로 이동하지는 않음.(그러나 BindingResult 객체에 error가 저장됨)

 

 

 

- RegisterController에 변환기능 추가하기

@InitBinder
public void toDate(WebDataBinder binder) {
    SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
    binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
    binder.registerCustomEditor(String[].class, new StringArrayPropertyEditor("#"));
}

@InitBinder를 이용해서 특정한 형식의 데이터를 변환해주는 메서드를 만들 수 있다.

이 메서드를 컨트롤러 안에 만들어주고 필요한 변환 기능을 넣으면 된다.

 

public class User {
	private String id;
	private String pwd;
	private String name;
	private String email;
	@DateTimeFormat(pattern="yyyy-MM-dd")
	private Date birth;
	private String[] hobby;
 	...
}

또는 직접 클래스에 @DateTimeFormat을 붙여서 특정한 형식의 데이터를 변환할 수 있다.

(Formatter를 사용한 것)

 

 

- PropertyEditor

 

PropertyEditor - 양방향 타입 변환(String -> 타입, 타입 -> String)

특정 타입이나 이름의 필드에 적용 가능

 

- 디폴트 PropertyEditor - 스프링이 기본적으로 제공

- 커스텀 PropertyEditor - 사용자가 직접 구현. PropertyEditorSupport를 상속하면 편리

 

모든 컨트롤러 내에서의 변환 - WebBindingInitializer를 구현 후 등록

특정 컨트롤러 내에서의 변환 - 컨트롤러에 @InitBinder가 붙은 메서드를 작성

 

- Converter와 ConversionService

 

Converter 는 단방향 타입 변환(타입A->타입B)을 한다.

PropertyEditor의 단점을 개선했음(stateful -> stateless)

(stateful의 의미는 iv를 사용한다는 것인데, 그렇다면 싱글톤으로 사용할 수 없어 매번 변환할 때마다 새로운 객체를 만들어야함)

 

그래서 Converter를 쓰는것이 더 좋음.

 

ConversionService - 타입 변환 서비스를 제공. 여러 Converter를 등록 가능

WebDataBinder에 DefaultFormattingConversionService이 기본 등록되어 있음

 

모든 컨트롤러 내에서의 변환 - ConfigurableWebBindingInitializer를 설정해서 사용

특정 컨트롤러 내에서의 변환 - 컨트롤러에 @InitBinder가 붙은 메서드를 작성하고

ConversionService에 Converter를 등록하여 사용

 

- Formatter

 

Formatter - 양방향 타입 변환(String -> 타입, 타입 -> String)

 

바인딩할 필드에 적용 - @NumberFormat, @DateTimeFormat

각각 숫자타입과 날짜타입을 변환할 때 사용

 

 

변환이 적용되는 우선순위

1) 커스텀 PropertyEditor

2) ConversionService에 등록된 Converter

3) 디폴트 PropertyEditor

 

 

- Validator

 

객체를 검증하기 위한 인터페이스. 객체 검증기(validator)구현에 사용

 

 

- Validator를 이용한 수동 검증

 

먼저 Validator를 구현한 UserValidator라는 클래스를 만든다. (Override 기능을 사용하면 편하다)

supports 메서드로 객체의 타입이 적절한지 검사하고

validate 메서드로 객체의 멤버값들이 적절한지 검사하는 코드를 작성한다.

 

validate의 매개변수로 target과 errors가 있는데

target은 검증할 객체이고 errors는 검증시 발생한 에러를 저장하는 객체이다.

 

BindingResult는 Errors의 자손이다. (둘 다 Interface임)

 

유효성 검사가 필요한 메서드 내부에 UserValidator 객체를 생성하고

validate 메서드를 호출하여 객체를 검사한 결과를 BindingResult에 저장한다.

 

- Validator를 이용한 자동 검증

 

@InitBinder가 붙은 메서드의 매개변수인 binder 객체를 이용해서 

setValidator 메서드로 등록을 한다. (addValidator가 아님에 주의)

 

그리고 사용하고자 하는 메서드의 매개변수에 @Valid 애너테이션을 붙이면 자동으로 해당 매개변수를 검증한다.

 

(@Valid 애너테이션은 스프링에 없기 때문에 별도로 ValidationAPI를 Maven Repository에서 다운로드 받아서 사용해야 한다)

 

 

- 글로벌 Validator

하나의 Validator로 여러 객체를 검증할 때, 글로벌 Validator로 등록

 

 

글로벌 Validator로 등록하는 방법)

 

[Servlet-context.xml]

<annotation-driven validator="globalValidator"/>

<beans:bean id="globalValidator" class="com.fastcampus.ch2.GlobalValidator"/>

 

 

- MessageSource

다양한 리소스에서 메세지를 읽기 위한 인터페이스

 

먼저 에러메세지들을 저장한 properties 파일을 만들고(src/main/resources 아래에 생성),

그 안에 요소와 에러메세지를 저장한다.

 

그리고 servlet-context.xml 파일에 

프로퍼티 파일을 메세지 소스로 하는 ResourceBundleMessageSource를 등록한다.

<beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
		<beans:property name="basenames">
			<beans:list>
				<beans:value>error_message</beans:value> <!-- /src/main/resources/error_message.properties -->
			</beans:list>
		</beans:property>
		<beans:property name="defaultEncoding" value="UTF-8"/>
</beans:bean>

 

 

 

- 검증 메세지의 출력

 

스프링이 제공하는 커스텀 태그 라이브러리를 사용

<%@ taglib = uri="http://www.springframework.org/tags/form" prefix="form" %>

 

위의 코드를 registerForm.jsp 파일 가장 상단에 추가.

 

<form> 대신 <form:form> 사용

 

<form> 태그의 속성을 다음과 같은 코드로 수정해준다

<form:form modelAttribute="user">

                   ↓ (실제 소스는 위 태그가 아래과 같이 변하게 됨)

<form id="user" action="/ch2/register/save" method="post">

 

 

<form:errors>로 에러를 출력. path에 에러 발생 필드를 지정(path를 *로 지정하면 모든 필드의 에러를 지정하게 됨)

 

<form:errors path="id" cssClass="msg"/>

                    ↓ (위 태그가 아래과 같이 변하게 됨)

<span id="id.errors" class="msg">필수 입력 항목입니다.</span>

 

 


출처 : 스프링의 정석 : 남궁성과 끝까지 간다

+ Recent posts