7. 입력 값 검증 및 BindException 클래스

스프링은 HTTP 요청 파라미터의 값을 커맨드 객체를 사용하여 저장한다. 
AbstractCommandController 클래스를 비롯하여, SimpleFormController 와 같은 하위 클래스들은 커맨드 객체에 파라미터 값을 저장한 뒤 커맨드 객체가 올바른 값을 갖는지 검사하는 검증과정을 거치게 된다.

커맨드 객체의 검증은 Validator를 통해서 이루어 진다. 

7.1 Validator 를 이용한 값 검증

org.springframework.validation.Validator 인터페이스는 객체를 검증할 때 사용된다. 
커맨드 객체를 사용하는 컨트롤러 클래스들은 커맨드 객체를 생성한 뒤 Validator에 커맨드 객체를 전달하여 검증을 요청한다.

  • boolean supports(Class clazz) : Validator가 해당 클래스에 대한 값 검증을 지원하는지의 여부를 리턴
  • void validate(Object target, Errors errors) : target객체에 대한 검증을 실행한다. 검증 결과 문제가 있을경우 errors 객체에 어떤 문제인지에 대한 정보를 저장한다.

ValidationUtils

  • 검증 관련 코드를 제공하는 유틸리티
  • http://www.springframework.org/documentation
  • 예를 들어 값이 null이거나 혹은 공백 문자만을 포함한 경우 잘못된 값으로 처리하고 싶을 때에는 ValidationUtils.rejectIfEmptyOrWhitespace() 를 사용하면 된다.         


    접기

    ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password", "required");

    접기



(1) AbstractCommandController 와 SimpleFormController 에서의 값 검증


AbstractCommandController를 상속받아 구현한 컨트롤러의 경우 validator프로퍼티를 이용하여 Validator를 등록할 수 있다.

<bean name="registMemberController" class="com.gom.study.controller.RegistMemberController"
    p:validator-ref="memberInfoValidator" />
<bean id="memberInfoValidator" class="com.gom.study.controller.MemberInfoValidator" />


두 개이상의 Validator를 등록하고 싶다면 다음과 같이 validators 프로퍼티에 <list>를 이용하여 Validator 목록을 전달하면 된다.

<bean name="registMemberController" class="com.gom.study.controller.RegistMemberController">
    <property name="validators">
        <list>
            <ref bean="memberInfoValidator" />
        </list>
    </property>
</bean>


Validator 는 검증결과를 Errors객체에 저장하는데 AbstractCommandController클래스의 경우 handle() 메서드의 BindException파라미터를 통해서 검증 결과를 전달받게 된다. 실제로 BindException클래스는 Errors인터페이스를 구현한 클래스로서 Validator에서 생성한 에러 정보를 담게 된다.

Errors 인터페이스는(즉, BindException 클래스는) hasErrors() 메서드를 제공하고 있으며, 이 메서드는 검증 결과 에러가 존재하는지의 여부를 리턴한다. 따라서 AbstractCommandController클래스를 상속받아 구현한 컨트롤러는 다음 코드와 같이 hasErrors() 메서드를 이용하여 파라미터 값을 저장한 커맨드 객체가 검증을 통과했는지의 여부를 확인할 수 있다.

public class RegistMemberContorller extends AbstractCommandController {
    protected ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) {
        if( errors.hsErrors()){
            return new ModelAndView("registMemberForm", errors.getModel() );
        } else {
            // 유효성 검사를 통과한 경우의 알맞은 처리
            return new ModelAndView("registMember", ...);
        }
    }
}


(2) AbstractWizardFormController 에서의 값 검증

AbstractWizardFormController클래스도 validator 프로퍼티나 validators 프로퍼티를 이용하여 Validator를 지정할 수 있다. 하지만 AbstractWizardFormController의 경우 1개 이상의 입력 폼에 대해서 값을 검증해 주어야 하기 때문에, 각 입력 폼에 맞는 Validator를 따로 지정해 주어야 한다.

<bean id="homepageRegistryController" class="com.gom.study.contorller.HomepageRegistryController" >
    <property name="validators">
        <list>
             <ref bean="page0Validator" />
             <ref bean="page1Validator" />
        </list>
    </property>
    <property name="pages">
        <list>
             ...
        </list>
    </property>
</bean>

<bean id="page0Validator" class="com.gom.study.controller.HomepageRegistryPage0Validator" />
<bean id="page1Validator" class="com.gom.study.controller.HomepageRegistryPage1Validator" />




AbstractWizardFormController는 각 페이지에서 폼을 전송할 경우, postProcessPage() 메서드를 호출하기 전에 다음과 같은 validatePage() 메서드를 호출하여 폼 검증을 요청한다.

@Override
protected void validatePage(Object command, Errors errors, int page, boolean finish) {
    if(page == 0) {
        ValidationUtils.invokeValidator(getValidators()[0] , command , errors );
    } else if(page == 1) {
        ValidationUtils.invokeValidator(getValidators()[1] , command , errors );
    }
}



validatePage() 메서드의 page 파라미터는 폼을 전송한 페이지 번호이며, 이 번호를 이용하여 각 입력 폼을 검증하기 위한알맞은 Validator를 이요하여 커맨드 객체를 검증할 수 있게 된다.


7.2 Errors 인터페이스와 BindException 클래스

앞서 Validator는 Error 인터페이스가 제공하는 rejectXXX() 를 이용하여 에러정보를 저장하였다. 그리고 AbstractCommandController, SimpleFormController, AbstractWizardControoler 클래스 등은 Error 인터페이스와 BindException 클래스를 파라미터로 전달 받아 에러 정보를 참고하거나 추가할 수 있었다.

  • Errors 인터페이스 : 커맨드 객체의 검증 결과를 저장한다.
  • BindingResult 인터페이스 : 요청 파라미터 값을 커맨드 객체에 복사한 결과를 저장하며, 에러 코드로부터 에러 메시지를 가져온다.
  • BindException 클래스 : 컨트롤러에 에러 정보를 전달 할 때 사용된다. 내부적으로 bindingResult 필드를 사용하여 실제 요청 처리를 위임한다. Exception 클래스를 상속받고 있기도 한다.
  • AbstractbindingResult 클래스 : BindingResult 인터페이스의 기본 구현 클래스, 검증 결과를 저장하고 에러 메시지를 추출하는 등의 기능을 제공한다.

Errors 인터페이스는 폼 자체(즉, 커맨드 객체 전체) 에 대한 에러를 저장하기 위한 reject() 메서드와 커맨드 객체의 각 프로퍼티에 대한 에러를 명시할 수 있는 rejectValue() 메서드를 제공하고 있다.

  • void reject(String errorCode)
  • void reject(String errorCode, String defaultMessage)
  • void reject(String errorCode, Object[] errorArgs, String defaultMessage)
  • void rejectValue(String field, String errorCode)
  • void rejectValue(String field, String errorCode, String defaultMessage)
  • void rejectValue(String field, String errorCode, Object[] errorArgs, String defaultMessage)


7.3 DefaultMessageCodesResolver와 에러메시지

앞서 Errors인터페이스의 reject()와 rejectValud() 메서드는 에러 코드를 사용하여 에러 정보를 전달하였다.
BindingResult 인터페이스의 기본 구현 클래스인 AbstractBindingResult 클래스는 MessageCodeResolver를 사용하여 에러 코드에 대한 에러 메시지를 추출하는데, 기본적으로 DefaultMessageCodeResolver를 MessageCodesResolver로 사용한다.

<bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
    <property name="basenames">
        <list>
            <value>messages.validation</value>
        </list>
     </property>
</bean>


예를 들어, 커맨드 객체 이름이 "loginCommand" 이고 다음과 같이 rejectValue() 메서드를 사용하여 "id"프로퍼티에 대하여 에러 코드 "required" 를 추가했다고 하자

errors.rejectValue("id" , "required" );


이 경우 사용되는 메시지 코드의 순서는 다음과 같다.

  1. required.loginCommand.id
  2. required.id
  3. required.java.lang.String
  4. required

에러 메시지를 출력할 때에는 스프링이 제공하는 <form:errors> 커스텀 태그를 사용하면 된다.
JSP가 아닌 Velocity와 같은 기술을 사용하는 경우에도 스프링은 Velocity에 알맞은 매크로를 제공하고 있어서 에러 메시지를 손쉽게 출력할 수 있다.

뷰로 JSP를 사용할 경우 두가지 방식으로 에러 메시지를 출력할 수 있다.

  1. <spring:hasBindErrors> 이용
  2. <form:form> 이용


8. 파일 업로드(multipart/form-data) 처리

웹 어플리케이션을 개발하다 보면 파일 업로드 기능이 필요할 때가 있다. 파일 업로드 처리를 위해 직접 InputStream으로부터 업로드된 데이터를 처리할 수도 있겠지만, 스프링은 업로드한 파일을 커맨드 객체에 저장할 수 있는 기능을 제공하고 있다. 따라서 개발자는 설정만으로 업로드한 파일을 커맨드 객체에 저장할 수 있다.

파일을 업로드 할 때에는 <form>태그의 enctype속성의 값을 "multipart/form-data"로 지정한다.

 

스프링이 제공하는 파일 업로드 처리 기능을 사용하려면 먼저 MultipartResolver를 빈으로 등록해 주어야 한다.
스프링은 Apache Commons FileUpload API 를 이용하여 파일 업로드를 처리하는 CommonsMultipartResolver클래스를 제공하고 있으며, 다음과 같이 스프링 설정 파일에 CommonsMultipartResolver를 빈으로 등록해 준다.
이때, 빈의 이름은 반드시 "multipartResolver" 이어야 한다. 다른 이름을 사용할 경우 MultipartResolver가 적용되지 않게 된다.

스프링 2.0까지는 COS API를 이용하여 파일 업로드를 처리하는 CosMultipartResolver를 제공했으나, 2.5버전 부터는 더이상 제공하고 있지 않다.


MultipartResolver를 등록했다면 업로드 한 파일을 커맨드 객체에 저장할 수 있게 된다. 
기본적으로 MultipartResolver를 통해 파일 업로드를 처리하면 MultipartFile타입의 프로퍼티에 업로드한 파일 정보가 저장된다. 따라서 커맨드 클래스는 업로드 한 파일정보를 저장할 프로퍼티 타입을 MultipartFile로 지정함으로써 업로드한 파일 정보를 저장할 수 있게 된다.

public class SubmitReportCommand {
    private String subject;
    private MultipartFile reportFile;
    ...
    public MultipartFile getReportFile() {
        return reportFile;
    }
    public MultipartFile setReportFile(MultipartFile reportFile) {
        this.reportFile = reportFile
    }
}


(1) MultipartFile 인터페이스의 주요기능
  • String getName() - 입력 폼의 파라미터 이름을 리턴한다.
  • String getoriginalFilename() - 업로드 한 파일의 이름을 리턴하낟. 업로드 할 파일을 선택하지 않은 경우 빈 문자열("")을 리턴한다.
  • String getContentType() - 업로드한 파일의 컨텐스 타입을 리턴한다. 타입이 정의되지 않았거나 업로드 할 파일을 선택하지 앟은 경우 null을 리턴한다.
  • boolean isEmpty() - 업로드 한 파일이 내용을 갖고 있지 않거나 또는 업로드 할 파일을 선택하지 않은 경우 true를 리턴한다.
  • long getSize() - 파일의 크기를 구한다.
  • byte[] getBytes() throws IOException - 파일의 내용을 byte 배열로 구한다.
  • InputStream getInputStream() throws IOException - 파일의 내용을 읽어 올 수 있는 InputStream을 구한다.
  • void transferTo(File dest) throws IOException, lllegalStateException - 업로드한 파일의 내용을 대상 파일(dest)에 저장한다. 만약, 대상파일이 이미 존재하면 삭제한 뒤 저장한다.

(2) CommonsMultipartResolver의 주요 파라미터

  • defaultEncoding - 기본 인코딩을 설정한다. 만약 필터를 통해서 request.setCharacterEncoding()을 통해 인코딩을 설정한 경우, 해당 인코딩을 사용한다. (ISO-8859-1)
  • maxUploadSize - 허용되는 최대 크기를 바이트 단위로 지정한다. -1인 경우 제한을 두지않는다. (-1)
  • maxInMemorySize - 업로드한 내용을 메모리에 저장할 수 있는 최대 크기를 바이트 단위로 지정한다. 지정한 크기를 넘어서면 임시 디렉토리에 파일로 저장한다. (10240)
  • uploadTempDir - 업로드 한 파일이 저장될 임시 디렉터리 경로 (서블릿 컨텡너 임시 디렉토리)


9. 어노테이션을 이요한 컨트롤러 구현
10. HandlerInterceptor를 통한 요청 가로채기



===================================================================================

웹 요청을 처리하다 보면 입력 값을 검증하는 경우가 많다..

Command 객체를 사용하는 Controller들은 Command 객체를 생성한 후 Validator가 설정되어 있으면 전달하여 검증을 요청한다.

org.springframework.validation.Validator를 구현해주면 되는데 아래와 같이 2개의 메소드가 정의되어 있다...


boolean supports(Class clazz) : 해당 클래스의 값을 검증할 것인지 여부

void validate(Object target, Errors errors) : target 객체 값에 대한 검증 실행 (잘못된 경우 errors에 기록)


검증할 클래스 타입의 객체가 전달되면 검증하고 검증 결과가 오류 있으면 errors에 기록하면 된다.

errors에 뭔가 추가가 되면 Controller에서 BindException 객체를 통해 오류가 있는지 확인할 수 있게 된다.

일단 먼저 예제를 보자...


public class TestCommandValidator implements Validator {

      public boolean supports(Class clazz) {

            return TestCommand.class.isAssignableFrom(clazz);

      }


      public void validate(Object target, Errors errors) {

            ValidationUtils.rejectIfEmptyOrWhitespace(errors, "value", "required");

      }

}


위 Validator는 TestCommand 객체를 검증하는 클래스이다.

대부분 요청 파라미터는 null이나 공백 체크이므로 ValidationiUtils를 사용하면 편하다..

rejectIfEmpty 메소드도 있으니 자세한 건 API 문서를 참고하자...

자 그럼... 다 만들었으니 아래처럼 XML 설정을 해준다...


<bean id="testController" class="com.inho.web.TestController"

      p:validator-ref="testValidator"/>


<bean id="testValidator class="com.inho.validator.TestCommandValidator"/>


참... Validator는 당연히 AbstractCommandController를 상속받은 Controller들만 사용할 수 있다..

자.. 이제 그러면 검증된 결과를 어떻게 처리하는지 살펴보자..


public class TestController extends AbstractCommandController {

      protected ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception {

            if (errors.hasErrors()) {

                  return ModelAndView("error", errors.getModel());

            }


            return ModelAndView("success");

      }

}


Validator는 검증 결과에 오류가 있으면 Errors 객체에 오류 정보를 저장하는데...

AbstractCommandController는 handle() 메소드의 파라미터인 BindException 객체에서 해당 정보를 가져올 수 있다.

해당 객체에서 hasErrors() 메소드로 오류가 있는지 확인한 후 오류 정보는 getModel() 메소드로 가져오면 된다.

BindException은 다음에 더 자세히 살펴보도록 하자...


SimpleFormController는 위와 거의 동일한데 차이점이 있다면 onSubmit() 메소드에서 오류가 있는 경우 showForm() 메소드로 입력 폼을 다시 보여준다는 것이다...


AbstractWizardFormController는 여러 단계를 거치기 때문에 Validator가 당연히 여러 개 있어야 하는데..

여러 개를 사용하고 싶다면 아래와 같이 list로 설정해주면 된다...


<bean id="testController" class="com.inho.web.TestController">

      <property name="validators">

            <list>

                  <ref bean="page0Validator"/>

                  <ref bean="page1Validator"/>

                  <ref bean="page2Validator"/>

            </list>

      </property>

</bean>


AbstractWizardFormController는 postProcessPage() 메소드를 호출하기 전에 validatePage() 메소드를 호출하므로 아래와 같이 구현해준다...


protected void validatePage(Object command, Errors errors, int page, boolean finish) {

      ValidationUtils.invokeValidator(getValidator()[page], command, errors);

}


위와 같이 페이지 순서대로 Validator를 설정해주면 if문을 사용하지 않고 간단하게 사용할 수 있다...

물론 한 페이지에서 Validator를 여러 개 사용한다면 위와 같이 해주는건 안되지만... -_-;;

ValidationUtils는 Validator를 실행하는 메소드도 있는데 저렇게 쓰기 싫으면 그냥 직접 validate() 메소드를 호출해줘도 된다...









Posted by linuxism
,