예외처리 - 이론

 

1. @ExceptionHandler와 @ControllerAdvice

 

예외처리를 위한 메소드를 작성하고 @ExceptionHandler를 붙인다.

 

@ControllerAdvice로 전역(모든 컨트롤러) 예외 처리 클래스 작성 가능(패키지 지정 가능)

예외 처리 메소드가 중복된 경우, 컨트롤러 내의 예외 처리 메소드가 우선

 

2. @ResponseStatus

 

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

1) 예외 처리 메소드

	@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 애너테이션을 붙여서 원하는 상태코드로 나타낼 수 있다.

 

 

2) 사용자정의 예외 클래스

@ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request.
class MyException extends RuntimeException {
	MyException(String msg) {
    	super(msg);
    }
    
    MyException() {
    	this("");
    }
}

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

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

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

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

 

 

 

3. <error-page> - web.xml

 

상태 코드별 뷰 맵핑

	<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에 추가하였다.

 

 

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

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

 

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

 

 

5. ExceptionResolver

1 2 3 순서대로 처리 가능한지 확인함

 

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

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

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

 

 

6.  스프링에서의 예외 처리

 

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

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

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

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

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

 

 

 

DispatcherServlet

 

1. DispatcherServlet이란?

전처리 작업을 해줌

 

 

2. Spring MVC의 요청 처리 과정

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

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

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

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

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

 

 

3. DispatcherServlet의 소스 분석

 

DispatcherServlet.class는 spring-webmvc-5.0.7 RELEASE.jar에 포함

소스 파일 위치 - org/springframework/web/servlet/DispatcherServlet.java

기본 전략 - org/springframework/web/servlet/DispatcherServlet.properties

 

주요 메소드

void initStrategies(ApplicationContext context) - 기본 전략을 초기화

void doService(HttpServletRequest request, HttpServletResponse response) - doDispatch() 호출

void doDispatch(HttpServletRequest request, HttpServletResponse response) - 실제 요청을 처리

void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain) - 예외가 발생했는지 확인하고, 발생하지 않았으면 render()를 호출

void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) - 응답결과를 생성해서 전송

 

 

 

데이터의 변환과 검증

1. WebDataBinder

타입변환이나 데이터 검증의 결과나 에러를 BindingResult 저장함 

 

 

2. 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)

 

 

3. PropertyEditor

 

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

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

 

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

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

 

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

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

 

 

4. Converter와 ConversionService

 

Converter - 단방향 타입 변환(타입A->타입B)

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

 

public class StringToStringArrayConverter implements Converter<String, String[]> {
	@Override
    public String[] convert(String source) {
    	return source.split("#"); // String -> String[]
    }
}

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

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

 

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

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

 

 

5. Formatter

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

 

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

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

 

 

6. Validator란?

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

 

 

7. Validator를 이용한 검증 - 수동

package com.fastcampus.ch2;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class UserValidator implements Validator {
	@Override
	public boolean supports(Class<?> clazz) {
//		return User.class.equals(clazz); // 검증하려는 객체가 User타입인지 확인
		return User.class.isAssignableFrom(clazz); // clazz가 User 또는 그 자손인지 확인
	}

	@Override
	public void validate(Object target, Errors errors) { 
		System.out.println("LocalValidator.validate() is called");

		User user = (User)target;
			
		String id = user.getId();
			
	//	if(id==null || "".equals(id.trim())) {
	//		errors.rejectValue("id", "required");
	//	}
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id",  "required");
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
			
		if(id==null || id.length() <  5 || id.length() > 12) {
			errors.rejectValue("id", "invalidLength");
		}
	}

}

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

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

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

 

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

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

 

 

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

public interface Errors {
	//...
    void reject(String errorCode);
    void rejectValue(String field, String errorCode);
}

 

 

@PostMapping("/register/save") // Spring 4.3 부터
	public String save(User user, BindingResult result, Model m) throws Exception {
//		System.out.println("result="+result);
		System.out.println("user="+user);
		
		UserValidator userValidator = new UserValidator();
		userValidator.validate(user, result); // BindingResult는 Errors 인터페이스의 자손
		
		// User객체를 검증한 결과 에러가 있으면, registerForm을 이용해서 에러를 보여줘야 함.
		if(result.hasErrors()) {
			return "registerForm";
		}
		
//		// 1. 유효성 검사
//		if(!isValid(user)) {
//			String msg = URLEncoder.encode("id를 잘못입력하셨습니다.","utf-8");
//
//			m.addAttribute("msg",msg);
//			return "forward:/register/add";
////			return "redirect:/register/add?msg="+msg; // URL재작성(rewriting)
//		}
		
		// 2. DB에 신규회원 정보를 저장
		return "registerInfo";
   }

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

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

 

 

 

 

8. Validator를 이용한 검증 - 자동

 

	@InitBinder
	public void toDate(WebDataBinder binder) {
		ConversionService conversionService = binder.getConversionService();
//		System.out.println("conversionService="+conversionService);
//		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
//		binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
		binder.registerCustomEditor(String[].class, "hobby", new StringArrayPropertyEditor("#"));
		
        binder.setValidator(new UserValidator()); // UserValidator를 WebDataBinder의 validator로 등록
	}

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

setValidator 메소드로 등록을 한다.

 

	@PostMapping("/register/save") // Spring 4.3 부터
	public String save(@Valid User user, BindingResult result, Model m) throws Exception {
		System.out.println("result="+result);
		System.out.println("user="+user);
		
//		// 수동 검증 - Validator를 직접 생성하고, validate()를 직접 호출
//		UserValidator userValidator = new UserValidator();
//		userValidator.validate(user, result); // BindingResult는 Errors 인터페이스의 자손
		
		// User객체를 검증한 결과 에러가 있으면, registerForm을 이용해서 에러를 보여줘야 함.
		if(result.hasErrors()) {
			return "registerForm";
		}
		
//		// 1. 유효성 검사
//		if(!isValid(user)) {
//			String msg = URLEncoder.encode("id를 잘못입력하셨습니다.","utf-8");
//
//			m.addAttribute("msg",msg);
//			return "forward:/register/add";
////			return "redirect:/register/add?msg="+msg; // URL재작성(rewriting)
//		}
		
		// 2. DB에 신규회원 정보를 저장
		return "registerInfo";
	}

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

 

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

 

Maven Repository에서 Validation을 검색해서 

이것을 클릭

 

최신 fianl 버전을 선택 후 

해당 코드 클릭해서 복사.

 

Maven 설정하는 파일인 pom.xml 에서 

<dependencies> 태그 안쪽에 복사한 코드를 넣어주고 저장 후

Project를 업데이트 해주면 @Valid 애너테이션이 추가됨.

 

 

9. 글로벌 Validator

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

글로벌 Validator로 등록하는 방법

 

[Servlet-context.xml]

<annotation-driven validator="globalValidator"/>

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

 

 

 

글로벌 Validator와 로컬 Validator를 동시에 적용하는 방법)

@InitBinder
public void toDate(WebDataBinder binder) {
	
//  binder.setValidator(new UserValidator()); // validator를 WebDataBinder에 등록
    binder.addValidators(new UserValidator()); // validator를 WebDataBinder에 등록

}

(글로벌 Validator에 추가로 로컬 Validator를 등록하는 것)

 

 

10. 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>

(다음과 같은 내용을 등록)

 

 

11. 검증 메세지의 출력

 

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

<%@ 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>

 

 

마지막으로 UserValidator.java 파일의 validate 메소드에 적절한 숫자를 추가해줘야 한다.

package com.fastcampus.firstSpring;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class UserValidator implements Validator {

	@Override
	public boolean supports(Class<?> clazz) { // 타입 검증
//		return User.class.equals(clazz);
		return User.class.isAssignableFrom(clazz); // clazz가 User 또는 그 자손인지 확인
	}

	@Override
	public void validate(Object target, Errors errors) { // 멤버 변수 검증
		System.out.println("LocalValidator.validate() is called");
		
		// 객체 형변환.
		User user = (User)target;
		
		// id 얻어오기
		String id = user.getId();
		
//		if(id==null || "".equals(id.trim())) {
//			errors.rejectValue("id", "required");
//		}
		
		// id, pwd가 비어있으면 에러에 저장
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "required");
		ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
		
		
		if(id==null || id.length() < 5 || id.length() > 12) {
			errors.rejectValue("id", "invalidLength", new String[] {"", "5", "12"}, null);
		}

	}

}

가장 마지막 코드를 보면 id 검증 부분에

errors.rejectValue("id", "invalidLength", new String[] {"", "5", "12"}, null); 로 수정했다.

 

String 배열 부분은 properties 파일에 {1}~{2}에 해당하는 부분의 값을 넣어준 것으로, 각각 index 0, 1, 2 의 값이기 때문에

에러 메세지가 5~12 와 같이 출력된다.

 

(null에 해당하는 부분은 에러메세지가 없을 시 출력되는 메세지를 넣는부분인데, 일단 null로 설정해둔것이다.)

 


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

 

+ Recent posts