Ch. 02 Spring DispatcherServlet, 데이터변환과 검증
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));
}
Converter와 ConversionService
- 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 |