2021 Spring Study

Spring 18. 커맨드 객체 validation

Yerim Kim 2021. 2. 18. 22:45

1. 커맨드 객체 validation 하기

1. springframework.validation.Validator 를 implement 하는 클래스를 구현한다.

아래는 springframework.validation.Validator 인터페이스이다.

package org.springframework.validation;

public interface Validator {

    boolean supports(Class<?> clazz);

    void validate(Object target, Errors errors);

}

<구현>

package controller;

import ...

public class RegisterRequestValidator implements Validator {

    ...

    // 파라미터로 전달받은 clazz 객체가 RegisterRequest 타입으로 변환이 가능한지 확인한다.
    @Override
    public boolean supports(Class<?> clazz) {
        return RegisterRequest.class.isAssignableFrom(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
        RegisterRequest registerRequest = (RegisterRequest) target;

        if (isEmailEmpty(registerRequest)) {
            // "email" 프로퍼티(필드)의 에러코드로 "required"를 추가
            errors.rejectValue("email", "required");
        }
        else if (isEmailFormatWrong(registerRequest)) {
            errors.rejectValue("email", "bad");
        }
        // "name" 필드가 비어있거나 공백으로만 이루어져있으면 "name" 필드에 "required" 에러코드 추가
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
        ValidationUtils.rejectIfEmpty(errors, "password", "required");
        ValidationUtils.rejectIfEmpty(errors, "confirmPassword", "required");

        if (!registerRequest.isPasswordEqualToConfirmPassword()) {
            errors.rejectValue("confirmPassword", "nomatch");
        }
    }

    private boolean isEmailEmpty(RegisterRequest registerRequest) {
        ...
    }

    private boolean isEmailFormatWrong(RegisterRequest registerRequest) {
        ...
    }
}
  • Errors 객체는 커맨드 객체의 프로퍼티 값을 구할 수 있는 getFieldValue() 메서드를 제공한다.
    따라서 ValidationUtils.rejectIfEmptyOrWhitespace() 메서드가 커맨드객체 없이 커맨드객체의 값을 검사할 수 있다.

2. 구현한 Validator 를 컨트롤러 메서드에서 사용한다.

package controller;

import ...

@Controller
public class RegisterController {

    ...

    @PostMapping("/register/step3")
    public String handleStep3(RegisterRequest registerRequest, Errors errors) {
        new RegisterRequestValidator().validate(registerRequest, errors);

        if (errors.hasErrors()) {
            return "register/step2";
        }
        try {
            registerService.register(registerRequest);
            return "register/step3";
        } catch (Exception e) {
            errors.rejectValue("email", "duplicate");
            return "register/step2";
        }
    }
}
  • request mapping 어노테이션을 적용한 메서드의 커맨드객체 파라미터 뒤에 Errors 타입 파라미터가 위치하면, 스프링MVC가 이 메서드를 호출할 때 이 커맨드객체와 연결된 Errors 객체를 생성해서 파라미터로 전달한다.

2. 커맨드 객체의 에러 메세지 출력하기

여기까지 구현했으면 step2 회원가입 실패시 다시 step2 화면을 보여준다.

이번에는 뭐가 잘못됐는지를 View에서 친절하게 출력해주도록 구현해보자.

에러 메세지는 Message Source 로 관리한다.

 

1. properties 파일 작성

  • 에러코드

    앞서 구현한 Validator 를 보면 errors.rejectValue("email", "required") 이런 부분을 볼 수 있다.

    첫번째 파라미터는 커맨드객체의 필드 이름, 두번째 파라미터는 errorCode 이다.

이 에러코드들 각각에 대응시킬 메세지를 properties 파일로 정의하자.

패키지나 파일이름은 마음대로 해도 된다.

# src/resources/my-message/my-test/my-label.properties
required=필수항목입니다.
bad.email=이메일이 올바르지 않습니다.
duplicate.email=중복된 이메일입니다.
nomatch.confirmPassword=비밀번호와 확인이 일치하지 않습니다.
  • 에러 코드에 해당하는 메시지 코드를 찾을 때에는 다음 규칙을 따른다.
    1. 에러코드.커맨드객체이름.필드명
    2. 에러코드.필드명
    3. 에러코드.필드타입
    4. 에러코드

2. MessageSource 빈 등록

setBasenames 파라미터는 properties 파일의 패키지.파일이름 이렇게 써주면 된다.

package config;

import org.springframework.context.MessageSource;
import ...

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource ms = new ResourceBundleMessageSource();
        ms.setBasenames("my-message.my-test.my-label");
        ms.setDefaultEncoding("UTF-8");
        return ms;
    }

    ...
}

 

3. <form:errors> 로 에러메세지 출력

<%@ page contentType="text/html; charset=utf-8" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<!DOCTYPE html>
<html>
<head>
    <title>회원가입</title>
</head>
<body>
    <h2>회원 정보 입력</h2>
    <form:form action="step3" method="post" modelAttribute="registerRequest">
    <p>
        <label>이메일:<br>
        <form:input path="email"/>
        <form:errors path="email"/>
        </label>
    </p>
    <p>
        <label>이름:<br>
        <form:input path="name"/>
        <form:errors path="name"/>
        </label>
    </p>
    <p>
        <label>비밀번호:<br>
        <form:password path="password"/>
        <form:errors path="password"/>
        </label>
    </p>
    <p>
        <label>비밀번호 확인:<br>
        <form:password path="confirmPassword"/>
        <form:errors path="confirmPassword"/>
        </label>
    </p>
    <input type="submit" value="가입 완료">
    </form:form>
</body>
</html>
  • <form:errors> 태그의 path 속성 : 에러메세지를 출력할 프로퍼티 이름

4. 글로벌 범위 Validator

글로벌 범위 Validator는 모든 컨트롤러에 적용할 수 있는 Validator 이다.

  1. 스프링MVC 설정파일에서 글로벌 범위 Validator를 Bean 으로 등록한다.

    package config;
    
    import ...
    
    @Configuration
    @EnableWebMvc
    public class MvcConfig implements WebMvcConfigurer {
    
      ...
    
      @Override
      public Validator getValidator() {
          return new RegisterRequestValidator();
      }
    }
  2. 글로벌 범위 Validator 를 사용하고 싶은 곳에 @Valid 를 붙인다.

    package controller;
    
    import ...
    
    @Controller
    public class RegisterController {
    
      ...
    
      @GetMapping(value = "/register/step1")
      public String handleStep1() {
          return "register/step1";
      }
    
      ...
    
      @PostMapping("/register/step3")
      public String handleStep3(@Valid RegisterRequest registerRequest, Errors errors) {
          if (errors.hasErrors()) {
              return "register/step2";
          }
          try {
              registerService.register(registerRequest);
              return "register/step3";
          } catch (Exception e) {
              errors.rejectValue("email", "myduplicate");
              return "register/step2";
          }
      }
    }
    
    • 만약 Erros 타입 파라미터가 없으면, 검증 실패시 400 에러를 응답한다.

5. 스프링 MVC가 제공하는 글로벌 Validator 사용하기

Bean Validation 2.0 (JSR 380) 이 제공하는 어노테이션을 이용해서 validation 을 해보자.

  1. dependency 추가

    compile 'javax.validation:validation-api:1.1.0.Final'
    compile 'org.hibernate:hibernate-validator:5.4.2.Final'
  2. 커맨드객체에 어노테이션 사용하기

    package spring;
    
    import javax.validation.constraints.Email;
    import javax.validation.constraints.NotBlank;
    import javax.validation.constraints.NotEmpty;
    import javax.validation.constraints.Size;
    
    public class RegisterRequest {
    
        @Email
        @NotBlank    // null 이거나 공백으로만 이루어져있으면 에러
        private String email;
        @Size(min = 6, max = 20)    // size가 지정한 값 범위에 있는지 검사
        private String name;
        @NotEmpty
        private String password;
        @NotEmpty
        private String confirmPassword;
    
        public String getEmail() {
            return email;
        }
    
        public void setEmail(String email) {
            this.email = email;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getConfirmPassword() {
            return confirmPassword;
        }
    
        public void setConfirmPassword(String confirmPassword) {
            this.confirmPassword = confirmPassword;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public boolean isPasswordEqualToConfirmPassword() {
            return password.equals(confirmPassword);
        }
    }
    
  • 이제 OptionalValidatorFactoryBean 객체를 빈으로 등록해야 한다.
    그런데 @EnableWebMvc 어노테이션을 사용하면 자동으로 등록되므로 신경 안써도 된다.
    단, 커스텀 글로벌 Validator를 만들어서 등록해놓으면 OptionalValidatorFactoryBean가 글로벌 범위 Validator로 사용 되지 않는다. 커스텀 글로벌 Validator가 있다면 삭제하자.

  • @Size 어노테이션의 경우 값이 null 이면 유효하다고 판단한다. 이렇게, null일 때 유효하다고 판단하는 어노테이션들을 쓸 때는 @NotNull 을 같이 사용해주자.

6. @InitBinder

@InitBinder 어노테이션은 컨트롤러에 들어오는 요청에 대해 preprocessing을 할 때 쓰인다.
WebDataBinder를 초기화하는 메서드에 붙인다.

 

Request 를 preprocessing 하는 예를 살펴보자.

 

다음과 같이 이름 앞, 뒤에 공백을 붙여서 가입해보았다. 그랬더니 DB에도 이 공백이 저장되었다.

 

preprocess 를 통해서 이름의 앞뒤 공백을 제거해보자.

 

package controller;

import org.springframework.beans.propertyeditors.StringTrimmerEditor;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
...

@Controller
public class RegisterController {

    ...

    @PostMapping("/register/step3")
    public String handleStep3(@Valid RegisterRequest registerRequest, Errors errors) {
        ...
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        StringTrimmerEditor stringTrimmerEditor = new StringTrimmerEditor(true);
        binder.registerCustomEditor(String.class, stringTrimmerEditor);
    }
}

 

 

앞뒤 공백이 제거되고 'j' 만 저장되었음을 볼 수 있다.

RegisterController에 있는 모든 커맨드객체에 대하여

String 필드는 이제 앞뒤 여백이 사라진 채로 들어올 것이다.

 

이러한 커맨드객체 preprocessing은 validation보다도 먼저 수행된다.

 

7. 컨트롤러 범위 Validator

package controller;

import ...

@Controller
public class RegisterController {

    ...

    @PostMapping("/register/step3")
    public String handleStep3(RegisterRequest registerRequest, Errors errors) {
        ...
    }

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.setValidator(new RegisterRequestValidator());
    }
}

 

 - WebDataBinder 는 validator 목록을 가지고있다. 이 목록에는 글로벌 범위 Validator가 기본으로 포함된다.

 - setValidator()를 사용하면 기존의 validator 목록을 삭제하고 전달받은 Validator 를 목록에 추가한다.

   즉 setValidator() 메서드를 사용하면 글로벌 범위 Validator 대신에 컨트롤러 범위 Validator를 사용하게 된다.

 


출처

초보 웹 개발자를 위한 스프링 5 프로그래밍 입문 (최범균) - ch 12

 

@InitBinder
https://medium.com/stackavenue/how-to-use-initbinder-in-spring-mvc-ecb974a6884