Maven Dependencies → spring-webmvc-5.0.7.RELEASE.jar
📑 소스 파일 위치: org.springframework.web.servlet → DispatcherServlet.java
📑 기본 전략: org.springframework.web.servlet → DispatcherServlet.properties
📑 주요 메서드(DispatcherServlet.java)
✅ void initStrategies(ApplicationContext context): 기본 전략 초기화
→ ✅ void doService(HttpServletRequest request, HttpServletResponse response): doDispatch() 호출
→ ✅ void doDispatch(HttpServletRequest request, HttpServletResponse response): 실제 요청 처리
→ ✅ void ProcessDispatchResult(HttpServletRequest, HttpServletResponse response, HandlerExceptionChain): 예외가 발생했는지 확인하고 발생하지 않았다면 render() 호출
→ ✅ void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response): 응답결과 생성 및 전송
: 양방향 타입변환(String ↔ 타입), stateful
✔️ 특정 타입이나 이름의 필드에 적용 가능(지정하지 않으면 문자열로 바꾸는 변환에 일괄적용)
binder.registerCustomEditor(String[].class, "hobby", new StringArrayPropertyEditor("구분자"));
✅ 디폴트 PropertyEditor: 스프링에서 기본적으로 제공
✅ 커스텀 PropertyEditor: 사용자 직접구현, PropertyEditorSupport를 상속하면 편리
✴️ 모든 컨트롤러 내에서의 변환: WebBindingInitializer를 구한 후 등록
✴️ 특정 컨트롤러 내에서의 변환: 컨트롤러에 @InitBinder가 붙은 메서드 작성
@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("#")); //# 구분자
}
✅ Converter: 단방향 타입변환(타입A->타입B)
✔️ PropertyEditor의 단점개선(stateful -> stateless_인스턴스 변수 사용하지 않음)
public class StringToStringArrayConverter implements Converter<String, String[]>{
@Override
public String[] convert(String source){
//String -> String[]
return source.split("#");
}
}
✅ ConversionService: 타입변환 서비스 제공, 여러 Converter 등록가능
✔️ WebDataBinder에 DefaultFormattingConversionService가 기본등록
✴️ 모든 컨트롤러 내에서의 변환: ConfigurableWebBindingInitializer를 설정하여 사용
✴️ 특정 컨트롤러 내에서의 변환: 컨트롤러에 @InitBinder가 붙은 메서드 작성
: 양방향 타입변환(String ↔ 타입)
✔️ 바인딩할 필드에 적용: 에너테이션 사용 @NumberFormat, @DateTimeFormat
// User.java
@DateTimeFormat(pattern="yyyy-MM-dd")
private Date birth;
@NumberFormat(pattern="###,###")
BigDecimal salary;
public interface Formatter<T> extends Print<T>, Parser<T>{
}
public interface Printer<T>{
// Object -> String
String print(T object, Locale locale);
}
public interface Parser<T>{
// String -> Object
T parse(String text, Locale locale) throws ParseException;
}
private Date birth;
private String[] sns;
: 객체를 검증하기 위한 인터페이스, 객체 검증기 구현에 사용
public interface Validator{
// 이 검증기로 검증 가능한 객체인지 알려주는 메서드
boolean supports(Class<?> clazz);
// 객체를 검증하는 메서드
// target: 검증할 객체, error: 검증 시 발생한 에러저장소
void calidat(@Nullable Object target, Error errors);
}
✴️ interface Errors
✅ void reject(String errorCode); : 객체 전제에 대한 에러
✅ void rejectValue(String field, String errorcode); : (iv, 에러)
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");
// }
// 비었거나 공백이면 "id"라는 인스턴스 변수(iv)에 "required"라는 에러코드로 저장
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "required");
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
// 5~11 사이 길이가 아니면 "invalidLength"(유효하지 않은 길이)
if(id==null || id.length() < 5 || id.length() > 12) {
errors.rejectValue("id", "invalidLength");
}
}
}
: 컨트롤러 메서드 내 검증코드를 분리
@PostMapping("/register/save")
public String save(Model model, User user, BindingResult result){
UserValidator userValidator = new UserValidator():
userValidator.validate(user, result); // validator로 검증
if(result.hasErrors()){ // 에러가 있으면
return "registerForm";
}
}
✔️ @InitBinder
을 이용하여 Validator 등록
binder.setValidator(new UserValidator());
✔️ 검증방법: 검증할 객체 앞에 @Valid
붙여줌
✔️ 자동검증을 이용하기 위해 maven dependecny를 pom.xml에 등록
<!-- https://mvnrepository.com/artifact/javax.validation/validation-api --> <dependency> <groupId>javax.validation</groupId> <artifactId>validation-api</artifactId> <version>2.0.1.Final</version> </dependency>
// RegisterController.java
// RegisterController 안에서만 동작
@InitBinder
public void toDate(WebDataBinder binder){
simpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
binder.registerCustomEditor(Data.class, new CustomDateEditor(df, false));
binder.setValidator(new UserValidator()); // validator를 WebDataBinder에 등록
List<Validator> validators = binder.getValidators();
System.out.println("validators="+validators);
}
@PostMapping("/register/save")
public String save(Model model, @Valid User user, BindingResult result){
if(result.hasErrors()){ // 에러가 있으면 회원가입 폼으로 돌아감
return "registerForm";
}
}
: 하나의 Validator로 여러 객체를 검증할 때, 글로벌 Validator로 등록
✅ 글로벌 Validator로 등록하는 방법
✔️ servlet-context.xml
<annotation-driven validator="globalValidator"/> <beans:bean id="globalValidator" class="com.fastcampus.ch2.GlobalValidator"/>
✅ 글로벌 Validator와 로컬 Validator를 동시에 적용하는 방법
✔️ setValidator이 아닌 addValidator
를 이용하여 글로벌 validator에 로컬 validator 추가 등록
@InitBinder
public void toDate(WebDataBinder binder){
simpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
binder.registerCustomEditor(Data.class, new CustomDateEditor(df, false));
// binder.setValidator(new UserValidator());
binder.addValidators(new UserValidator()); // validator를 WebDataBinder에 등록
List<Validator> validators = binder.getValidators();
: 다양한 리소스(파일, 배열...)에서 메시지를 읽기 위한 인터페이스
✔️ 특정 코드를 주면 그 코드에 대한 메시지 반환
public interface MessageSource{ String getMaesssage(String code, Object[] args, String defaultMessage, Locale locale); String getMaesssage(String code, Object[] args, Locale locale) throws NoSuchMessageexception; String getMaesssage(MessageSourceResolvable resolbable, Locale locale) throws NoSuchMessageexception; }
✅ Object[] args
: 메시지에 사용될 값들
ex) {"5", "11"}, error_message.properties | 아이디의 길이는 {1}~{2}사이어야 합니다!
아이디의 길이는 5~11사이어야 합니다!
✅ String defaultMessage
: properties 파일에서 해당하는 메시지코드를 찾지 못했을 때 보여줄 메시지, 기본값 Null
✅ Locale locale
: 지역정보
✴️ 디폴트: error_message.properties
✴️ 지역에 따라: error_message_ko.properties, error_message_en.properties...
✔️ 프로퍼티 파일을 메시지 소스로 하는 ResourceBundleMessageSource
를 등록
🥕 servlet-context.xml에서 오류페이지나 view-controller, resources와 같이 시행되지않음. 아직 해결방법을 찾지 못합 다른 bean은 정상작동되지만 추가적인 설정은 작동되지 않음.
servlet-context.xml
<beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <beans:property name="basenames"> <beans:list> <!-- /src/main/resources/error_message.properties --> <beans:value>error_message</beans:value> </beans:list> </beans:property> <beans:property name="defaultEncoding" value="UTF-8"/> </beans:bean>
✴️ error_message.properties
/src/main/resources/error_message.properties 파일 생성(파일 이름은 <beans:value>와 같게 설정, 확장자 properties)
: registerForm.jsp
✔️ 스프링이 제공하는 커스텀 태그 라이브러리 사용
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
✔️ <form>
대신 <form:form> 사용
< form action="<c:url value="/register/save"/>" method="post">
➡ <form:form modelAttribute="user">
출력: <form id="user" action="/ch2/register/save" method="post">
✔️ <form:error>로 에러 출력, path에 에러 발샐 필드 지정(*은 모든 필드의 에러)
<form:errors path="id" cssClass="msg"/>
출력: <span id="id.errors" class="msg">필수 입력 항목입니다.</span>
참고) 자바의 정석 | 남궁성과 끝까지 간다