관리 메뉴

Just Do it

스프링 WebDataBinder의 데이터 변환과 검증 본문

신입 개발자가 되기 위해 공부했던 독학 자료들/Spring

스프링 WebDataBinder의 데이터 변환과 검증

Seojoo21 2022. 2. 11. 20:32

0. WebDataBinder

   @RequestMapping("/getYoilMVC5")
    public String main(@ModelAttribute MyDate date, BindingResult result) {

- 매개변수의 참조형으로 MyDate가 들어와서 아래와 같이 쿼리스트링으로 값이 입력되었을 때, 이 입력된 값이 MyDate의 인스턴스에 저장된다.  

 

http://localhost/ch2/getYoilMVC5?year=2021&month=10&day=8 

 

쿼리 스트링으로 입력된 값의 반환 타입은 String 이고 MyDate의 인스턴스에 저장될 때의 반환 타입은 int 일 때, 타입 변환을 도와주는 것이 바로 WebDataBinder이다.

 

- WebDataBinder가 하는 일은 두 가지이다. 

 

1) 타입 변환

2) 데이터 검증 (Validation) 

 

- WebDataBinder는 입력된 값에 대해 먼저 타입 변환을 하고 그다음 데이터 검증을 하는데, 결과나 에러가 있으면 BindingResult에 저장(Binding)한다.

 

-BindingResult에 저장된 값은 Controller에 넘겨줄 수 있고, Controller는 BindingResult에 저장된 값을 가지고 필요한 처리를 할 수 있다. 이때 BindingResult는 반드시 Binding 할 객체의 바로 뒤에 와야한다.  

 

-그리고 BindingResult는 아래처럼 예외처리 메서드 내 매개 변수로 넣어서 BindingResult에 저장된 값이 필요할 경우 사용할 수 있다. 

	@ExceptionHandler(Exception.class)
	public String catcher(Exception ex, BindingResult result) {
		// ex.printStackTrace(); 예외가 왜 발생했는지 스프링 콘솔 창에서 보고 싶으면 printStackTrace()를 쓰면 된다. 
		return "yoilError"; // 예외 발생 시 보여줄 view 폴더 내 파일 
	}

 

1. WebDataBinder의 타입 변환

 

- 스프링이 알아서 변환하지 못하는 것들은 타입 변환에서 에러가 발생한다. 그래서 이 에러를 해결하기 위해서는 변환기를 등록해야한다.

예) String으로 입력된 날짜를 Data 타입으로 바꿀 때 (날짜 형식은 굉장히 다양해 스프링에서 자동으로 변환하지 못한다.) 

 

1.1 PropertyEditor를 이용한 타입 변환 (양방향 타입 변환) 

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

- PropertyEditor를 이용한 타입 변환 방법에는 변환 메서드를 이용하는 방법과 어노테이션을 이용하는 방법이 있다. 

* 날짜와 숫자는 굉장히 많이 사용하기 때문에 별도의 어노테이션이 있다 (숫자: @NumberFormat, 날짜: @DateTimeFormat)

 

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

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

 

-  특정 타입이나 이름의 필드에도 적용 가능하다. 

예) 

@InitBinder
public void toDate(WebDataBinder binder) {
    // "hobby"라는 필드에만 아래 변환을 적용한다.  
	binder.registerCustomEditor(String[].class, "hobby", new StringArrayPropertyEditor("#"));

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

참고: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/beans/propertyeditors/package-summary.html

 

org.springframework.beans.propertyeditors (Spring Framework 5.3.15 API)

Class Summary  Class Description ByteArrayPropertyEditor Editor for byte arrays. CharacterEditor Editor for a Character, to populate a property of type Character or char from a String value. CharArrayPropertyEditor Editor for char arrays. CharsetEditor Ed

docs.spring.io

 

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

 

예 1-1) 변환 메서드 이용 

- 변환 메서드에는 @InitBinder 애너테이션을 붙여줘야한다.

- 변환 메서드는 컨트롤러 내에서만 적용된다.  

//변환메서드 

@InitBinder
public void toDate(WebDataBinder binder) {
	SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 
    // 스프링이 제공하는 CustomDateEditor를 이용해서 변환: String -> Date
    //registerCustomEditor() = 변환기 
    binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false)); // yyyy-MM-dd 형식으로 입력되는 문자 타입의 날짜를 Date 타입으로 변환. 
    binder.registerCustomEditor(String[].class, new StringArrayPropertyEditor("#")); // #을 구분자로 입력되는 문자열을 배열로 나눈다.  
}

예 1-2)  필드에 직접 어노테이션 이용 

public class User{
	private String id;
    private String pwd;
    private String name;
    private String email;
    @DateTimeFormat(pattern="yyyy/MM/dd") // 입력된 연월일을 "yyyy/MM/dd" 형식으로 바꿔준다.
   	private Date birth;
    private String[] sns;
    private String[] hobby;
}

1.2 Converter와 ConversionService를 이용한 타입 변환 (단방향 타입 변환) 

- Converter: 단방향 타입 변환 (타입 A -> 타입 B). PropertyEditor의 단점을 개선 (stateful -> stateless) 

- 아래 예시와 같이 Converter를 만든 후 ConversionService로 등록을 해야한다.

 

예) 

public class StringToStringArrayConverter implements Converter<String, String[]> {
	@Override
    public String[] convert(String source) {
    	return source.split("#"); // #을 구분자로 하는 문자 String을 문자 배열 String[] 로 바꾼다.
    }

}

 

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

* WebDataBinder에 DefaultFormattingConversionService가 기본적으로 등록되어있다. 

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

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

 

1.3 Formatter (양방향 타입 변환) 

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

- 바인딩할 필드에 적용한다. (숫자: @NumberFormat, 날짜: @DateTimeFormat)

* 1.1의 PropertyEditor의 어노테이션 이용 방법과 유사하다. 

@DateTimeFormat(pattern="yyyy/MM/dd") // yyyy/MM/dd 로 들어오는 String을 Date로 변환 
Date birth;

@NumberFormat(pattern="###,###") // ###,### 로 들어오는 String을 BigDecimal로 변환
BigDecimal salary;

 

** WebDataBinder에 타입 변환기를 등록할 수 있는 방법은 아래와 같고, 처리 우선 순위도 아래 순서와 같다. (1->2->3)

1) 커스텀 PropertyEditor

2) 디폴트 PropertyEditor

3) ConversionService

 

2. WebDataBinder의 데이터 검증 

 

2.1 Validator

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

public interface Validator {
	// 이 검증기로 검증 가능한 객체인지 알려주는 메서드
    boolean supports(Class<?> clazz>;
    // 객체를 검증하는 메서드 - target: 검증할 객체, errors: 검증시 발생한 에러 저장소
    void validate(@Nullable Object target, Errors errors);
}

Validator 구현 예)

public class UserValidator implements Validator {
	@Override
    public boolean supports(Class<?> clazz) {
    	return User.class.isAssignableFrom(clazz) // clazz가 User 또는 그 자손인지 확인. isAssignableFrom 대신 equals를 사용해도 괜찮다.
     }
    @Override
    public void validate(Object target, Errors errors) {
    	User user = (User)target; 
        // Object타입의 target을 User로 형변환. 위에서 이미 User 또는 그 자손인지 확인했으므로 instanceof를 쓸 필요가 없다.
        
        String id = user.getId(); 
        // id를 검사하기 위해 id값을 가져온다.
        
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id", "required");
        // id가 비어있거나 공백이면 id 필드에 required라는 에러 코드를 저장한다. 
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");
        // pwd가 비어있거나 공백이면 pwd 필드에 required라는 에러 코드를 저장한다. 
        
        if (id==null || id.length()<5 || id.length() > 12) {
			errors.rejectValue("id", "invalidLength");   
        //id가 비어있거나 id의 길이가 5보다 작거나 12보다 길면 invalidLength라는 에러 코드를 저장한다.     
        } // 먼저 에러코드를 저장하고 나중에 에러코드에 맞는 메세지를 작성하여 출력한다. 
    }
}

 

Validator를 이용한 검증 - 수동 예)

//컨트롤러에서 객체검증기를 불러온다.

@PostMapping("/register/add")
public String save(Model model, User user, BindingResult result) {

	UserValidator userValidator = new UserValidator();
	userValidator.validate(user,result); 
    // validator로 user 객체를 검사하여 에러를 result로 반환한다. 
    // 여기서 result는 위의 save 메서드의 매개변수 안에 있는 result이다. BindingResult는 Errors의 자손이다.

	if(result.hasErrors()) { 
		return "registerForm"; // 에러가 있으면 registerForm내의 <form:errors> 코드를 통해 화면에 보여준다.
	}
}

 

Validator를 이용한 검증 - 자동 예)

*검증할 객체 앞에 @Valid 애너테이션을 붙일 때 빨간 밑줄이 생기는 이유:

@Valid 애너테이션은 스프링이 아닌 자바 표준 애너테이션이라서 import 되지 않는다. Maven Repo에서 가져와서 써야한다. 

1) 아래 사이트에 가서 Maven 칸에 있는 코드 복사  

https://mvnrepository.com/artifact/javax.validation/validation-api/2.0.1.Final

2. pom.xml에 복사해온 코드를 복사해 넣고 저장한다.

3. pom.xml이 바뀌었으니 프로젝트파일 우클릭->Maven->Update Project 클릭해서 업데이트 해준다. 

4. 작업하던 컨트롤러 파일로가서 import 하면 @Valid를 쓸 수 있다.

 

//1. Validator 등록 
@InitBinder
public void toDate(WebDataBinder binder) {
	SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
    binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
    
    binder.setValidator(new UserValidator()); // validator를 WebDataBinder에 등록
    
    List<Validator> validators = binder.getValidators();
    System.out.println("validators=" + validators);
}


@PostMapping("/register/add")
//2. 검증할 객체 앞에 @Valid를 붙인다.  
public String save(Model model, @Valid User user, BindingResult result) {

	if(result.hasErrors()) { 
		return "registerForm"; // 에러가 있으면 registerForm내의 <form:errors> 코드를 통해 화면에 보여준다.
	}
}

 

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

 

*글로벌 Validator로 등록하는 방법: servlet-context.xml 파일 내에 아래 bean을 등록한다. 그럼 검증할 객체 앞에 @Valid를 붙여 사용할 수 있다. 

<annotation-driven validator="globalValidator"/>
<beans:bean id="globalValidator" class="com.xxx.xxxx.ch2.GlobalValidator"/> 
// class="패키지명.클래스이름(glovalValidator로 사용하기 위해 만든 클래스)" 를 id="globalValidator"로 등록한다.

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

// 1.컨트롤러 안에 @InitBinder가 붙은 메서드를 만든다. 
@InitBinder
public void toDate(WebDataBinder binder) {
	SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd");
    binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
    
    binder.addValidators(new UserValidator()); // 2. 로컬 Validator를 글로벌 Validator로 등록한다.

 

2.2 MessageSource

- 다양한 리소스(파일, 배열 등)에서 메시지를 읽기 위한 인터페이스.

- 이 인터페이스를 이용하여 사용자 화면에 에러 코드에 의한 에러 메세지를 띄울 수 있다. 

- 프로퍼티 파일을 메시지 소스로 하는 ResourceBundleMessageSource를 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 -->
                //위에 erro_message 이 이름은 프로퍼티파일의 이름과 동일해야한다. 
			</beans:list>
		</beans:property>
		<beans:property name="defaultEncoding" value="UTF-8"/>
	</beans:bean>

 

- 에러 코드에 대한 메세지가 있는 프로퍼티 파일(.properties)을 만들어줘야하고 이 프로퍼티 파일이 화면에 출력된다. 

 

2.2 검증메세지의 출력

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

<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>

 

- <form> 대신 <form:form>을 사용한다. 

<form:form modelAttribute="user"> // 큰 따옴표("")사이에는 검증할 객체가 온다.

 

- <form>태그 안에 <form:errors>로 에러를 출력한다. path에 에러 발생 필드를 지정한다. (*은 모든 필드의 에러를 의미한다.) 

<form:errors path="id" cssClass="msg"/>
//user 객체의 id 필드에 해당하는 메세지를 보여준다.