Vlidator – ValidationUtils 사용하기

Spring에서 Validator를 구현하는 방법은 두 가지가 있습니다.
1. Programmatic
2. Declarative

그 중에서 첫 번째 Programmatic 방법을 사용하여 구현할 때 ValidationUtils를 사용하면 매우 간단하게 구현할 수 있습니다.
사용자 삽입 이미지인터페이스 중에 인자가 네개인 녀석을 사용하여 defaultMessage를 주면 프로퍼티 파일을 만들지 않아도 메시지를 출력할 수 있습니다.

1. Validator 만들기

public class MemberInfoValidator implements Validator{

    public boolean supports(Class clazz) {
        return MemberInfo.class.isAssignableFrom(clazz);
    }

    public void validate(Object object, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, “email”, “required”, “Enter your email”);
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, “password”, “required”, “Enter your password”);
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, “confirmPassword”, “required”, “Enter the same password for confirmation”);
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, “confirmMember”, “required”, “Enter ajn member code”);
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, “name”, “required”, “Enter your name”);
    }

}

Validator 인터페이스를 구현하고 ValidationUtils를 사용하여 간단하게 구현할 수 있습니다.

2. Controller에 등록하기

  public CreateMemberInfoController() {
        setCommandClass(MemberInfo.class);
        setCommandName(“memberInfo”);
        setFormView(“createMemberInfo”);
        setSuccessView(“viewMemberList”);
        setValidator(new MemberInfoValidator());
    }

setValidator 메소드를 사용하여 위에서 만든 Validator를 등록해 줍니다.

3. 화면에 보여주기

<form:errors path=”속성 이름” />

이런식으로 속성 이름을 적어주면 그 이름에 해당하는 에러 메시지를 고자리에 출력해 줍니다.

사용자 삽입 이미지

<form:errors path=”*” />

이렇게 써주면 저 태그가 들어간 위치에 모든 에러 메시지를 모아서 출력할 수 있습니다. 좋군요~
사용자 삽입 이미지

13.10. Handling exceptions

특정 예외가 발생했을 때 특정 페이지로 이동 시킬 수 있습니다.

먼저 컨트롤러든 서비스 단이든 벨리데이션 할 때든 에러를 발생시킵니다. 예외 처리 할 것도 없기 때문에 RuntimeException이 좋겠네요.

    public void add(MemberInfo memberInfo) {
        if(memberInfo.getConfirmMember().equals(“ajnapaqj”)){
            String email = memberInfo.getEmail();
            if(memberInfoDao.findByEmail(email) == null)
                memberInfoDao.insert(memberInfo);
            else
                throw new AlreadyExistEmailException();
        }
        else
            throw new InvalidMemberCodeException();
    }

저는 서비스 단에 구현했습니다. 사실 고민이 됩니다. 이런 거는 벨리데이터에 넣어야 필드 값 검사할 때 같이 하면 좋을 것 같은데 아직 벨리데이터를 만들지 않았기 때문에 간단하게 서비스 단에 구현해 뒀습니다. 나중에 옮겨야 벨리데이터 만들고 옮겨야 겠습니다.

그리고 사용자 예외를 만들어 줍니다.

public class AlreadyExistEmailException extends RuntimeException {}
public class InvalidMemberCodeException extends RuntimeException {}

그리고 Spring의 ExceptionResolver를 등록해줍니다.

    <!– Exception Resolver –>
    <bean id=”exceptionMapping”
        class=”org.springframework.web.servlet.handler.SimpleMappingExceptionResolver”>
        <property name=”exceptionMappings”>
            <props>
                <prop key=”InvalidMemberCodeException”>
                    /exception/joinFailedByMemberCode
                </prop>
                <prop key=”AlreadyExistEmailException”>
                    /exception/joinFailedByExistingEmail
                </prop>
            </props>
        </property>
    </bean>

이런식으로 Exception과 view 이름을 매핑 시켜 주면 view 리졸버에 의해 view를 찾게 됩니다.

Intercepting requests 예제

어떤 서비스를 로그인 한 유저에게만 제공하고 싶다면…

request를 처리하는 메소드 내에서

if (request.getSession.getAttribute(“user”) != null)
    throw LoginRequiredException();

이런식으로 예외를 발생시키고 이 예외가 발생할 때마다 로그인 페이지로 이동 시키게 할 수 있습니다.

하지만 저런 코드가 여러 부분에서 필요하게 될 것이고 이것을 AOP를 적용하여 Aspect로 빼내고 싶어지는 것이 당연합니다.

그런데 Spring AOP가 조금 복잡하자나열(2.0 에서는 많이 나아졌지만)…pointcut 정의 해줘야 하는데 문법도 좀 희한하고 advice 종류도 다양한데 특정 인자를 받아와야 하는 방법을 써야 하니까 args 표현식을 써야 하고.. 등등 복잡합니다.

그래서.. 간단하게 HadlerMapping과 HandlerInterceptor 를 사용해서 AOP를 적용할 수 있습니다. 요녀석을 사용하면 HttpRequest와 HttpResponse를 인자로 받는 Around Advice를 적용하는 기분이 들 것입니다.

1. Handler Mapping 등록하기
– Handler Mapping을 여러 개 사용할 수 있습니다. 그리고 그 들간의 순서를 order 속성을 사용하여 지정해 줄 수 있습니다. viewResolver를 여러 개 등록하는 거랑 같습니다.

    <bean id=”handlerMapping” class=”org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping”>
        <property name=”order” value=”2″ />
    </bean>

기존에 사용하던 HandlerMapping은 BeanNameUrlHandlerMapping으로 암묵적으로[footnote]명시적으로 handleMapping을 등록하지 않으면 DispacherSevlet이 알아서 이녀석을 만들어서 사용하기 떄문이빈다.[/footnote] 사용하고 있었습니다.

이 녀석을 위에 보시다시피 명시적으로 표기해 주고 순서를 두 번째로 지정합니다. 다음으로는 User를 체크해야 할 request를 처리할 HandlerMapping을 등록합니다.

<bean id=”userCheckHandlerMapping”
          class=”org.springframework.web.servlet.handler.SimpleUrlHandlerMapping”>
        <property name=”order” value=”1″ />
        <property name=”interceptors”>
            <list>
                <ref bean=”memberCheckInterceptor“/>
            </list>
        </property>
        <property name=”mappings”>
            <value>
                /board/createArticle.html = createArticleController
                /board/createReply.html = createReplyController
                /createAuction.html = createLPAuctionController
                /auction/insertAuctionReply.html = insertAuctionReplyController
                /fileUpload.html = uploadFileController
            </value>
        </property>
    </bean>

SimpleUrlHandlerMapping을 사용하여 다섯개의 request를 처리할 것입니다. 저 request 들은 모두 memberCheckInterceptor를 거쳐가게 되어있습니다.

따라서 이제 memberCheckInterceptor만 구현해 주면 구현이 끝납니다.

2. Interceptor 만들기

public class MemberCheckInterceptor extends HandlerInterceptorAdapter {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(request.getSession().getAttribute(“user”) != null)
            return true;
        else {
            response.sendRedirect(“http://localhost:8080/classicMania/login.html”);
            return false;
        }
    }
}

와.. 간단합니다. user라는 이름의 객체가 세션에 있으면 계속 request를 처리하고 아니면 login 페이지로 넘어가도록 합니다. true는 계속 처리 false는 처리 안하게 합니다. Around Advice에서 해당 메소드를 실행 할지 말지 선택할 수 있었던 것과 유사하지 않나효.

3. 실행


로그인 안 한 상태에서 파일 업로드 페이지로 가려고 하면 => 로그인 페이지로 이동
로그인 한 상태라에서 파일 업로드 페이지로 가려고 하면 => 파일 업로드 페이지 요청을 처리합니다.

MultipartResolver 사용하여 파일 업로드 구현 예제

1. 사용할 Resolver 선택 및 등록
1.1. 필요한 jar 파일 추가

2. 폼 만들기

3. 모델 객체에 파일 다룰 속성 추가
3.1. String, bate[], MultupartFile 중 어떤 타입으로 받을 것인가 선택 및 코딩

4. 컨트롤러 만들기
(4.0. 바인딩 할 꺼면-String, byte[] 사용할 때- PropertyEditor 사용해서 바인딩 하기)
4.1. 디렉토리 설정
4.2. 디렉토리로 원본의 사본을 만들어 넣어 둡니다.

제 나름대로 책보고 예제 만들어 보다가 정리한 순서이지 꼭 저 위에 있는 순서대로 해야 하는건 아닙니다.

1. 사용할 Resolver 선택 및 등록

Cos와 Commons 라이브러리 중에 선택할 수 있습니다. Spring Reference, Pro Spring, Spring MVC 책 모두 Commons 라이브러리를 사용하고 있었습니다. 그래서 저도 그냥 Commons 라이브러리 사용했습니다.

1.1 필요한 Jar 파일 추가

Commons 라이브러리를 사용할 때 필요한 jar 파일은 두 개 입니다. 레퍼런스에는 commons-fileupload.jar 만 필요하다고 나오는데 저 jar 가 commons-io.jar 에 의존성을 가지고 있기 때문에 commons-io.jar도 추가해야 합니다.

2. 폼 만들기

간단합니다. <input type=”file”> 로 해주면 파일 업로드 할 수 있는 버튼이 추가 됩니다. 이 때 폼태그에는 추가해줘야 할 것이 있습니다. request 타입을 multipart를 포함하는 request임을 알려주기 위한 것 같네요.

<form:form commandName=”fileCommand” method=”post” enctype=”multipart/form-data”>
    파일 선택 <input type=”file” name=”file”/><br/>
    <input type=”submit” value=”Submit”/>
</form:form>

3. 모델 객체에 파일 다룰 속성 추가

굳이 모델 객체에 추가하지 않고 커맨드 객체를 사용할 것이라면 커맨드 객체에 속성을 추가해 줍니다. 하지만 전 객체 하나 만드는 것 보다 필드 하나 추가하는 걸 선택했습니다. 따라서 FILE 도메인에 속성 하나를 추가할 것입니다.

3.1. String, bate[], MultupartFile 중 어떤 타입으로 받을 것인가 선택 및 코딩

폼에서 입력되는 파일을 객체의 속성으로 받기 위해 Spring에서 지원해주는 기능으로 사용할 수 있는 타입은 String, byte[], MultipartFile 세가지 입니다. 이 중에서 File 타입으로 받아서 File의 이름과 사이즈 등을 구하려면 역시 MultipartFile을 선택하는 것이 편하겠습니다.

    private String name;
    private Long size;
    private String filePath;
    private MultipartFile file;


4. 컨트롤러 만들기

4.0 String, bytep[], Multipart 로 바인딩 하기

Spring Reference 13.8.3에 보면 예제가 잘 나와있습니다. 저 세가지 중에서 String과 byte[]로 바인딩 할 때는 Spring이 제공하는 별도의 PropertyEditor를 등록해줘야 합니다. 하지만 MultipartFile은 그럴 필요가 없이 그냥 알아서 바인딩 해줍니다.

4.1. 디렉토리 설정

Spring 워크북에는 조금 다른 방식으로 등록했지만 저는 Spring MVC에 나와있는 예제가 사용한 방식을 사용했습니다.
컨트롤러에 File 타입으로 디렉토리를 속성으로 주고 setter injection을 사용합니다.

    <!– MusicFile  –>
    <bean name=”/fileUpload.html”
        class=”com.classicMania.controller.file.UploadFileController”>
        <property name=”destinationDir” value=”D://down” />
    </bean>

4.2. 디렉토리로 원본의 사본을 만들어 넣어 둡니다.

Spring이 제공하는 FileCopyUtils 를 사용하여 간단하게 원본(웹에서 입력하고 있는 파일)을 사본(위에서 설정한 디렉토리로)을 만들 수 있습니다.

컨트롤러의 소스코드를 보면 다음과 같습니다.

public class UploadFileController extends SimpleFormController implements InitializingBean {
    // 4.1
    private File destinationDir;

    public void setDestinationDir(File destinationDir) {
        this.destinationDir = destinationDir;
    }

    public UploadFileController() {
        setCommandName(“fileCommand”);
        setCommandClass(FILE.class);
        setFormView(“file/uploadFile”);
        setSuccessView(“file/viewFileList”);
    }

    public void afterPropertiesSet() throws Exception {
        if (destinationDir == null) {
            throw new IllegalArgumentException(“Must specify destinationDir”);
        }
        else if (!destinationDir.isDirectory() && !destinationDir.mkdir()) {
            throw new IllegalArgumentException(destinationDir + ” is not a ” + “directory, or it couldn’t be created”);
        }
    }

    @Override
    protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object command,
            BindException exception) throws Exception {
        response.setContentType(“text/plain”);
        if (! (request instanceof MultipartHttpServletRequest)) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, “Expected multipart request”);
            return null;
        }
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
        MultipartFile file = multipartRequest.getFile(“file”);
        String fileName = file.getOriginalFilename();
        File destination = File.createTempFile(“file”, fileName, destinationDir);
        //4.2
        FileCopyUtils.copy(file.getInputStream(), new FileOutputStream(destination));

        Member member = (Member) request.getSession().getAttribute(“user”);

        FILE newFile = (FILE) command;
        newFile.setMember(member);
        newFile.setFilePath(destination.getAbsolutePath());
        newFile.setName(file.getOriginalFilename());
        newFile.setSize(file.getSize());
        newFile.setFile(file);
        ServiceManager.getMemberService().update(member);
        ServiceManager.getFileService().add(newFile);

        Map<String, Object> model = new HashMap<String, Object>();
        model.put(“files”, ServiceManager.getFileService().getAll());
        model.put(“owner”, member.getName());
        return new ModelAndView(getSuccessView(), model);
    }

}

아~ Spring 좋아라~ 너무 좋아라~

파일 업로드 화면
사용자 삽입 이미지파일 선택 화면
사용자 삽입 이미지파일 업로드 선택
사용자 삽입 이미지파일 업로드 결과
사용자 삽입 이미지
위 컨트롤러에서 설정한 패스(D:\down)에 잘 들어갔습니다.

13.8. Spring’s multipart (fileupload) support

13.8.1. Introduction

MultipartResolver 인터페이스를 사용하여 파일 업로드를 구현할 수 있습니다. 내부 구현체는 Commons FileUpload 와 COS FileUpload 를 사용합니다.

기본적으로 Spring은 Multipart 를 지원하진 않지만 Multipart를 포함하는 요청이 들어오면 Context에 설정해둔 MutipartResoolver를 사용하여 적절하게 처리할 수 있습니다.

13.8.2. Using the MultipartResolver

Multipart 기능을 사용하기 위해 먼저 사요할 MultipartResolver를 bean으로 등록해야 합니다.

CommonsMultipartResolver 등록하기 :: commons-fileupload.jar, commons-io.jar 필요함

<bean id=”multipartResolver”
    class=”org.springframework.web.multipart.commons.CommonsMultipartResolver”>

    <!– one of the properties available; the maximum file size in bytes –>
    <property name=”maxUploadSize” value=”100000″/>
</bean>

CosMultipartResolver 등록하기 :: cos.jar 필요함

<bean id=”multipartResolver” class=”org.springframework.web.multipart.cos.CosMultipartResolver”>

    <!– one of the properties available; the maximum file size in bytes –>
    <property name=”maxUploadSize” value=”100000″/>
</bean>

13.8.3. Handling a file upload in a form

화면에서 파일 입력 받기.

<html>
    <head>
        <title>Upload a file please</title>
    </head>
    <body>
        <h1>Please upload a file</h1>
        <form method=”post” action=”upload.form” enctype=”multipart/form-data”>
            <input type=”file” name=”file”/>
            <input type=”submit”/>
        </form>
    </body>
</html>

화면에서 입력받은 파일을 객체로 바인딩 해야 하는데 이 때 String 타입으로 바인딩하려면 StringMultipartEditor, byte타입의 배열로 바인딩 하려면 ByteArrayMultipartEditor 를 사용할 수 있습니다.

byte 타입의 배열 이나 String으로 바인딩하지 않고 MultipartFile 타입으로 받으려면 별다른 바인딩이 필요 없습니다.

이 세가지 방법 중 String으로 바인딩하는 예제를 보겠습니다.

public class FileUploadController extends SimpleFormController {

    protected ModelAndView onSubmit(
        HttpServletRequest request,
        HttpServletResponse response,
        Object command,
        BindException errors) throws ServletException, IOException {

         // cast the bean
        FileUploadBean bean = (FileUploadBean) command;

         let’s see if there’s content there
        String file = bean.getFile();
        if (file == null) {
             // hmm, that’s strange, the user did not upload anything
        }

         // well, let’s do nothing with the bean for now and return
        return super.onSubmit(request, response, command, errors);
    }

    protected void initBinder(HttpServletRequest request, ServletRequestDataBinder binder)
        throws ServletException {
        // to actually be able to convert Multipart instance to a String
        // we have to register a custom editor
        binder.registerCustomEditor(String.class, new StringMultipartFileEditor());
        // now Spring knows how to handle multipart object and convert them
    }

}

public class FileUploadBean {

    private String file;

    public void setFile(String file) {
        this.file = file;
    }

    public String getFile() {
        return file;
    }
}

오호.. 간단하군요. 집에가서 써먹어 봐야겠습니다.