본문 바로가기

백엔드 개발/SpringMVC

데이터 변환과 검증(타입 변환 추가 하는 법, BindingResult 이용)

1.WebDataBinder의 원리 (복습)

(0)Binding이란? 

요청의 파라미터를 컨트롤러 매소드의 인수와 연결 시키는 것을 말한다.

 

WebDataBinder는 요청의 파라미터를 Controller Method의 인수에 맞게 형변환을 시켜주고, 또 인수로 들어갈 수 있는 값인지 확인하는 장치이다. 해당 과정을 거쳐서 파라미터가 인수와 연결된다.

(1) 형 변환 

요청의 파라미터는 무조건 String이기 때문에 형 변환이 필요하다. 여기서는 MyDate라는 객체가 컨트롤러의 인수로 쓰였고, 객체의 인스턴스 변수로 binding 되어야 한다. (binding 될 대상은 Spring이 알아서 잡아준다.) 따라서 "2021"을 int 2021로 바꿔주는 역할을 한다. 만약 이 과정에서 에러가 발생하면, (year = "Happy"를 넣어서 변환 실패 등) 에러에 대한 정보를 BindingResult에 넣는다. 

(2)데이터 검증 

데이터 검증은 해당 변수의 값으로 들어가기에 말이 되는가?를 체크하는 파트이다. month에는 1~12까지의 값만 들어갈 수 있다. 근데 만약에 month에 "31"이 들어가 있다면 WebDataBinder는 데이터 검증 과정에서 나온 에러의 정보를 BindingResult에 기록할 것이다. 만약 에러 없으면 그대로 Binding(요청의 파라미터는 인수에 들어갈 수 있게되어 사용된다.)

 

Spring이 WDB를 통해 형 변환과 데이터 검증을 기본으로 해주지만, 내장된 기능을 벗어나는 범주의 일처리는 못 할 수있다. 우리가 이번 포스팅과 다음 포스팅에서 만들 것은 WebDataBinder가 형 변환이나 데이터 검증을 못하는 경우에, 우리가 직접 형 변환 매소드나 데이터 검증 매소드를 만들어 Binder에 등록하여 WebDataBinder가 자기 역할을 다시 잘 해낼 수 있게 하는 것이다. 

2.Register Controller에 변환 기능 추가하기 - 실습

(0) insta,facebook,kakao 체크 박스에 체크 친 것이 String으로 어떻게 변환 되었을까? 

sns 의 자료형이 String이다.

위를 보면 체크박스에 찍은 값들이 String으로서 변환 된 것을 알 수 있다. Spring은 처음에 클라이언트가 체크한 값들을 String 배열로서 받는다. 그리고 이걸 다시 일렬로 나열된 String으로 변환해주는 것이다. 이건 Spring이 자동으로 해준다.

(1) User 클래스에서 birth의 자료형을 String이 아닌 Date로 바꾸어보자.

     (**Date 클래스는 Java에서 제공하는 클래스로 년월일 시간을 반환한다.)

//UserClass에서 변환
public class User {
	
	private String id;
	private String pwd;
	private String name;
	private String email;
	private Date birth; 
	private String[] sns;
	private String[] hobby;
	
	

	public String[] getHobby() {
		return hobby;
	}
	public void setHobby(String[] hobby) {
		this.hobby = hobby;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getPwd() {
		return pwd;
	}
	public void setPwd(String pwd) {
		this.pwd = pwd;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public Date getBirth() {
		return birth;
	}
	public void setBirth(Date birth) {
		this.birth = birth;
	}
	public String[] getSns() {
		return sns;
	}
	public void setSns(String[] sns) {
		this.sns = sns;
	}	
	@Override
	public String toString() {
		return "User [id=" + id + ", pwd=" + pwd + ", name=" + name + ", email=" + email + ", birth=" + birth 
				+ ", hobby="+ Arrays.toString(hobby) + "sns="+ Arrays.toString(sns) + "]";
	}
}

그리고 서버를 돌려보자. 회원가입 창을 열겠다.

만약 이 형식으로 생년월일을 적으면 제대로 돌아간다. 왜냐하면 yyyy/MM/dd 형식은 Spring이 Date 클래스의 변수로 변환 가능한 변주에 있기 때문이다. 

하지만 다음과 같이 적으면 에러가 난다.

이유는 yyyy-MM-dd 형식은 Spring이 변환할 수 있는 기본 범주 내에 없기 때문이다. 

따라서 이를 변환하는 매소드를 우리가 커스터마이징 해서 만들어줘야 한다.

(2) 에러 나더라도 끝까지 출력하는 방법은?

컨트롤러에 우리가 binding 하려는 객체 옆에 BindingResult 객체를 인수로 넣는다.

public String save(User user,BindingResult result ,Model m) throws Exception {
		//result 객체를 찍으면 어디서 에러가 났고 총 몇개의 에러가 났는지 알려준다.
        System.out.println("result="+result);
		
		if(!isValid(user)) {
			String msg = URLEncoder.encode("id를 잘못 입력하셨습니다.", "utf-8");
			
			m.addAttribute("msg",msg);
			return "redirect:/register/add";

이렇게 넣고 프로그램을 돌리면, 바인딩 실패한 변수에는 Null 값이 들어간 채로 결과가 잘 출력된다.

잘 출력되는 이유는 BindingResult를 컨트롤러 매소드의 인수로 넣는 것의 의미는 다음과 같다.

해당 매소드가 일 처리할 때, 바인딩 오류가 있더라도 에러 페이지로 넘어가지 않도록 하는 것이다.

또한 바인딩 결과 자체를 컨트롤러한테 주고, 컨트롤러가 바인딩 에러를 처리하도록 권한을 주는 것이다.

(3) 문자열을 부분 부분 짤라서 Date의 변수로 넣어주는 변환 매소드를 만들자. 

이제 우리는 yyyy-MM-dd 패턴의 문자열을 yyyy , MM, dd로 짤라서 Date의 인스턴스 변수로 넣어주는 변환 매소드를 만들어보겠다. 먼저 코드를 보자.

	//Binder로서 해당 매소드를 등록하는 어노테이션
	@InitBinder

	public void toDate(WebDataBinder binder) {
		//년월일 SimpleDateFormat 객체 생성
		//해당 객체는 문자별로 뜻하는 숫자가 달라서 이걸 이용해 패턴 만들 수 있다.
		// y가 들어가면 년도를 나타낸다. M은 월이다. (1~12만 가능), d는 날짜이다.(1~31만 가능)
		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
		
		// binder에 새로운 커스텀데이트에디터로 등록한다.
		// 등록의 첫번째 인수가 바인딩할 객체의 클래스정보, 커스텀데이트 에디터 객체 생성이다.
		// CustomDateEditor의 인수 첫번째는 새 바인딩 방법, 두번째는 공백 허용 여부이다. 
		binder.registerCustomEditor(Date.class, new CustomDateEditor(df,false));
	}

잘 된다.

(4) Register에 Hobby 추가 (String으로 받은 것을 String[]로 짜르고 싶다.)

resgisterForm과 User에 Hobby 추가

   <!--registerForm.jsp 내부-->
   <label for="">취미</label>
    <input class="input-field" type="text" name="hobby" placeholder="취미">
	// User 내부
    private String[] hobby;

(5) String을 String []로 변환하는 매소드 추가 

hobby는 input 하나로 들어오기 때문에 만약 다수의 취미를 적는다 해도, 배열[0]에 다 들어가서, 무슨 취미 가졌는지 선호도 조사 같은 것을 하기 어려울 것이다. 이를 각각 때어내서 String 배열 다른 칸에도 들어가도록 해보자.

먼저 취미들 사이에 #을 넣어 서로 구분하여 입력한다고 가정하자. (이렇게  입력하도록 클라이언트 유도)

그러면 이번에는 #을 구분자로 해서 String을 배열로 때어내는 변환 매소드를 만들면 된다. 아까 만든 변환 매소드 안에 해당 변환 기능을 추가하자. 

	// Binder로서 해당 매소드를 등록하는 어노테이션
    // 해당 어노테이션으로 등록된 바인더는 해당 컨트롤러 내부에서만 적용이 된다. 
	@InitBinder
	// 매소드안에 다수의 변환 기능을 넣을 수 있다.
	public void toDate(WebDataBinder binder) {
		SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); 
		binder.registerCustomEditor(Date.class, new CustomDateEditor(df,false));
		
		//#을 기준으로 String을 배열로 짤라 넣는 객체
		binder.registerCustomEditor(String[].class, new StringArrayPropertyEditor("#"));
	}

만약 객체 내의 어떤 필드에 대해서만 해당 변환 기능이 돌아가도록 특정하고 싶다면 

binder.registerCustomEditor(String[].class, "hobby", new StringArrayPropertyEditor("#"));

이렇게 적으면 된다. 

이러면 다른 입력값들은 아무리 많이 적어도 하나의 문자열에 다 바인딩 되고, 

hobby란 변수만 #을 기준으로 String 배열로 짤려서 binding 된다.

(추가) Date 변환을 빠르게 하는 방법 (이것밖에 변환할 게 없어서 변환 매소드 만들기 번거로운 경우)

그냥 객체의 인스턴스 변수 선언부분에 어노테이션을 써서 바로 변환할 수도 있다.

@DateTimeFormat(pattern="우리가 등록할 패턴") 

public class User {
	
	private String id;
	private String pwd;
	private String name;
	private String email;
	//String을 날짜, 날짜를 String으로 양뱡향 변환 가능한 어노테이션
	@DateTimeFormat(pattern="yyyy-MM-dd")
	private Date birth; 
	private String[] sns;
	private String[] hobby;
    
    /...
    }

3. Spring의 타입 변환 기능들 정리 (Property Editor, converter, Formatter)

(1) Property Editor

PropertyEditor는 종류가 많다. 우리가 쓴 StringArrayPropertyEditor도 그 종류 중 하나이다. 

PropertyEditor 중에는 스프링이 기본적으로 제공하는 디폴트PropertyEditor가 존재한다. 우리가 쓴 String을 적절히 짤라 String[]안에 집어넣는 StringArrayPropertyEditor도 그 중 하나이다. 

디폴트 PropertyEditor의 목록은 다음과 같다. 외울 필요 없고, 상황에 따라 적절히 이용하자.

사용자가 직접 구현할 수도 있다. 이걸 CustomPropertyEditor라고 한다. 근데 아무것도 없는 것에서 처음부터 다 구현하는 것은 힘들다. 그래서 보통 PropertyEditorSupport를 상속하여 이 부모클래스의 매소드를 오버로딩하여 많이 구현한다. 

 PropertyEditor는 양방향 타입 변환기이다. 

하나의 Editor 객체가 String을 A자료형으로 바꿀 수 있고, A 자료형을 String으로 바꿀 수도 있다.

 

모든 컨트롤러에서 구현한 PropertyEditor가 적용되어 값이 변환 되도록 하고 싶다면, WebBindinfInitializer라는 것을 구현 후 등록해야 한다. 

특정 컨트롤러 내부에서만 구현한 PropertyEditor가 적용되도록 하고 싶다면 방법은 다음과 같다. 

먼저 PropertyEditor객체가 포함된 변환 매서드를 컨트롤러 내부에 작성하고 이 매서드에 @InitBinder 어노테이션을 붙인다.

(2) Converter

Converter는 단방향 변환기이다. 하나의 객체를 만들어 놓으면 이 객체는 String -> 다른 타입 혹은 다른 타입 -> String 한 방향으로 밖에 변환하지 못한다. 따라서 PropertyEditor와 같은 성능을 내게 하고 싶다면, Converter 2개를 써야한다. 

현업에서는 Converter를 많이 사용한다. 그 이유는 Converter는 PropertyEditor와 달리 iv(인스턴스 변수)를 쓰지 않기 때문이다. PropertyEditor는 인스턴스 변수를 쓰는데, 이는 만드는 객체마다 안의 변수 값이 달라진다는 것을 의미한다. 이러면 싱글톤을 실현할 수 없다. 반면 인스턴스 변수가 없는 Converter는 싱글톤을 실현할 수 있다. 

(싱글톤이란, 같은 요청 마다 요청을 처리할 객체를 새로 만들지 않고, 그 요청을 해결할 수 있는 객체를 하나 만들고 같은 요청이 들어올 때마다 그 객체를 재사용 하는 것이다.)

 

Spring의 WebDataBinder에는 DefaultFormattingConversionService라는 Converter들이 기본으로 등록되어 있다.

하나의 Converter를 이용하는 매소드를 만들고, 이 매소드가 모든 컨트롤러에 적용되게 하고 싶다면, 

ConfigurableWebBindingInitalizer를 설정해서 사용 해야한다.

만약 특정 컨트롤러 내부에서만 적용되게 하고 싶다면, PropertyEditor와 같이, 해당 컨트롤러 내부에 변환 매소드 만들고 그 매소드에 @InitBinder 어노테이션 붙이면 된다. 

(3) Formatter

아까 Date Birth로 요청의 파라미터 값을 변경할 수 있는 두 가지 방법이 있었다. 하나는 매소드 내부에 PropertyEditor를 쓰는 방법 이었고, 하나는 바인딩 하고자 하는 필드에 어노테이션 적용하여 바로 바꾸는 것이었다. 

Formatter는 여기서 후자의 방법을 말한다. 

Formatter도 PropertyEditor와 마찬가지로 양방향으로 변환한다. (String -> 다른 자료형, 다른 자료형 -> String)

 

@NuberFormat 은 숫자를 String으로, String을 숫자로 변환한다. 

@DateTimeFormat 은 날짜를 String으로, String을 날짜로 변환한다.

해당 어노테이션의 ()안에 변환 해야하는 패턴을 적어넣을 수 있다. 

 

예시는 다음과 같다. 

// yyyy는 년으로, MM은 월로, dd는 일로 변환하여 계산하라. 
// User 클래스 내의 Birth 인스턴스 선언 위에 적음
@DateTimeFormat(pattern = "yyyy/MM//dd")
Date Birth;


// ###,###으로 들어온 값을 , 빼고 int로 변환하라
@NumberFormat(pattern = "###,###")
BigDecimal salary;

(4)적용의 우선순위

a. 모든 컨트롤러에 적용되는 변환 매소드 vs 특정 컨트롤러 내부의 변환 매소드 

예외처리 때와 마찬가지로, 변환을 해야하는 곳에서 가까운 쪽의 매소드가 우선적으로 작동한다. 

특정 컨트롤러 내에서 바인딩을 위한 변환이 필요한 경우, 그 컨트롤러 내부에 @initBinder로 등록된 변환 매소드가 있고, 그 녀석이 변환을 할 수 있으면, 이녀석이 우선이다. 컨트롤러 내부에서 해결이 안될 때, 모든 컨트롤러에 적용되는 변환 매소드를 이용한다.

b. 변환 매소드 내의 변환 기능을 하는 객체들 사이의 우선순위

어떤 컨트롤러 내부에 바인딩을 위한 변환이 필요하다고 해보자. 

먼저 Spring은 CustomPropertyEditor 중에 이 변환을 해결할 수 있는 녀석이 있는지 찾는다. 

CustomPropertyEditor는 개발자가 직접 구현한 PropertyEditor이다. 

여기서 해결이 안나면, 그 다음엔 Spring에 등록된 Converter인 CoversionService를 돌아다닌다. 

이 녀석들 중에도 해결할 녀석이 없으면 DefaultPropertyEditor(Spring에 등록된 PropertyEditor) 중에 해결할 녀석이 있나 찾아본다. 

따라서 우선 순위는 

1. 커스텀 PE, 2. ConversionService, 3.디폴트PE 순이다.

4. 직접 해보기 

(1) 커스텀 PE, Formatter 직접 쳐보기

	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("#"));

	}

(2) Salary 추가 해보기 

	@NumberFormat(pattern="###,###")
	private int Salary;
	

	public int getSalary() {
		return Salary;
	}
	public void setSalary(int salary) {
		Salary = salary;
	}
    <label for="">봉급</label>
    <input class="input-field" type="text" name="Salary" placeholder="###,###">
<h1>Salary=${user.salary}</h1>