[Spring #13] 데이터의 변환과 검증

HJoo·2023년 1월 19일
0

TodayStudy

목록 보기
51/111
post-thumbnail

데이터의 변환과 검증의 필요성

  • 클라이언트가 서버에 요청을 보낼 때 GET 방식이라면, 데이터가 URL + ?파라미터&파라미터&.. 의 방식으로 보내진다
  • 데이터가 Parameter Map 방식으로 정리가 되는데, 이 값은 컨트롤러에서 매개변수로 생성한 객체로 전달되고 정리되어 사용된다

  • 위 그림에서 요청으로 보내진 year=2021&month=10&day=10이 Map 형태로 보내지고,
    컨트롤러의 매개변수로 생성된 객체인 MyDate 의 멤버 값들로 각각 전달된다
  1. 타입 변환
    기본적으로 요청 url에 들어있는 value들은 String이다
    객체의 맴버 타입에 맞게 변환되어 저장되어야 하기 때문에 데이터의 변환이 필요하다
    위 그림에서는 String이 int로 변환되었다
  2. 데이터 검증
    url로 들어온 value들이 객체의 멤버 값으로 바로 들어가게 된다면 사용자가 허용하지 않은 범위의 값들이 들어올 수 있다
    위 그림에서 month는 달을 의미하기 때문에 1부터 12의 값만 들어와야 한다
    이를 확인해주는 과정이 필요하다
  • 타입 변환의 결과와 에러는 각각 BindingResult로 저장되고
    데이터 검증 후 에러일 경우 BindingResult로 저장되고 에러가 없을 경우 바로 MyDate로 바인딩 된다
    BindingResult는 컨트롤러로 넘겨주어 컨트롤러에서 적절히 처리할 수 있게 한다

데이터의 변환

실습 : RegisterController에 변환 기능 추가하기

  • 회원가입을 위한 form을 제출하면, 사용자가 적은 정보들이 User user 객체에 저장된다, 이때 각각 멤버의 타입에 맞게 변환되어야 한다

1. String 타입의 생일을 Date 타입으로 변환

문제 상황

  • 2000/11/29와 같이 입력하면 Date형식에 맞게 잘 출력된다
    스프링이 기본적으로 ../../.. 형식을 Date로 변환할 수 있기 때문
  • 구분자를 - 로 바꾼다면 2000-11-29 에러 발생
    이 형식은 스프링이 Date 타입으로 변환해주지 않기 때문

    해결방법
  1. 컨트롤러에 BindingResult 타입 객체를 매개변수로 삽입
    바인딩할 객체 바로 뒤에 자리해야한다!
    public String save(User user, BindingResult result, Model m)
  • BindingResult를 출력해보면,

    에러 페이지는 나오지 않지만 아무것도 나오지 않는다
    출력결과 - (변환에서 에러가 하나 발생하여 BindingResult에 에러가 전달되었다)
    result= org.springframework.validation.BeanPropertyBindingResult: 1 errors
    Field error in object 'user' on field 'birth': rejected value [2000-11-29]; codes [typeMismatch.user.birth,typeMismatch.birth,typeMismatch.java.util.Date,typeMismatch];
  1. 클라이언트에게 다시 작성하라고 redirect할 수도 있고,
  2. InitBinder 메서드를 선언할 수 있다
    메서드를 만들면, 타입을 변환할 때 이 메서드를 먼저 확인한다
       @InitBinder
       public void toDate(WebDataBinder binder){ //매개변수로 꼭 넣어줘야함!
           SimpleDateFormat df = new SimpleDateFormat("yyyy-mm-dd");
           binder.registerCustomEditor(Date.class, new CustomDateEditor(df, false));
       }
    실행결과 -

2. String 타입의 sns를 String 배열로 변환

  • 기존 sns를 체크하면 String으로 연결되어 전달했는데, 각각의 요소를 배열로 만들어 user에 저장하도록 한다
    객체의 타입을 String[]으로 바꾸면 입력된 데이터가 각각 나눠져서 저장이 된다
  • String[]타입의 멤버 hobby를 추가해보자
    hobby는 form에서 sns처럼 체크박스가 아닌 text 형태로 값을 입력받는다

문제상황

  • text 형태로 hobby를 여러개 입력받으면 자동으로 구분자로 나눠서 배열에 저장되지 않는다
  • 예를 들어 tennis#piano#swimming 을 입력했을 때 [tennis, piano, swimming]이 아닌 [tennis#piano#swimming] 으로 하나로 인식된다
  • 각각의 문자열이 배열로 들어가게 바꿔주어야 한다

해결방법

  • InitBinder에 구분자(여기선 #)로 String을 나눠 String[]에 저장하는 메서드를 사용한다
    binder.registerCustomEditor(String[].class, new StringArrayPropertyEditor("#"));

실행결과 -
[tennis, piano, swimming] 각각 나뉘어서 잘 저장되었다

💡애노테이션을 사용하는 방법💡

User 클래스에서 멤버로 birth를 선언할 때 애노테이션으로 @DateTimeFormat(pattern="yyyy-mm-dd")를 붙이면 컨트롤러에 데이터 변환을 설정하지 않아도 데이터가 변환된다


PropertyEditor

  • 양방향 타입 변환
    String -> 타입, 타입 -> String
    특정 타입이나 이름의 필드에 적용 가능
    binder.registerCustomEditor(String[].class, "hobby", new StringArrayPropertyEditor("#"));
    StringArray라는 PropertyEditor를 hobby라는 특정 필드에만 적용하겠다는 의미,
    적지 않으면 문자열을 문자열 배열로 바꾸는 변환에 사용하겠다는 의미이다
  • 디폴트 PropertyEditor는 스프링이 기본적으로 제공
    커스텀 PropertyEditor는 사용자가 직접 구현하는 것이고 PropertyEditorSupport를 상속하면 편리하다
  • PropertyEditor의 종류
  • @InitBinder의 메서드는 해당 메서드가 정의된 컨트롤러 내에서만 사용가능하다
  • 모든 컨트롤러에서 사용하고 싶다면 WebBindingInitializer를 사용해야 한다

Converter와 ConversionService

Converter

  • 단방향 타입 변환, 타입 A -> 타입 B
  • PropertyEditor의 단점(iv를 사용, stateful)을 개선, iv를 사용하지 않고, stateless
    iv가 있다는 것은 싱글톤으로 사용할 수 없다. 변환할 때마다 새로운 객체를 만들어야 한다
  • 원하는 Convert를 위한 클래스에 Converter 인터페이스를 implements Converter<String, String[]> 하고 메서드를 오버라이딩 하여 변환할 내용 구현하여 Converter 생성
  • 해당 Converter를 ConversionService에 등록한다
  • Converter도 특정 컨트롤러 내에서만 변환되게할 수도, 모든 컨트롤러에서 변환되게할 수도 있다

Conversionservice

  • Converter 인터페이스를 이용해 구현된 다양한 Converter클래스들이 등록되어 있다
  • 타입을 변환하는 방법들이 모여있는 곳
  • WebDataBinder에 DefaultFormattingConversionService가 기본적으로 등록되어 있다
    굉장히 많은 수의 변환 방법이 이미 등록되어 있다

Formatter

  • 양방향 타입 변환
    String -> 타입, 타입 -> String
  • 애노테이션으로 바인딩할 특정 필드에 적용
  • @NunberFormat, @DateTimeFormat 등
  • Formatter 인터페이스는 Printer<T>, Parser<T> 인터페이스를 상속하고 있다
  • Printer는 타입을 String으로 변환해주는 것
  • Parser는 String을 타입으로 변환해주는 것
  • 그래서 양방향

데이터 검증

Validator 인터페이스

  • 객체를 검증하기 위한 인터페이스
  • 객체 검증기(Validator)구현에 사용한다
  • support(Class<?> clazz) 메서드 : 이 검증기로 검증 가능한 객체인지 알려주는 메서드
  • validate(Object target, Errors errors) 메서드 : 객체를 검증하는 메서드
    target이 검증할 객체, errors는 검증시 발생한 에러 저장소
  • 두가지 메서드를 오버라이딩하여 검증기를 만든다
    validate 메서드에는 if문으로 에러가 발생했을 때 errors 객체에 오류에 대한 정보를 저장할 수 있고
    ValidationUtil에 정의된 에러 체크 메서드들을 이용해서 errors에 오류를 저장할 수 있다
  • errors에 에러가 발생한 정보를 저장한다는 의미는
    Errors인터페이스 내부에 reject(String errorCode) 메서드rejectValue(String field, String errorCode) 메서드 등 에러를 표현하기 위한 다양한 메서드가 있기 때문에 이 중에 rejectValue 메서드로 필드에 대한 에러가 발생할 경우 필드에 대한 에러 메세지를 저장한다
    reject 메서드는 특정 필드의 에러가 아닌 객체 전체에 대한 에러가 발생했을 경우 공통으로 처리하기 위해 사용한다
  • 컨트롤러 내부에서 검증 절차를 구현할 수 있지만 이 부분을 분리하여 코드를 간소화하며 따로 관리하도록 validator 관심사를 만든 것이다
  • 컨트롤러에서 생성한 validator 클래스의 객체를 생성하여 validate(객체, 결과) 메서드를 이용해 검증을 진행하고 검증 결과에서 hasErrors() 메서드를 이용해 에러가 있을 경우를 따로 처리한다
  • Before
  • After

자동 검증

  • 앞서 한 방법은 사용자가 수동으로 검증하는 방법
  • 자동 검증은 @InitBinder 애노테이션 이용
  • WebDataBinder의 메서드 setValidation(new 은 사용자가 구현한 validator)는 사용자가 구현한 validator를 WebDataBinder에 등록한다
  • 그 후에 검증을 하려는 객체 앞에 @Valid 애노테이션을 붙이면 해당 객체를 WebDataBinder에 있는 validator로 검증한다
  • 사용자가 정의한 validator를 WebDataBinder에 등록하였기 때문에 자동 검증으로 해당 객체를 검증할 수 있다
  • setValidation()은 컨트롤러 안에서 진행되고, setValidation으로 새로운 validator가 등록된 WebDataBinder는 그 컨트롤러 내부에서만 사용할 수 있다

Global Validator

  • 컨트롤러 내부에서만 사용하는 validator가 아닌 모든 컨트롤러에서 validator를 사용 가능하도록 하는 방법
  • 하나의 Validator로 여러 객체를 검증할 때 그 Validator를 Global Validator로 등록한다

Global Validator 등록하는 방법

servlet-context.xml에

	<annotation-driven validator="globalValidator"/>
	<beans:bean id="globalValidator" class="com.fastcampus.ch2.GlobalValidator"/>

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

  • 컨트롤러 안에 @initBinder가 붙은 메서드를 만들고 addValidators(로컬 validator) 한다
  • setValidators()가 아닌 addValidators()를 해야한다
    글로벌 validator에 추가로 로컬 validator를 등록하는 것이기 때문

데이터 검증 실습

1. UserValidator를 만든다

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; //target이 Object형이기 때문에 형변환

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

2. 유효성 검사 대신 만든 Validator를 직접 이용한다

//유효성 검사
if(!isValid(user)) {
    String msg = URLEncoder.encode("id를 잘못입력하셨습니다.", "utf-8");
    m.addAttribute("msg", msg);
    return "forward:/register/add";
		return "redirect:/register/add?msg="+msg; // URL재작성(rewriting)
}
------------------------------------------------------------------------
//수동 검증 - validator를 직접 생성하고 validate()를 직접 호출했음
  UserValidator userValidator = new UserValidator();
  userValidator.validate(user, result);
//user 객체를 검증한 결과 에러가 있으면, registerForm을 이용해서 에러를 보여준다
if(result.hasErrors()){
    return "registerForm";
}

3. Global Validator에 등록하여 자동 검증을 사용한다

  • controller 내에 @InitBinder 애노테이션이 붙은 메서드에 추가로 작성한다
//자동 검증
    binder.setValidator(new UserValidator()); //UserValidator를 WebDataBinder의 로컬 validator로 등록

4. 메서드의 매개변수 중 검증이 필요한 객체의 앞에 @Valid 애노테이션을 붙인다

public String save(@Valid User user, BindingResult result, Model m) throws Exception

5. 자바 표준 애노테이션인 @Valid를 사용하기 위해 추가 작업을 해준다

  • 메이븐 레포지토리에서 validation을 검색후 Bean Validation API에 들어간다
  • 2.0.1 final 버전을 선택하고 Maven 코드를 클릭하여 복사한다
  • 인텔리제이로 돌아와 pom.xml에 붙여넣는다
  • pom파일이 변경되었으니 프로젝트를 업데이트해준다
    프로젝트 파일을 우클릭하고,
  • 컨트롤러로 가서 valid 클래스를 import 한다

데이터 검증 실습 - Global Validator와 Local Validator 둘 다 사용하기

1. addValidators한다

2. servlet-context.xml에 등록한다

  • 글로벌 validator를 bean으로 등록하고
    그거를 글로벌 validator로 등록
	<annotation-driven validator="globalValidator"/>
	<beans:bean id="globalValidator" class="com.fastcampus.ch2.GlobalValidator"/>

결과

  • UserValidator를 직접 등록했고 (add)
    GlobalValidator를 xml을 이용해 등록했기 때문에 리스트에 두 validator가 등록되어 있는 것을 알 수 있다

  • Global Validator만 사용하고 싶다면 add한 부분을 삭제하면 된다

에러 표현 방법

  • 콘솔에서 에러를 보는 것이 아닌 사용자에게 보여줄 수 있는 방법

MessageSource 인터페이스

  • 다양한 리소스에서 메세지를 읽기 위한 인터페이스
  • 사용하기 위해서는 servlet-context.xml에 ResourceBundleMessageSource를 등록
	<beans:bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
		<beans:property name="basenames">
			<beans:list>
				<beans:value>error_message</beans:value> //아래와 이름이 error_message로 같아야 한다
              <!-- /src/main/resources/error_message.properties -->
			</beans:list>
		</beans:property>
		<beans:property name="defaultEncoding" value="UTF-8"/>
	</beans:bean>
  • getMessage() 메서드

       @Override
       public String getMessage(String code, Object[] args, String defaultMessage, Locale locale) {
           return null;
       }
       @Override
       public String getMessage(String code, Object[] args, Locale locale) throws NoSuchMessageException {
           return null;
       }
       @Override
       public String getMessage(MessageSourceResolvable resolvable, Locale locale) throws NoSuchMessageException {
           return null;
       }

    어떤 code를 주면 그에 대한 메세지를 반환하는 것

  • 에러 코드에 대한 메세지가 있는 파일을 생성해야 한다
    이름과 확장자는 error_message.properties

    메세지 코드 = 메세지로 구성되어 있음

  • 국가별로 다른 메세지 파일을 지정할 수 있다
    error_message.properties 는 디폴트
    error_message_ko.properties 는 getMessage의 locale이 ko일 때
    error_message_en.properties 는 getMessage의 locale이 en일 때

  • getMessage의 args 값으로 메세지에 들어갈 값을 줄 수 있다
    값으로 new String[]{"5", "11"}을 주면 의 {1}에 5가, {2}에 11이 들어가서 출력됨

  • Error에 "id" 필드에 "required" 값이 저장되었다면 에서 required.user.id 찾고 없으면 required.id찾고 없으면 required.java.lang.String찾고 없으면 required 찾고 그래도 없으면 default message 출력한다
    위 파일에서는 id를 찾으면 required가 출력될 것이다

taglib

  • 스프링이 제공하는 커스텀 태그 라이브러리를 사용하기 위해 jsp 파일에 다음 코드를 추가한다
<%@ 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"/> id라는 필드에서 발생한 에러 보여줌
<span id="id.errors" class="msg">필수 입력 항목입니다.</span> 로 변환된다
  • validator에서 args에 값을 넣어주면 {1}과 {2}에 값이 들어간다
if(id==null || id.length() <  5 || id.length() > 12) {
    errors.rejectValue("id", "invalidLength", new String[] {"5", "12"}, null);
}
profile
안녕하세요. Chat JooPT입니다.

0개의 댓글