[Spring] MVC2: 메세지, 커맨드 객체 검증

첫걸음 - 12

Posted by owin2828 on 2019-12-30 16:35 · 16 mins read

1. <spring:message> 태그로 메세지 출력


  • 기존 JSP에 문자열을 하드 코딩하는 방법은 다음과 같은 두 가지 문제가 존재
    1. 동일한 문자열에 대해 변경
    2. 다국어 지원
  • 이러한 문제를 해결하는 방법은 다음과 같다.
    1. 뷰 코드에서 사용할 문자열을 언어별로 파일에 보관
    2. 필요시 파일에서 문자열을 읽어와 출력
  • 스프링은 이러한 기능을 자체적으로 제공하며, 다음과 같은 작업을 거친다.
    1. 문자열을 담은 메세지 파일을 작성
    2. 메세지 파일에서 값을 읽어오는 MessageSource 빈을 설정
    3. JSP 코드에서 <spring:message> 태그를 사용해 메세지를 출력
      // MvcConfig.java
      @Configuration
      @EnableWebMvc
      public class MvcConfig implements WebMvcConfigurer {
          ...
          @Bean
              // Bean 등록
          public MessageSource messageSource() {
              ResourceBundleMessageSource ms = 
                      new ResourceBundleMessageSource();
                      // 읽어올 파일 등록
              ms.setBasenames("message.label");
              ms.setDefaultEncoding("UTF-8");
              return ms;
          }
      }
    
  • 설정파일에 MessageSource Bean 객체를 등록

    이때 Bean의 아이디를 "messageSource"로 지정, 다른 이름은 동작하지 않음

  • 기본 프로퍼티 값으로 message.label을 할당
    • src/main/resources/message 경로에 label.properties 파일을 생성 후 아래와 같이 작성
      // label.properties
      member.register=회원가입
    
      term=약관
      term.agree=약관동의
      ...
    
  • 위 설정을 이용하여 JSP 파일을 다음과 같이 변경 가능
      <!-- step1.jsp -->
      <%@ page contentType="text/html; charset=utf-8" %>
      <%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
      <!DOCTYPE html>
      <html>
      <!-- 변경전 코드
      <head>
          <title>회원가입</title>
      </head>
      -->
      <head>
          <title><spring:message code="member.register" /></title>
      </head>
      ...
    
  • <spring:message> 태그는 MessageSource로부터 코드에 해당하는 메세지를 읽어옴

1-1. 메세지 처리를 위한 MessageSource와 <spring:message> 태그

  • 스프링은 지역(로케일)에 상관없이 일괄된 방법으로 문자열(메세지)를 관리할 수 있는 MessageSource 인터페이스를 정의
  • 특정 지역에 해당하는 메세지가 필요한 코드는 MessageSource의 getMessage() 메서드를 통해 필요한 메세지를 가져와 사용
  • 이 기능을 사용하면 국내에서 접근하면 한글로, 해외에서 접근하면 영어로 보여주는 처리 가능
      // MessageSource interface
      public interface MessageSource{
          /*
          * code 파라미터: 메세지를 구분하기 위한 코드
          * locale 파라미터: 지역을 구분하기 위한 Locale
          * 이 기능을 사용하면 지역에 따른 메세지 출력 가능
          */
          String getMessage(String code, Object[] args,
              Srting defaultMessage, Locale locale);
    
          String getMessage(String code, Object[] args, Locale locale)
              throws NoSuchMessageException;
          ...
    

    다국어 메세지를 지원하려면 각 프로퍼티 파일 이름에 언어에 해당하는 로케일 문자를 다음과 같이 추가하며
    해당하는 지역이 없을 경우, 기본값인 label.properties 파일의 메세지를 사용

    • label_ko.properties
    • label_en.properties

    브라우저는 서버에 요청을 전송할 때 Accept-Language 헤더에 언어 정보를 담아 전송

    • 한글일 경우 헤더값으로 “ko“를 전송

    Spring MVC는 웹 브라우저가 전송한 Accept-Language 헤더를 이용해서 Locale을 구하고 메세지를 출력함

1-2. <spring:message> 태그의 메세지 인자 처리

  • <spring:message> 태그를 사용할 때는 arguments 속성을 이용해 인덱스 기반 변수값을 다음과 같이 전달
  • properties 파일에는 다음과 같이 사용할 인덱스의 정보를 이용해 메세지를 저장
      <!-- step3.jsp -->
      <spring:message code="register.done" arguments="${registerRequest.name}, ${registerRequest.email}" />
    
      <!-- label.properties -->
      register.done=<strong>{0}님 ({1})</strong>, 회원 가입을 완료했습니다. 
    


2. 커맨드 객체의 값 검증과 에러 메세지 처리


  • 어플리케이션을 개발할 때 놓쳐서는 안되는 다음 두 가지 문제가 존재
    1. 폼 값 검증 - 웹 페이지에 입력한 값에 대한 검증 필요
    2. 에러 메세지 처리 - 만약 잘못된 값을 입력하여 다시 페이지가 로드 된다면, 이유를 알려줌
  • 스프링은 이 두 가지 문제를 처리하기 위해 다음 방법을 제공
    1. 커맨드 객체를 검증하고 결과를 에러 코드로 저장
    2. JSP에서 에러 코드로부터 메세지 출력

2-1. 커맨드 객체 검증과 에러 코드 지정

  • 스프링 MVC에서 커맨드 객체 값이 올바른지 검사하려면 다음의 두 인터페이스를 사용
    1. org.springframework.validation.Validator
    2. org.springframework.validation.Errors
  • 객체를 검증할 때 사용하는 Validator 인터페이스는 다음과 같다.
      package org.springframework.validation;
    
      public interface Validator{
          boolean supports(Class<?> clazz);
          void validate(Object target, Errors errors);
      }
    
  • 위 코드에서 각 매서드의 역할은 다음과 같다.
    • supports() 매서드는 Validator가 검증할 수 있는 타입인지 검사
    • validate() 매서드는 첫 번째 파라미터로 전달받은 객체를 검증하고 오류 결과를 Errors에 담는 기능
      // RegisterRequesValidator.java
      public class RegisterRequestValidator implements Validator {
          ...
          /*
              * 파라미터로 전달받은 clazz 객체가 RegisterRequest 클래스로 타입 변환이 가능한지 확인  
              * 스프링 MVC가 자동으로 검증 기능을 수행하도록 하려면 올바르게 다음 매서드를 구현해야 함
              */
          @Override
          public boolean supports(Class<?> clazz) {
              return RegisterRequest.class.isAssignableFrom(clazz);
          }
    
          /*
              * 두 개의 파라미터중 target은 검사 대상 객체, errors는 결과 에러 코드를 설정하기 위한 객체
              * validate()는 다음과 같이 구현
              * 1. 검사 대상 객체의 특정 프로퍼티나 상태가 올바른지 검사 
              * 2. 올바르지 않다면 Errors의 rejectValue() 매서드를 이용해 에러 코드 저장
              */
          @Override
          public void validate(Object target, Errors errors) {
              System.out.println("RegisterRequestValidator#validate(): " + this);
    
              // 실제 타입으로 변환
              RegisterRequest regReq = (RegisterRequest) target;
                
              /*
                      * "email" 프로퍼티 값이 유효한지 검사
              * 유효하지 않다면(NULL이거나 빈 문자열) 에러 코드로 "required"를 추가
              */
              if (regReq.getEmail() == null || regReq.getEmail().trim().isEmpty()) {
                  errors.rejectValue("email", "required");
              }
    
              /*
                      * ValidationUtils 클래스는 객체의 값 검증 코드를 간결하게 작성할 수 있도록 도와줌
              * 다음의 코드들은 앞선 email을 검증하는 코드와 동일
              */
              ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "required");
              ValidationUtils.rejectIfEmpty(errors, "password", "required");
              ValidationUtils.rejectIfEmpty(errors, "confirmPassword", "required");
              if (!regReq.getPassword().isEmpty()) {
                  if (!regReq.isPasswordEqualToConfirmPassword()) {
                      errors.rejectValue("confirmPassword", "nomatch");
                  }
              }
          }
    
      }
    
  • 실제 Validator()는 다음과 같이 필요한 부분에서 호출 됨
      // RegisterController.java
      @Controller
      public class RegisterController {
          ...
          @PostMapping("/register/step3")
          public String handleStep3(RegisterRequest regReq, Errors errors) {
              new RegisterRequestValidator().validate(regReq, errors);
              if (errors.hasErrors())
                  return "register/step2";
              try {
                  memberRegisterService.regist(regReq);
                  return "register/step3";
              } catch (DuplicateMemberException ex) {
                  errors.rejectValue("email", "duplicate");
                  return "register/step2";
              }
          }
      }
    


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

  • 에러 코드는 알맞은 에러 메세지를 출력하기 위해 지정
  • JSP는 스프링이 제공하는 <form:errors> 태그를 이용해 에러에 해당하는 메세지를 출력
      <!-- step2.jsp -->
      ...
      <body>
          <h2><spring:message code="member.info" /></h2>
          <form:form action="step3" modelAttribute="registerRequest">
          <p>
              <label><spring:message code="email" />:<br>
              <form:input path="email" />
              <form:errors path="email"/>
              </label>
          </p>
      ...
    
  • <form:errors> 태그의 path 속성은 에러 메세지를 출력할 프로퍼티 이름을 지정
  • 메세지를 찾을 때는 앞서 서술한 MessageSource를 사용하므로 에러 코드에 해당하는 메세지를 프로퍼티 파일에 추가해야 함

    Controller에서 에러가 발생시, Errors 객체에 추가하는 메시지프로퍼티 파일에 다음과 같이 추가하면 됨

    required = 필수항목입니다.
    bad.email = 이메일이 올바르지 않습니다.

3. 글로벌 범위 Validator와 컨트롤러 범위 Validator


스프링 MVC는 다음 두 가지 Validator를 제공

  1. 모든 컨트롤러에 적용할 수 있는 글로벌 Validator
  2. 단일 컨트롤러에 적용할 수 있는 Validator

3-1. 글로벌 범위 Validator 설정과 @Valid 어노테이션

  • 글로벌 범위 Validator를 적용하려면 다음 두 가지 설정 필요
    1. 설정 클래스에서 WebMvcConfigurer 인터페이스의 getValidator() 매서드가 Validator 구현 객체를 리턴하도록 구현
    2. 글로벌 범위 Validator가 검증할 커맨드 객체에 @Valid 어노테이션 적용
      // MvcConfig.java
      @Configuration
      @EnableWebMvc
      public class MvcConfig implements WebMvcConfigurer {
          /*
              * WebMvcConfigurer 인터페이스의 getValidator 매서드를 다음과 같이 오버라이딩하여
              * 우리가 원하는 검증을 하도록 설정
              */
          @Override
          public Validator getValidator(
              return new RegisterRequestValidator();
          }
      }
    
      // RegisterController.java
      @PostMapping("/register/step3")
      public String handleStep3(@Valid RegisterRequest regReq, Errors errors){
          // 기존에 객체를 검증하는 코드를 작성할 필요가 없어짐  
          // new RegisterRequestValidator().validate(regReq, errors);
          ...
      }
    
  • @Valid 어노테이션이 붙은 파라미터는 글로벌 범위 Validator가 해당 타입을 검증할 수 있는지 확인
  • 검증 가능하면 실제 검증을 수행하고 그 결과를 Errors에 저장
  • 이 과정은 요청 처리 매서드(handleStep3()) 실행 전에 적용

    결국 handleStep3() 매서드 안에서 RegisterRequest 객체를 검증하는 코드를 작성할 필요가 없어짐

3-2. @InitBinder 어노테이션을 이용한 컨트롤러 범위 Validator

  • @InitBinder 어노테이션을 이용해 컨트롤러 범위 Validator를 설정 가능

      // RegisterController.java
      @PostMapping("/register/step3")
      public String handleStep3(@Valid RegisterRequest regReq, Errors errors){
          ...
      }
      ...
    
      /* 
      * 어떤 Validator가 객체를 검증할지 다음 @InitBind 어노테이션이 붙은 매서드에서 정의
      * 여기서는 RegisterRequest 타입을 지원하는 Validator를 컨트롤러 범위 Validator로 설정
      */
      @InitBinder
      protected void initBinder(WebDataBinder binder){
          binder.setValidator(new RegisterRequestValidator());
      }
    
  • 글로벌 범위 validator와 마찬가지로 validator() 매서드를 호출하는 부분이 없음
  • @InitBind 어노테이션을 적용한 매서드는 WebDataBinder 타입 파라미터를 가짐
  • setValidator() 매서드를 이용해 컨트롤러 범위에 적용할 Validator 설정 가능

    글로벌 범위와 컨트롤러 범위 validator()가 모두 존재할 경우,
    글로벌 -> 컨트롤러 범위 순으로 validator()가 적용됨

4. Bean Validation을 이용한 값 검증 처리


@Valid 어노테이션은 Bean Validation 스펙에 정의되어 있는데, @NotNull, @Digits, @Size 등의 어노테이션과 함께 정의되며,
Bean Validaion이 제공하는 어노테이션을 이용해 커맨드 객체의 값을 검증하는 방법은 다음과 같다.

  1. Bean Validation과 관련된 의존을 설정에 추가(Maven의 경우 pom.xml에 추가)
  2. 커맨드 객체에 @NotNull, @Digits 등의 어노테이션을 이용해 검증 규칙을 설정

     // RegisterRequest.java
     public class RegisterRequest {
         @NotNull
         @Email
         private String email;
         @Size(min=6)
         private String password;
         @NotEmpty
         private String confirmPassword;
         @NotEmpty
         private String name;
     ...
    
  • 위와 같은 어노테이션을 사용했다면 Bean Validation 어노테이션을 적용한 커맨드 객체를 검증할 수 있는
    OptionalValidatorFactoryBean 클래스를 Bean으로 등록
  • 설정 파일에 @EnableWebMvc 어노테이션을 사용해 OptionalValidatorFactoryBean글로벌 범위 Validator로 등록
  • 다음과 같이 @Valid 어노테이션을 붙여 글로벌 범위 Validator로 검증 가능
  • 각 어노테이션의 에러 메세지는 Bean validation 프로바이더가 제공하는 기본 에러 메세지를 출력

    @NotNull: 반드시 값이 존재하고 공백 문자를 제외한 길이가 0보다 커야 합니다.
    @NotEmpty: 반드시 값이 존재하고 길이 혹은 크기가 0보다 커야 합니다.

      @PostMapping("/register/step3")
      public String handleStep3(@Valid RegisterRequest regReq, Errors errors) {
          new RegisterRequestValidator().validate(regReq, errors);
          if (errors.hasErrors())
              return "register/step2";
          try {
              memberRegisterService.regist(regReq);
              return "register/step3";
          } catch (DuplicateMemberException ex) {
              errors.rejectValue("email", "duplicate");
              return "register/step2";
          }
      }
    

    만약 글로벌 validator가 따로 설정되어 있다면,
    Spring은 OptionalValidatorFactoryBean를 글로벌 범위 Validator로 사용하지 않음
    따라서 따로 설정한 글로벌 범위 validator는 삭제해 주어야 함

4-1. Bean Validation 에러 메세지 커스터마이징

  • 기본 에러 메세지 대신, 원하는 에러 메세지를 사용하기 위해 다음과 같이 properties 파일에 작성

      NotBlank=필수 항목입니다. 공백 문자는 허용하지 않습니다.
      NotEmpty=필수 항목입니다.
      Size.password=암호 길이는 6자 이상이어야 합니다.
      Email=올바른 이메일 주소를 입력해야 합니다.
    

    @NotBlank: 필수 항목입니다. 공백 문자는 허용하지 않습니다.
    @NotEmpty: 필수 항목입니다.