[Spring] 05. DispatcherSevlet/데이터의 변화과 검증

Hyeongmin Jung·2023년 6월 4일
0

Spring

목록 보기
5/17

🐣 DispatcherSevlet

Spring MVC의 요청 처리과정

DispatcherSevlet의 소스분석

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): 응답결과 생성 및 전송


🐣 데이터의 변화와 검증

🪄 타입 변환

  • 우선순위: 커스텀 PE → ConversionService → 디폴트 PE

❶ PropertyEditor

: 양방향 타입변환(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와 ConversionService

✅ 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가 붙은 메서드 작성

❸ Formatter

: 양방향 타입변환(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;
}

실습 | RegisterController에 변환기능 추가

private Date birth;
private String[] sns;


🪄 데이터 검증

Validator

: 객체를 검증하기 위한 인터페이스, 객체 검증기 구현에 사용

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";
    }
}

Global Validator

: 하나의 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();

MessageSource

: 다양한 리소스(파일, 배열...)에서 메시지를 읽기 위한 인터페이스
✔️ 특정 코드를 주면 그 코드에 대한 메시지 반환

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>


참고) 자바의 정석 | 남궁성과 끝까지 간다

0개의 댓글