본문 바로가기

Server Programming/BackEnd Project

[스프링의 정석-리뷰] 1. 데이터 변환과 검증

반응형

1. WebDataBinder

쿼리스트링이 담긴 URL을 요청하면, 파라미터 Map으로 쿼리시트링에 담긴 값들이 담긴다.

 

Map은 다시 컨트롤러의 매개변수로 전달받은 객체에 담기게 된다.

 

WebDataBinder

: Map의 Value는 모두 String이기 때문에, 객체의 자료형에 맞춰서 타입 변환과 데이터 검증을 수행하고, BindingResult에 타입 변환과 데이터 검증의 결과를 담아서 응답한다.

 

 

  • 회원 가입 예제
    1. 타입 변환 
      • String -> Date 변환 (변환 메서드를 정의한 변환기 이용해 변환)
      • String[] -> String 변환 (여러 개 문자열로 이루어진 문자열 배열을 문자열로 스프링이 자동변환)
    2. 데이터 검증
    3. BindingResult타입의 매개변수로 성공한 결과 혹은 실패시 에러가 담긴 결과 반환해 해당 결과를 컨트롤러에서 사용
    4.  

 

  • Date
    1. 기본 형식인 "yyyy/MM/dd"으로 String 문자열로 요청된  경우, 자동 변환 수행
    2. 하지만 나머지 형식은 변환기에서 변환할 형식 지정 필요 : @DataTimeFormat 어노테이션으로 필드에 직접 형식 지정 가능
    3. 해당 요청을 담는 객체 바로 다음에 BindindResult를 함께 매개변수로 전달하면, 예외가 발생해도 컨트롤러에 결과를 담아서 컨트롤러가 처리하도록 전달한다.
    4. 예외가 발생했을 경우, 컨트롤러가 처리하는 방식
      1. msg를 문자열로 지정해, URLEncoder로 변환 후 모델에 담아서 사용자에게 에러 결과를 URL재작성해 전달
      2. 스프링 Validator 인터페이스 구현하는 방법으로 해당 인스턴스에서만 활용되는 Validator로 인터페이스에는 두 개의 메서드가 존재한다.
        1. supoorts : 동작할 조건을 정의하는데, 일반적으로 클래스의 타입을 비교한다.
        2. validate :  원하는 검증을 수행한다.
      3. Java Bean Validation 이용하는 방법으로, 자바 빈 객체 내에서 어노테이션으로 검증방법 명시
        : 요청 DTO에 어노테이션으로 명시하고, @RequestBody 앞에 @Valid 어노테이션을 붙여서 검증
        만약 자바 빈 Validation으로 검증 실패시 MethodArgumentNotValidException이 발생한다.
        1. @NotBlank
        2. @NotNull
        3. @Size
        4. @Min
        5. @Email
  • sns을 문자열 배열 바꿔도 스프링이 자동 변환

BindingResult로 변환 및 검증 결과 전달해 컨트롤러에 변환기능 추가

 

2. 데이터 변환 방법

: 인스턴스 변수를 사용하는 PropertyEditor와 싱글톤에서 사용하는 Converter

 

  • 문자열을 날짜 형식으로 변환기 추가
    1. Data Binding 방식 : 빈으로 등록시 ConversionService에 등록돼 자동으로 동작한다.
      1.  PropertyEditor를 상속받아 양방향으로 변환을 수행하는 @InitBinder을 이용해 해당 클래스에서 변환수행하는데 인스턴스 변수를 변경하는 방식으로 싱글톤에선 사용이 불가능하다.
      2. 단방향으로 변환을 수행하기 위해서 Conveter를 구현한 클래스를 이용
      3. Formatter는 특정 객체와 문자열 간의 변환을 담당 인터페이스
        1. print : API 요청에 대한 응답에 대해서, 전달 받은 데이터를 원하는 형태의 문자열로 변환
        2. parse : API 요청을 받아올 때, 문자열로 된 형식의 데이터를 원하는 형식으로 변환
  • @InitBinder 이용해 문자열을 날짜로 변환형식 전달해 변환에러 해결
    1. WebDataBinder를 매개변수로 받는 메서드 정의
    2. 원하는 날짜형식을 전달하는 SimpleDateFormat 객체 생성
    3. 전달받은 WebDataBinder 매개변수의 registerCustomEditor 메서드 호출하는데 스프링이 제공하는 CustomDataEditor 객체를 생성해서 만들어둔 형식과 빈값 허용여부를 인자로 전달
    4. 커스텀변환기로 형식 확인 후, 디폴트 변환기로 형식 확인해 변환

  • 취미 필드 추가해 문자열을 구분자로 #지정해 문자열 배열로 들어가도록 변환 [PropertyEditor 이용]
    • registerCustomEditor의 원하는 필드와 StringPropertyEditor 객체 생성해 구분자를 인자로 전달

 

 

3. Converter와 ConversionService

 

  • Converter
    • 싱글톤에서 사용가능한 단방향 타입 변환기
  • ConversionService
    • 여러 컨버터 등록 가능
    • 타입 변환 서비스 제공
    • WebDataBinder에 DefaultFormattingConversionService 자동 등록
    • 모든 컨트롤러에서 변환 - ConfigurableWebBindingInitializer 
    • 특정 컨트롤러에서 변환 - @InitBinder가 붙은 메서드 작성

4. Fomater

  • Printer<T>와 Parser<T> 인터페이스를 상속하는 Formatter
    • 양방향 타입 변환
    • 바인딩할 필드에 어노테이션으로 직접 적용 - @NumberFomart, @DataTimeFormat

 

 

 

5. Validator [컨트롤러 내의 관심사 분리]

  • 객체를 검증하기 위한 인터페이스로, 객체 검증기 구현에 사용
    1. supports : 검증 가능한 객체 여부를 판단하기 위한 메서드로 일반적으로 클래스 타입을 지정
      [클래스타입.class.isAssginableFrom(clazz)로 clazz가 해당 클래스 타입 또는 그 자손인지 확인]
    2. validate : 객체를 검증하는 메서드로 검증할 객체와 검증시 발생한 에러 저장소를 인자로 전달
  • BindingResult의 조상클래스인 Errors 인터페이스
    • 메서드
      • 객체 전체에 대한 에러를 전달하는 reject 메서드 
      • 필드에 대한 에러코드를 전달하는 rejectValue 메서드로 세번째 인자에 디폴트 메시지 전달 가능
    • Errors 클래스의 rejectValue 메서드 호출해 <key, value> 형식으로
      필드와 필드에 해당하는 에러 코드를 인자로 전달, 에러코드에 해당하는 메시지를 전달하는데 사용한다.
    • ValidationUtils 클래스의 rejectIfEmptyOrWhitespace 메서드를 이용해 간단하게 공백과 빈 문자열 확인 가능
  • 뷰에서는 <form:errors>태그를 이용해 전달받은 에러코드에 해당하는 에러 메시지를 전달하는데 사용 가능

 

6. 검증기를 작성한 컨트롤러안에서만 사용가능한 로컬 Validator

  1. Validator 수동 검증 : 작성한 Validator 클래스를 호출해 직접 검증
  2. Validator 자동 검증 : 작성한 Validator를 WebDataBinder에 등록하고 검증할 객체에 @Valid 선언해 검증
    @Valid 어노테이션 : 자바 표준 어노테이션으로, Bean Validation API 의존성 추가 필요

 

 

7. 하나의 Validator로 여러 객체 검증가능한 글로벌 Validator

  1. 클래스 정의해 서블릿 컨텍스트에 빈으로 등록
  2. 서블릿 컨텍스트에 등록한 빈을  <annotation-driven>에 Validator 속성으로 등록
  3. 글로벌 검증기에 로컬 검증기를 추가하는 방식으로 글로벌 검증기와 로컬 검증기 동시에 적용

 


  • 회원가입 예제에 적용

1. 회원가입 컨트롤러에 수동 검증

  • 객체의 필드에 대한 지정한 에러코드로 규칙을 이용해 여러개의 에러코드를 만들거나, 빠진 필드가 존재할 경우에는 디폴트 메시지를 이용해 에러 메시지를 보여준다.
  • 필드에 대한 에러코드를 만들면, 여러 개의 에러코드를 만들어 해당하는 메시지를 찾는다.
  • 세 번째 인자에 디폴트 메시지를 전달할 수도 있는데 적지 않을 경우 null로 전달 [일반적으로 직접 작성하지 않고 별도의 파일로 관리한다.]

 

 

2. 회원가입 컨트롤러에 자동검증 추가

 

3. 전역 검증기 적용

  1. 전역 검증기 작성
  2. 서블릿 컨텍스트에 빈 등록
  3. 컨트롤러의 @InitBinder에 addValidator로 추가해 글로벌 검증기와 로컬 검증기 동시 적용

 

 

 


8. MessageSource 인터페이스

  1. MessageSource 인터페이스의 Locale 매개변수로 지역의 국가코드별로 원하는 에러메시지 작성 가능
  2. 프로퍼티 파일에 <에러코드=에러메시지>으로 에러코드에 해당하는 메시지 코드 작성
  3. 프로퍼티 파일을 메시지 소스로하는 ResourceBundleMessageSource 등록
  4. 해당 에러코드의 여러 변형형태순으로 에러메시지 검색 [복잡한 형태 -> 단순한 형태]
    : required.user.id -> required.id -> required.java.lang.String -> required

 

 

 

  • 에러 메시지 설정
    1. text의 디폴트 인코딩을 UTF-8으로 설정
    2. 에러메시지를 properties로 작성
    3. 서블릿 컨텍스트에 작성한 프로퍼티 이름을 빈으로 등록

 

required=필수 항목입니다.
required.user.pwd=사용자 비밀번호는 필수 항목입니다.
invalidLength.id=아이디의 길이는 {0}~{1}사이어야 합니다.

 

	<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 -->
			</beans:list>
		</beans:property>
		<beans:property name="defaultEncoding" value="UTF-8"/>
	</beans:bean>

 

 

  • 검증 메시지 출력
    1. 스프링이 제공하는 커스텀 태그 라이브러리 추가
    2. <form> 대신 <form:form>으로 변경
    3. <form: errors>로 에러 출력하는데 , path에 에러 발생 필드 지정 (*은 모든 필드의 에러) : 자바스크립트 이용
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form" %>
<form:form modelAttribute="user">
  <div class="title">Register</div>
  <div id="msg" class="msg">
    <c:if test="${not empty param.msg}">
      <i class="fa fa-exclamation-circle"> ${URLDecoder.decode(param.msg)}</i>
    </c:if>
  </div>
  <label for="">아이디</label>
  <input class="input-field" type="text" name="id" placeholder="8~12자리의 영대소문자와 숫자 조합">
  <label for="">비밀번호</label>
  <input class="input-field" type="text" name="pwd" placeholder="8~12자리의 영대소문자와 숫자 조합">
  <label for="">이름</label>
  <input class="input-field" type="text" name="name" placeholder="홍길동">
  <div class="sex-chk">
    <label><input type="radio" name="sex" value="true" checked="checked"/>남</label>
    <label><input type="radio" name="sex" value="false" />여</label>
  </div>
  <label for="">연락처</label>
  <input class="input-field" type="text" name="tel" placeholder="010-1234-1234">
  <label for="">이메일</label>
  <input class="input-field" type="text" name="email" placeholder="example@fastcampus.co.kr">
  <label for="">생년월일</label>
  <input class="input-field" type="text" name="birth" placeholder="2020-12-31">
  <button>회원 가입</button>
</form:form>

 

<form: errors> 기본 구조

<form : errors path="id" cssClass="msg"/>

 

css 적용해 <form: errors> 삽입

<div id="msg" class="msg"><form : errors path="id">

 

자바스크립트 적용시

<div id="msg" class="msg">
  <c:if test="${not empty param.msg}">
    <i class="fa fa-exclamation-circle"> ${URLDecoder.decode(param.msg)}</i>
  </c:if>
</div>

 

<script>
  function formCheck(frm) {
    var msg ='';
    if(frm.id.value.length<3) {
      setMessage('id의 길이는 3이상이어야 합니다.', frm.id);
      return false;
    }
    return true;
  }
  function setMessage(msg, element){
    document.getElementById("msg").innerHTML = `<i class="fa fa-exclamation-circle"> ${'${msg}'}</i>`;
    if(element) {
      element.select();
    }
  }
</script>

 

package com.fastcampus.comicbookrental.validator;

import com.fastcampus.comicbookrental.dto.UserDTO;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;

public class UserDTOValidator implements Validator {
    @Override
    public boolean supports(Class<?> clazz) {
//    return UserDTO.class.equals(clazz); // 검증하려는 객체가 UserDTO타입인지 확인
        return UserDTO.class.isAssignableFrom(clazz); // clazz가 UserDTO 또는 그 자손인지 확인
    }

    @Override
    public void validate(Object target, Errors errors) {
        System.out.println("UserDTOValidator.validate() is called");

        UserDTO user = (UserDTO)target;

        String id = user.getId();

//    if(id==null || "".equals(id.trim())) {
//       errors.rejectValue("id", "required");
//    }
        //앞뒤 공백을 잘라서 빈문자열인지 확인하는 클래스와 static메서드 (유틸리티클래스)
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "id",  "required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "pwd", "required");

        if(id==null || id.length() <  5 || id.length() > 12) {
            errors.rejectValue("id", "invalidLength", new String[] {"", "5", "12"}, null);
            //errors.rejectValue("id", "invalidLength", new String[] {"5", "12"}, null);
        }
    }
}
반응형

'Server Programming > BackEnd Project' 카테고리의 다른 글

50일차 - TIL  (0) 2023.02.01
49일차 - TIL  (0) 2023.01.31
48일차 - TIL  (0) 2023.01.29
45일차 - TIL  (0) 2023.01.26
44일차 - TIL  (0) 2023.01.25