본문 바로가기

🌈 Spring Framework/🌱 Spring-boot

Validator - BindingResult

📌 데이터 검증 / Validator

: 포맷 데이터를 파라미터로 받아 데이터를 만들고 모델에 담아 뷰에 보여주는 과정.

→ 파라미터가 데이터로서 사용 가능한지 파악하는 단계

 - build.gradle update

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-validation'
}

 

▶️ ContentDto

: 기본적으로 페이지끼리 전달할 데이터 정보

- 폼 데이터의 유효성은 검증 온 두단계에서 이루어진다

1. 클라이언트인 Html 페이지에서 자바스크립트를 통한 검사 

2. 서버 페이지인 Jsp.Servlet에서 파라미터로 받은 후 검증

import lombok.Data;

@Data
public class ContentDto {
    private int id;
    private String writer;
    private String content;
}

 

▶️ ContentValidator

: 필요한 필드만 검증하는 로직 (support, validate) 구현

커맨드 객체를 검증하기 위해 ContentDto 를 사용해, 내용이 null이거나 비어있으면 에러가 발생해다는 로그 출력 및 key-value 형태로 값을 넣어준 것

 - 데이터가 유효성 검사에서 통과를 못했다면 errors 객체 변수에 에러내용을 담는다.

모든 요소를 검증할 필요는 없다. 필요한 필드나 (요소)만을 검증하는 로직을 만들면 된다.

public class ContentValidator implements Validator{

    @Override
    public boolean supports(Class<?> arg0) {
        // arg0 : 검증할 클래스 타입 정보
        return ContentDto.class.isAssignableFrom(arg0);  
    }

    @Override
    public void validate(Object obj, Errors errors) { //원하는 커맨드 객체 obj
        ContentDto dto = (ContentDto) obj; //형변환하고 변수에 저장 

        String sWriter = dto.getWriter(); //컨맨드 객체로부터 작성자의 값을 구해와서 
        /*
            커멘드 객체로 부터 작성자의 값을 구해와서
            값이 널이지 공백인지를 체크하는 로직이다  
        */
        if(sWriter == null || sWriter.trim().isEmpty()) {
            System.out.println("Writer is null or empty"); 
            errors.rejectValue("writer", "trouble");
        }

        String sContent = dto.getContent(); 
        if(sContent ==null || sContent.trim().isEmpty()) { 
            System.out.println("Content is null or empty");
            errors.rejectValue("content", "trouble");
        }

    }

}

→ 참고! :: `ValidationUtils` 클래스도 이용가능! (아래 예제 continue)

ValidationUtils.rejectIfEmptyOrWhitespace
                                (errors, "writer", "writer is empty.");

▶️ MyController

@Controller
public class MyController {

// @ResponseBody 에 의해 String "Validator(1)" 이 그대로 호출 
    @RequestMapping("/")
    public @ResponseBody String root() throws Exception {
        return "Validator (1)";
    }

    // createPage.jsp 호출 
    @RequestMapping("/insertForm")
    public String insert1() {
        return "createPage";
    }

    @RequestMapping("/create")
    public String insert2(@ModelAttribute("dto") ContentDto contentDto, 
                              BindingResult result) {
        // 커맨드 객체 파라미터로 폼 데이터를 받아서 처리 

        String page = "createDonePage";
            System.out.println(contentDto);

        // 유효성 검증 객체를 만든다. 
        ContentValidator validator = new ContentValidator(); 
        validator.validate(contentDto, result); // 파라미터로 받음 

        // result 값이 있다면 에러가 있다. -> 체크를 한다. 
        if(result.hasErrors()) {
            page = "createPage";
        }
        // 에러가 없다면 결과페이지 jsp를 호출,에러가 있다면 입력 페이지의 jsp 를 리턴 
        return page;
    }

}

☑️ /create

: createDonePage.jsp 가 호출해야하지만 ContentValidator 클래스와 validate의 데이터 검증과정을 통해 유효한 값이 입력되면 그대로 호출하고 에러가 발생하면 다시 createPage로 돌아간다.

 -  @ModelAttribute("dto") : jsp 페이지에서 dto이라는 변수명으로 접근이 가능


    <% 
        **String conPath = request.getContextPath();**
    %>
    <form action = "<%= conPath %>/create" >
        작성자 : <input type = "text" **name = "writer" value = "${dto.writer }**"><br/>
        내용 : <input type = "text" **name = "content" value = "${dto.content }**"><br/>
        <input type = "submit" value ="전송"> <br/>
    </form>
</body>
</html>

<!--  
    request.getContextPath() 는 프로젝트의 경로 를 가져온다.
    form형식에 맞게 작성자 부분과 내용 부분을 입력하면 그값은 각각 
    dto.wrtier, dto.content 값들이 되고 validate함수에 의해 데이터 겁증 과정을 거친다. 
 -->

▶️ Result 

// writer content가 3 자리 이하일때 
ContentDto(id=0, writer=작가, content=hello helllo )
getAllErrors: [Field error in object 'dto' on field 
'writer': rejected value [작가]; codes [
writer is too short.dto.writer,writer is too short.writer,
writer is too short.java.lang.String,writer is too short]; 
arguments []; default message [null]]
1:writer is too short

// 완벽한 result 
ContentDto(id=0, writer=나는작가, content=hello helllo )

 

👉 위에서는 공백 처리에 대한 validator을 알아봤다면 글자수도 제한시켜보자!

 

 

▶️ ContentValidator

public class ContentValidator implements Validator{

    @Override
    public boolean supports(Class<?> arg0) {
        return ContentDto.class.isAssignableFrom(arg0);
        // 검증할 객체의 클래스 타입 정보 
    }

    @Override
    public void validate(Object obj, Errors errors) {
        ContentDto dto = (ContentDto) obj;

        **ValidationUtils.rejectIfEmptyOrWhitespace
                                        (errors, "writer", "writer is empty.");**

        String sWriter = dto.getWriter();

        /**
         * writer 나 content 값이 비어있거나 null 이면 해당 에러를 출력. 
         * 특히 "writer" 부분은 길이가 3보다 작을 때도 에러 출력 
         */

        if(sWriter.length() < 3) {
            errors.rejectValue("writer", "writer is too short");

        }

        **ValidationUtils.rejectIfEmptyOrWhitespace
                                    (errors, "content", "content is empty.");**
    }

}

 

▶️ MyContoller

@Controller
public class MyController {
    // 약한결합과 디펜던시를 적용시킴 
    @RequestMapping("/")
    public @ResponseBody String root() throws Exception {
        return "Valid_initBinder (3)";

    }

    @RequestMapping("/insertForm")
    public String insert1() {
        return "createPage";
    }

    /* @Valid contentDto 객체변수에 대한 유효성 검증을 하겠다고 표시하는 것이다 
     객체 변수가 들어오면 스프링이 binder변수에 저장된 갹체를 통해서 즉시 유효성 검사를 하고 
     에러가 있다면 result 변수에 담아둔다. 
     BindingResult : Validator를 상속받는 클래스에서 객체값을 검증하는 방식
*/
    @RequestMapping("/create")
    public String insert2(@ModelAttribute("dto") @Valid ContentDto contentDto,
            BindingResult result) { 

        String page = "createDonePage";
        System.out.println(contentDto);

        // ContentValidator validator = new ContentValidator();
        // validator.validate(contentDto, result); 
        if(result.hasErrors()) {
            System.out.println("getAllErrors: " + result.getAllErrors());
            if(result.getFieldError("writer") != null) {
                System.out.println("1:" + result.getFieldError("writer").getCode());

            }
            if(result.getFieldError("content") != null) {
                System.out.println("1:" + result.getFieldError("content").getCode());

            }
            page = "createPage";
        }
        return page; 
        /** 
         * ContentValidator 클래스에 유효성 검증 에러를 출력, 스프링에서 제공되는 Api 를 사용 
         * 에러를 담은 결과만 리턴받기 때문에 MyController 클래스에서 에러를 출력하도록 수정하였다. 
         */
    }

    @InitBinder
    protected void initBinder(WebDataBinder binder) {
        binder.setValidator(new ContentValidator());
    }


}

📌 ValidationUtils

: org.springframework.validation.Validator - 애플리케이션에서 사용하는 객체 검증용 인터페이스

Validator - rejecting empty fields

 

특징

 - 어떤한 계층과도 관계가 없다. => 모든 계층(웹, 서비스, 데이터)에서 사용해도 좋다.

- 구현체 중 하나로, JSR-303(Bean Validation 1.0)과 JSR-349(Bean Validation 1.1)을 지원한다. (LocalValidatorFactoryBean)

 - DataBinder에 들어가 바인딩 할 때 같이 사용되기도 한다.

 

인스턴스

boolean supports(Class clazz): 어떤 타입의 객체를 검증할 때 사용할 것인지 결정함

내가 검증해야 하는 인스턴스의 클래스가 이 밸리데어터가 지원하는지 확인하는 메서드(검증할 수 있는 클래스인지 확인하는 메서드)

void validate(Object obj, Errors e): 실제 검증 로직을 이 안에서 구현

실질적으로 검증을 하는 로직. 구현할 때 ValidationUtils 사용하며 편리함.

- 유효성 검사 :

구체적으로 말하면 유효성검사는 웹티어에 묶이지 않아냐 하고 쉽게 지역화해야 하고 이용가능한 어떤 밸리데이터(validator)에 연결할 수 있어야 한다. 스프링은 어플리케이션의 모든 레이어에서 기본이 되고 아주 사용하기 편리한 Validator 인터페이스를 제안했다.

데이터바인딩은 어플리케이션의 도메인 모델(또는 사용자 입력을 처리하려고 사용하는 어떤 객체)에 사용자 입력을 동적으로 바인딩하는데 유용하다. 스프링은 이 작업을 하기 위해서 DataBinder를 제공한다. Validator와 DataBinder는 주로 MVC 프레임워크에서 사용되지만 제한이 있는 것은 아닌 validation 패키지를 구성한다.

 

 

✅ validator parameter 분석

    public String insert2(@ModelAttribute("dto") @Valid ContentDto contentDto,
            BindingResult result)

☑️ @ModelAttribute

 - 파라미터로 넘겨준 타입의 오브젝트를 자동으로 생성

@ModelAttribute User user 

- 인수를 넘기면 User타입의 user 오브젝트를 자동으로 생성 

 - 생성된 오브젝트에 HTTP 로 넘어온 값들을 자동으로 바인딩

: HTTP 파라미터는 문자열이기 때문에 오브젝트에 맞는 형 변환이 필요. 스프링 에디터 사용

 - 오브젝트로 넘어온 값을 검증

: 사용자가 자체적으로 검증기를 등록해 Validation 체크 진행

☑️ @Valid

 - @Valid는 객체에 들어가는 값을 검증해주는 어노테이션

 - 유효한 값인지 검증은 소스코드 여러군데서 이루어지기 때문에 불필요한 중복코드가 늘어나고 복잡

 

☑️ BindingResult

 - Validator를 상속받는 클래스에서 객체값을 검증하는 방식

 

 

@Valid 어노테이션을 사용하는 방법, BindingResult를 사용하는 방법 두가지로 나뉜다

  1. BindingResult

: join url에 호출될 때 마다 initBinder 함수가 호출되면서 UserInfoValidator 객체를 생성한 후 해당 객체의 validate 함수를 호출해서 값이 유효한지 체크한다

- model : 검증 클래스를 따로 만들어서 거기서 유효값을 체크

 - validator : Validator를 인터페이스 상속 후 supports, validate 함수를 재정의; 유효한지 체

public String join(@Validated UserInfo userInfo,  BindingResult bindingResult){
           System.out.println("name:"+userInfo.getName());
           System.out.println("pass:"+userInfo.getPassword());

          System.out.println("error:"+bindingResult.hasErrors());

           if(bindingResult.hasErrors()){
                return "join";
           }
           return "home";
     }

@InitBinder
     protected void initBinder(WebDataBinder binder) {
           binder.setValidator(new UserInfoValidator());
     }
  1. @Valid

: hibernate-validatori/ Validator를 상속하는 유효검증 클래스를 만든 후 검증을 구현

public String join(@Validated UserInfo userInfo,  BindingResult bindingResult){
           System.out.println("name:"+userInfo.getName());
           System.out.println("pass:"+userInfo.getPassword());
          System.out.println("error:"+bindingResult.hasErrors());

          if(bindingResult.hasErrors()){
                                //에러 리스트는 getAllErrors() 함수로 가져온다
                List<ObjectError> list =  bindingResult.getAllErrors();
                for(ObjectError e : list) {
                     System.out.println(e.getDefaultMessage());
                }
                return "join";
           }
           return "home";
     }
public class UserInfo {
     @NotEmpty(message="name을 입력해주세요")
     private String  name;
     @NotEmpty(message="비밀번호를 입력해주세요")
     @Size(min = 6, max=10, message = "길이가 알맞지 않습니다")
     private String  password;
     @Range(min = 1, max = 5)
     private int     grade;
}

 

 

스프링에서 바인딩 일어날때 값에 적절한 형변환

✔️ PropertyEditor

: 스프링 기본적으로 제공하는 바인딩용 타입(형) 변환 API

 - 커스텀 프로퍼티 에디터를 만들 때는 PropertyEditorSupport 클래스를 상속해서 필요한 메소드(getAsText, setAsText)만 구성

levelPropertyEditor.java - 커스텀 프로퍼티 에디터

static class LevelPropertyEditor extends PropertyEditorSupport {

    public void setAsText(String text) throws IllegalArgumentException {

        this.setValue(Level.valueOf(Integer.parseInt(text.trim())));

    }

    public String getAsText() {

        return String.valueOf(((Level)getValue()).intValue()); 

    }        

}

 

✔️ InitBinder

: WebDataBinder 초기화 메서드를 사용해서 커스텀 프로퍼티 에디터를 등록할 수 있다.

@InitBinder 메소드

@InitBinder
public void initBinder(WebDataBinder dataBinder){
    dataBinder.registerCustomEditor(Level.class, new LevelPropertyEditor());
    //dataBinder에 커스텀에디터를 등록한다. Level 오브젝트를 만나면 
   //  LevelPropertyEditor와 연결된다.
}

@initBinder 애노테이션을 통해 level 타입을 무조건 LevelPropertyEditor 와 연결

 

참고 :

'🌈 Spring Framework > 🌱 Spring-boot' 카테고리의 다른 글

Lombok 사용시 주의사항  (0) 2021.05.03
@SpringBootApplication  (0) 2021.02.18
IoC - ApplicationContext / Bean  (0) 2021.02.18