Ch. 02 Spring DispatcherServlet, 데이터변환과 검증

2023. 1. 10. 11:54

1. DispatcherServlet 파헤치기

 

<DispatcherServlet>

 

- 기본적으로 입력,처리,출력 서블릿이 여러개 존재한다
- 공통 처리 부분을 DispatcherServlet을 통해 제거한다. 즉 DispatcherServlet이 전처리를 해준다.

 

 

<Spring MVC의 요청 처리 과정>

 

1. 요청이 들어오면 DispatcherServlet은 HandlerMapping한테 해당 URL에 대한 Handler Method 정보를  받아옴 
(HandlerAdapter를 통해 DispatcherServlet와 Controller을 느슨한 연결을 함)
2. HandlerMapping을 통해 받아온 정보로 누가 이것을 처리할 수 있는지 모든 HandlerAdapter에 대해 물어봄 그리고  HandlerAdapter를 통해 어떤 Controller로 호출할지 알게 되는 것임.
3. DispatcherServlet가 해당 HandlerAdapter를 통해 Controller를 호출하고 뷰이름을 받아온다. 
4. DispatcherServlet가 InternalResourceViewResolver (servlet - context.xml)에 view이름을 넘겨주면 해당 이름에 접두사(/WEB-INF/views/)와 접미사(.jsp)를 붙혀 실제 뷰이름을 반환함
5. DispatcherServlet이 해당 뷰를 호출하고 모델을 전달하면 해당 jsp 파일이 모델을 이용해 응답결과를 만들고 client에 응답을 해준다
6. jsp 파일은 JstlView가 처리

 

(DispatcherServlet의 소스 분석은 2-29강의를 통해 가볍게 듣기)

DispatcherServlet.class는 spring-webmvc-5.0.7.RELEASE.jar에 포함
소스 파일 위치 - org/springframework/web/servlet/DispatcherServlet.java
기본 전략 (기본 사용 클래스) - org/springframework/seb/servlet/DispatcherServlet.properties

 

 

2. 데이터 변환 - 실습

 

<WebDataBinder>

- WebDataBinder가 데이터가 들어오면 타입 변환을 하여 BindingResult에 값을 넣는다. (에러면 에러넣음)
- 데이터 검증도 해줌 (예를들어 month는 1~12 사이 숫자여야 함)
- Binding결과는 컨트롤러로 전해짐 

 

<RegisterController에 변환 기능 추가하기 - 실습>

- registerForm으로부터 받은 데이터(String)들이 RegisterController로 넘어 가는데
이 값들이 WebDataBinder를 통해 타입변환되어서 넘어간다. (예로 birth는 String -> Date)
- sns에 여러개 체크하면 String[]이 넘어가는데 User 객체의 sns는 String 타입임
- 이걸 spring이 자동 변환해줌 (근데 String -> Date는 자동으로 안해줌)

 

a. <RegisterController.java> - @InitBinder는 이 Controller에서만 사용 가능

	...
	// 스프링이 제공하는 CustomDateEditor를 이용해서 변환. String -> Date
   	@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("#"));
        // String[]에서 #을 구분자로 사용 가능
	}
	
    ...
    	@PostMapping("/save") // spring 4.3부터
	public String save(User user,BindingResult result,Model m) throws Exception{
    ...

 

b. <User.java> - 밑에 코드로 pattern 설정도 가능함. (깔끔)

@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;

 

3. 데이터 변환 - 이론

 

<PropertyEditor>

- 양방향 타입 변환 (String -> 타입, 타입 -> String)
- 특정 타입이나 이름의 필드에 적용 가능
- 디폴트 PropertyEditor : 스프링이 기본적으로 제공 (구글링하면 다 나옴)
- 커스텀 PropertyEditor : 사용자가 직접 구현. PropertyEditorSupport를 상속하면 편리
//(문자열 -> 문자열 배열)을 hobby 에만 적용, 구분자는 #
binder.registerCustomEditor(String[].class,"hobby", new StringArrayPropertyEditor("#"));

 

WebBindingInitializer@InitBinder

- 모든 컨트롤러 내에서의 변환 - WebBindingInitializer를 구현후 등록
- 특정 컨트롤러 내에서의 변환 - 컨트롤러에 @InitBinder가 붙은 메서드를 작성
	@InitBinder
	public void toDate(WebDataBinder binder) {
		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
		//스프링이 제공하는 customDateEditor를 이용해서 변환. String -> Date
		binder.registerCustomEditor(Date.class, new CustomDateEditor(df,false));
	}

 

ConverterConversionService

 

- Converter : 단방향 타입 변환 (타입A -> 타입B), PropertyEditor의 단점 개선(stateful -> stateless)
- Property가 인스턴스변수라서 stateful이다. -> 싱글톤을 사용할수가 없음

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

 

- Converter를 위에 코드처럼 만들고 ConversionService에 등록 해야함
- ConversionService : 타입 변환 서비스를 제공. 여러 Converter를 등록 가능
- WebDataBinder에 DefaultFormattingConversionService이 기본 등록
- 모든 컨트롤러 내에서의 변환 : ConfigurableWebBindingInitializer를 설정해서 사용
- 특정 컨트롤러 내에서의 변환 : 컨트롤러에 @InitBinder가 붙은 메서드를 작성
	@InitBinder
	public void toDate(WebDataBinder binder) {
		ConversionService conversionService = binder.getConversionService();

 

Formatter

- Formatter : 양방향 타입 변환, 바인딩할 필드에 적용(@NumberFormat, @DateTimeFormat)
public interface Formatter<T> extends Printer<T>, Parser<T> {
}

public interface Printer<T>{
	String print(T object, Locale locale); // Object(숫자)-> String
}
 
public interface Parser<T>{
 	T parse(String text, Locale locale) throws ParseException; // String -> Object(숫자)
}

 

<사용예시>

@DateTimeFormat(pattern="yyyy/MM/dd")
Date birth;

@NumberFormat(pattern="###,###")
BigDecimal salary;

 

WebDataBinder의 타입 변환은 밑에 3개가 일어나고 우선순위는 위에서 부터이다.

  • 커스텀 PropertyEditor
  • ConversionService
  • 디폴트 PropertyEditor

 

4. 데이터 검증

 

<Validater>

- 객체를 검증하기 위한 인터페이스.
- 객체 검증기(validator) 구현에 사용
- 이전에는 컨트롤러 메서드에서 검증을 함 -> 지저분해서 별도로 분리한 것
public interface Validator{
	// 이 검증기로 검증가능한 객체인지 알려주는 메서드
	boolean supports(Class<?> clazz);
	// 객체를 검증하는 메서드 - target : 검증할 객체, errors : 검증시 발생한 에러저장소
	void validate(@Nullable Object target, Errors errors);
}

 

[참고] interface Errors

void reject(String errorCode)는 객체 전체에 대한 에러
void rejectValue(String field, String errorCode); 는 필드(id, pwd등)에 대한 에러

 

Validator 수동 검증

Validator interface를 활용하여 UserValidator class를 구현하고 다음과 같이 생성해서 사용

 

<RegisterController.java>

	@PostMapping("/save") // spring 4.3부터
	public String save(User user,BindingResult result,Model m) throws Exception{
		
		// 수동 검증 - Validator를 직접 생성하고, validator()를 직접 호출
		UserValidator userValidator = new UserValidator();
		userValidator.validate(user, result); //BindingResult는 Errors의 자손
		
		// User객체를 검증한 결과 에러가 있으면, registerForm을 이용해서 에러를 보여줘야 함.
		if(result.hasErrors()) {
			return "registerForm";
		}

 

<UserValidator.java>

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) { 
		//타겟이 instanceof User인지 확인해야하지만 supports에서 하므로 필요없음
		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 자동 검증

- WebDataBinder binder에 Validator를 등록해주고 검증하려는 객체앞에 @Valid 붙혀주면 됨
- binder.setValidator(new UserValidator()); 
- 이거는 해당 컨트롤러 안에서만 동작
// UserValidator를 WebDataBinder의 validator로 등록
binder.setValidator(new UserValidator());​

 

<글로벌 Validator>

- 하나의 Validator로 여러 객체를 검증할 때, 글로벌 Validator로 등록
- 글로벌 Validator로 등록하는 방법
<annotation-driven validator="globalValidator"/>
<beans:bean id="globalValidator" class="com.fastcampus.ch2.GlobalValidator"/>

-글로벌 Validator와 로컬 Validator를 동시에 적용하는 방법 (setValidator 안됨)

binder.addValidator(new UserValidator());

 

MessageSource

- 다양한 리소스에서 메시지를 읽기 위한 인터페이스
public interface MessageSource{
	String getMessage(String code, Object[] args, String defaultMessage, Locale locale);
    String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException;
    String getMessage(MessageSourceResolvable resolvable,Locale locale) throws NoSuchMessageException;
}​


- 프로퍼티 파일을 메시지 소스로 하는 ResourceBundleMessageSource를 beans에 등록(servlet_context.xml)

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



- error_message.properties 파일을 하나 만들어야 함 (경로는 /src/main/resources/error_message.properties)

required= 필수 항목입니다.
required.user.pwd= 사용자 비밀번호는 필수 항목입니다.
invalidLenth.id= 아이디의 길이는 ~ 사이어야 합니다.
...

 

검증 메시지의 출력

- 스프링이 제공하는 커스턴 태그 라이브러리를 사용
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>​

- <form> 대신 <form:form>. 사용 (검증할 객체) 
<form:form modelAttribute="user">
// 위를 쓰면 아래와 같음
<form id="user" action="/ch2/register/save" method="post">

- <form:errors> 로 에러를 출력. path에 에러 발생 필드를 지정. (*은 모든 필드의 에러)
<form:errors path="id" cssClass="msg"/>
// 위에서 아래로 최종 변경됨
<span id="id.errors" class="msg"> 필수 입력 항목입니다. </span>​

 

[참고]

intellij에서 바로 스프링 프로젝트 만들면 번거러움 (sts에서 만든 프로젝트 import하는게 편함)

'Spring > 스프링의 정석' 카테고리의 다른 글

Ch. 03 Spring DI  (0) 2023.01.12
Ch. 03 Spring DI 흉내내기  (0) 2023.01.11
Ch. 02 Spring 예외처리  (0) 2023.01.09
Ch. 02 Spring 쿠키와 세션  (0) 2023.01.09
Ch. 02 Spring - Redirect와 Forward  (0) 2023.01.08

BELATED ARTICLES

more