Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

[Spring 레퍼런스] 19장 포틀릿(Portlet) MVC 프레임워크 #2

이 문서는 개인적인 목적이나 배포하기 위해서 복사할 수 있다. 출력물이든 디지털 문서든 각 복사본에 어떤 비용도 청구할 수 없고 모든 복사본에는 이 카피라이트 문구가 있어야 한다.



19.6 뷰와 뷰 처리
앞에서 언급했듯이 스프링 포틀릿 MVC가 직접 스프링 웹 MVC의 뷰 기술들을 모두 재사용한다. 이는 다양한 View 구현체들을 포함하지 않고 ViewResolver 구현체들도 포함하고 있지 않다는 뜻이다. 자세한 내용은 Chapter 17, 뷰 기술와 Section 16.5, “뷰 처리”를 각각 참조해 봐라.

존재하는 View와 ViewResolver 구현체를 사용할 때 몇가지 언급해야 할 것들이 있다.

  • 대 부분의 포탈(portal)은 포틀릿을 렌더링한 결과가 HTML 조각이 되기를 기대한다. 그러므로 JSP/JSTL, Velocity, FreeMarker, XSLT등이 모두 가능하다. 하지만 포틀릿에서는 다른 타입의 문서를 반환하는 뷰는 적당하지 않다.
  • 포틀릿내에서는 HTTP 리다이렉트 같은 것이 존재하지 않는다. (ActionResponse의 sendRedirect(..) 메서드는 포탈내에서 존재할 수 없다.) 그러므로 포틀릿 MVC 내에서는 RedirectView와 'redirect:' 접두사의 사용이 제대로 동작하지 않을 것이다.
  • 포틀릿 MVC에서 'forward:' 접두사를 사용할 수도 있다. 하지만 포틀릿이므로 현재 URL이 무엇인지 알 수 없다. 즉, 웹 어플리케이션에서 다른 리소스에 접근하는 상대 URL을 사용할수 없고 절대 URL을 사용해야 한다.

또한 JSP 개발에서 새로운 스프링 Taglib와 스프링 폼 Taglib 모두 포틀릿 뷰에서도 서블릿 뷰의 동작과 완전히 똑같히 동작한다.


19.7 멀티파트(파일 업로드) 지원
스 프링 포틀릿 MVC에는 포틀릿 어플리케이션에서 웹 MVC처럼 파일 업로드를 다루는 멀티파트 지원을 내장하고 있다. 멀티파트 지원은 org.springframework.web.portlet.multipart 패키지에 정의된 플러그할 수 있는 PortletMultipartResolver 객체로 설계되었다. 스프링은 Commons FileUpload와 함께 사용할 수 있는 PortletMultipartResolver를 제공한다. 파일 업로드를 지원하는 방법은 이 섹션의 남은 부분에서 설명할 것이다.

일 부 개발자들은 멀티파트를 직접 다루기 원하므로 기본적으로 스프링 포틀릿 MVC는 멀티파트를 처리하지 않을 것이다. 웹 어플리케이션 컨텍스트에 멀티파트 리졸버를 추가해서 직접 멀티파트 지원을 활성화해야 한다. 활성화하고 나면 DispatcherPortlet이 각 요청이 멀티파트인지 검사할 것이다. 멀티파트가 아니라면 요청은 기대대로 계속 된다. 하지만 요청이 멀티파트라면 컨텍스트에 선언한 PortletMultipartResolver를 사용할 것이다. 그 다음부터는 요청의 멀티파트 속성을 다른 속성처럼 다룰 것이다.

Note
설 정한 모든 PortletMultipartResolver 빈은 반드시 "portletMultipartResolver"라는 아이디(또는 이름)을 가져야 한다. 다른 이름으로 PortletMultipartResolver를 정의했다면 DispatcherPortlet가 PortletMultipartResolver를 찾지 못하므로 멀티파트 지원이 동작하지 않을 것이다.



19.7.1 PortletMultipartResolver의 사용
다음 예제는 CommonsPortletMultipartResolver를 사용하는 방법을 보여준다.

<bean id="portletMultipartResolver" class="org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver">
  <!-- 사용할 수 있는 프로퍼티 중 하나; 파일의 최대 바이트 크기 -->
  <property name="maxUploadSize" value="100000"/>
</bean>

물 론 멀티파트 리졸버가 동작하도록 클래스 패스에 적절한 jar도 둘어야 한다. CommonsMultipartResolver의 경우 commons-fileupload.jar를 사용해야 한다. Commons FileUpload의 과거의 버전은 JSR-168 포틀릿 어플리케이션을 지원하지 않으므로 최소한 1.1 버전의 Commons FileUpload를 사용해야 한다.

포틀릿 MVC가 멀티파트 요청을 처리하도록 설정하는 방법을 보았으니 이제 실제로 어떻게 사용하는지 보자. DispatcherPortlet가 멀티파트 요청을 발견하면 DispatcherPortlet가 컨텍스트에 선언한 리졸버를 활성화하고 요청을 전달한다. 그 다음 리졸버는 ActionRequest을 멀티파트 파일 업로드를 지원하는 MultipartActionRequest로 감싼다. MultipartActionRequest를 사용해서 해당 요청의 멀티파트 정보를 얻을 수 있고 컨트롤러에서 멀티파트 파일 자체에 실제로 접근할 수 있다.

RenderRequest의 일부가 아니라 ActionRequest의 일부로만 멀티파트 파일을 받을 수 있다는 점을 기억해라.


19.7.2 폼의 파일 업로드 처리
PortletMultipartResolver 가 자신의 작업을 완료한 후에 요청은 다름 요청과 마찬가지로 처리된다. PortletMultipartResolver를 사용하려면 업로드 필드를 가진 폼(아래 예제 참고)을 생성하고 스프링이 파일을 폼(지원 객체, backing object)에 바인딩하도록 한다. 실제로 사용자가 파일을 업로드하게 하려면 (JSP/HTML) 폼을 생성해야 한다.

<h1>Please upload a file</h1>
<form method="post" action="<portlet:actionURL/>" enctype="multipart/form-data">
  <input type="file" name="file"/>
  <input type="submit"/>
</form>

여 기서 보듯이 byte[] 배열을 담고 있는 빈의 프로퍼티와 일치하는 “file”라는 이름의 필드를 생성했다. 게다가 브라우저가 멀티파트 필드를 어떻게 인코딩해야 하는지 알 수 있도록 인코딩 속성(enctype="multipart/form-data")을 추가했다.(이 부분을 잊지 마라!)

순식간에 문자열이나 프리미티브 타입으로 변환할 수 없는 다른 프로퍼티와 마찬가지로 객체에 바이너리 데이터를 보관하려면 PortletRequestDataBinder로 커스텀 에디터를 등록해야 한다. 파일을 다루고 객체에 설정하는데 사용할 수 있는 두개의 에디터가 존재한다. 파일을 문자열(사용자가 정의한 캐릭터셋(character set)을 사용해서)로 변환하는 StringMultipartFileEditor와 파일을 바이트 배열로 변환하는 ByteArrayMultipartFileEditor가 있다. 이 기능들은 CustomDateEditor와 유사하다.

그러므로 폼을 사용해서 파일을 업로드하게 하려면 리졸버, 빈을 처리할 컨트롤러에 대한 매칭과 컨트롤러를 선언한다.

<bean id="portletMultipartResolver"
    class="org.springframework.web.portlet.multipart.CommonsPortletMultipartResolver"/>

<bean class="org.springframework.web.portlet.handler.PortletModeHandlerMapping">
  <property name="portletModeMap">
    <map>
      <entry key="view" value-ref="fileUploadController"/>
    </map>
  </property>
</bean>

<bean id="fileUploadController" class="examples.FileUploadController">
  <property name="commandClass" value="examples.FileUploadBean"/>
  <property name="formView" value="fileuploadform"/>
  <property name="successView" value="confirmation"/>
</bean>

이렇게 한 후 컨트롤러와 파일 프로퍼티를 가진 실제 클래스를 생성한다.

public class FileUploadController extends SimpleFormController {

  public void onSubmitAction(ActionRequest request, ActionResponse response,
    Object command, BindException errors) throws Exception {

    // 빈을 캐스팅한다
    FileUploadBean bean = (FileUploadBean) command;

    // 내용이 존재하는지 확인한다
    byte[] file = bean.getFile();
    if (file == null) {
      // 흠.. 뭔가 잘못됐다. 사용자가 아무것도 업로드하지 않았다
    }

    // 여기서 파일로 어떤 작업을 한다
  }

  protected void initBinder(
      PortletRequest request, PortletRequestDataBinder binder) throws Exception {
    // 실제로 Multipart 인스턴스를 byte[]로 변환할 수 있게 하려면
    // 커스텀 에디터를 등록해야 한다
    binder.registerCustomEditor(byte[].class, new ByteArrayMultipartFileEditor());
    // 이제 스프링이 멀티파트 객체를 어떻게 다루고 어떻게 변환하는지 알고 있다
  }
}

public class FileUploadBean {
  private byte[] file;

  public void setFile(byte[] file) {
    this.file = file;
  }

  public byte[] getFile() {
    return file;
  }
}

여 기서 보듯이 FileUploadBean은 파일을 담고 있는 byte[] 타입의 프로퍼티를 가진다. 컨트롤러는 스프링이 멀티파트 객체를 실제로 변환하는 방법을 알게 하고 리볼버가 빈이 지정한 프로퍼티를 찾도록 하는 커스텀 에디터를 등록한다. 이 예제에서는 빈의 byte[] 프로퍼티 자체로는 아무것도 하지 않지만 실사례에서는 원하는 무엇이든지 할 수 있다.(데이터베이스에 저장하거나 누군가에게 메일로 보내는 등)

폼을 지원하는 객체(form backing object)의 문자열 타입의 프로퍼티에 직접 바인딩되는 동일한 예제는 다음과 같을 것이다.

public class FileUploadController extends SimpleFormController {

  public void onSubmitAction(ActionRequest request, ActionResponse response,
      Object command, BindException errors) throws Exception {

    // 빈을 캐스팅한다
    FileUploadBean bean = (FileUploadBean) command;

    // 내용이 존재하는지 검사한다
    String file = bean.getFile();
    if (file == null) {
      // 흠.. 뭔가 잘못됐다. 사용자가 아무것도 업로드하지 않았다
    }

    // 여기서 파일로 어떤 작업을 한다
  }

  protected void initBinder(
    PortletRequest request, PortletRequestDataBinder binder) throws Exception {

    // 실제로 Multipart 인스턴스를 문자열로 변환할 수 있게 하려면
    // 커스텀 에디터를 등록해야 한다
    binder.registerCustomEditor(String.class,
      new StringMultipartFileEditor());
    // 이제 스프링이 멀티파트 객체를 어떻게 다루고 어떻게 변환하는지 알고 있다
  }
}

public class FileUploadBean {

  private String file;

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

  public String getFile() {
    return file;
  }
}

물론 이 마지막 예제는 평범한 텍스트 파일을 업로드하는 경우에만 (논리적으로) 말이된다. (이미지 파일을 업로드하는 경우에는 제대로 동작하지 않을 것이다.)

세 번째(그리고 마지막) 선택사항은 (폼을 지원하는) 객체의 클래스에 선언한 MultipartFile 프로퍼티에 직접 바인딩하는 것이다. 이 경우에는 타입 변환을 수행할 필요가 없으므로 어떤 커스텀 프로퍼티 에디터도 등록할 필요가 없다.

public class FileUploadController extends SimpleFormController {

  public void onSubmitAction(ActionRequest request, ActionResponse response,
      Object command, BindException errors) throws Exception {

    // 빈을 캐스팅한다
    FileUploadBean bean = (FileUploadBean) command;

    // 내용이 존재하는지 검사한다
    MultipartFile file = bean.getFile();
    if (file == null) {
      // 흠.. 뭔가 잘못됐다. 사용자가 아무것도 업로드하지 않았다
    }

    // 여기서 파일로 어떤 작업을 한다
  }
}

public class FileUploadBean {

  private MultipartFile file;

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

  public MultipartFile getFile() {
    return file;
  }
}


19.8 예외 처리
서 블릿 MVC와 마찬가지로 포틀릿 MVC도 요청에 적합한 핸들러가 요청을 처리하는 도중에 발생한 의도하지 않는 예외를 쉽게 처리해주는 HandlerExceptionResolver를 제공한다. 포틀릿 MVC도 던져질 예의의 클래스명을 받아서 뷰 이름에 매핑할 수 있도록 포틀릿에 한정된 구현체 SimpleMappingExceptionResolver를 제공한다.


19.9 어노테이션에 기반한 컨트롤러 구성
스 프링 2.5는 @RequestMapping, @RequestParam, @ModelAttribute 등의 어노테이션을 사용해서 MVC 컨트롤러에 어노테이션에 기반한 프로그래밍 모델을 도입했다. 이 어노테이션 지원은 서블릿 MVC와 포틀릿 MVC 모두에서 사용할 수 있다. 이 방식으로 구현한 컨트롤러는 특정 기반 클래스를 확장하거나 특정 인터페이스를 구현하지 않는다. 게다가 원한다면 서블릿이나 포틀릿 기능에 쉽게 접근할 수 있기는 하지만 보통은 서블릿이나 포틀릿 API에 직접 의존성을 갖지 않는다.

다음 섹션에서는 이러한 어노테이션을 포틀릿 환경에서 보통 어떻게 사용하는지 설명한다.


19.9.1 어노테이션 지원에 대한 디스패처 설정
디 스패처에 대응되는 HandlerMapping(타입 수준의 어노테이션에서)이나 HandlerAdapter(메서드 수준의 어노테이션에서)이 존재하는 경우에만 @RequestMapping를 처리할 것이다. 이 동작이 DispatcherServlet과 DispatcherPortlet에서 모두 기본값이다.

하지만 커스텀 HandlerMappings이나 HandlerAdapters를 정의했다면 대응되는 DefaultAnnotationHandlerMapping나 AnnotationMethodHandlerAdapter도 역시 정의해야 한다. (@RequestMapping를 사용할 것이라면)

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <bean class="org.springframework.web.portlet.mvc.annotation.DefaultAnnotationHandlerMapping"/>

  <bean class="org.springframework.web.portlet.mvc.annotation.AnnotationMethodHandlerAdapter"/>

  // ... (컨트롤러 빈 정의) ...
</beans>

매 핑 전략을 커스터마이징한다면 DefaultAnnotationHandlerMapping이나 AnnotationMethodHandlerAdapter를 명시적으로 정의하는 것도 절적하다. 예를 들어 커스텀 WebBindingInitializer를 지정하는 것 등이다.(아래 참조)


19.9.2 @Controller로 컨트롤러 정의하기
@Controller 어노테이션은 특정 클래스가 컨트롤러의 역할을 한다는 것을 나타낸다. 여기서 어떤 기반 컨트롤러 클래스를 확장하거나 포틀릿 API를 참조할 필요가 없다. 필요하다면 여전히 포틀릿에 특화된 기능을 참조할 수 있다.

@Controller 어노테이션의 기본적인 목적은 어노테이션이 붙은 클래스에 스테레오타입으로(클래스의 역할을 나타내면서) 동작하는 것이다. 디스패처는 매핑된 메서드와 @RequestMapping 어노테이션을 참조하기 위해서 이렇게 어노테이션이 붙은 클래스를 스캔할 것이다.(다음 섹션을 참고해라.)

어노테이션이 붙은 컨트롤러 빈은 디스패처의 컨텍스트에서 표준 스프링 빈 정의를 사용해서 명시적으로 정의할 것이다. 하지만 @Controller 스테레오타입도 클래스패스의 컴포넌트 클래스 탐지와 이 클래스들의 빈 정의의 자동 등록에 대한 스프링 2.5의 일반적인 지원으로 자동탐지를 허용한다.

이렇게 어노테이션이 붙은 컨트롤러의 자동탐지를 활성화하려면 설정에 컴포넌트 스캔을 추가해야 한다. 이는 다음 XML 코드에서 보듯이 spring-context 스키마를 사용해서 쉽게 추가할 수 있다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns:p="http://www.springframework.org/schema/p"
  xmlns:context="http://www.springframework.org/schema/context"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <context:component-scan base-package="org.springframework.samples.petportal.portlet"/>

  // ...
</beans>


19.9.3 @RequestMapping로 요청 매핑하기
@RequestMapping 어노테이션은 전체 클래스나 특정 핸들러 메서드에 'VIEW'/'EDIT'같은 포틀릿 모드를 매핑하는데 사용한다. 보통 타입수준의 어노테이션은 폼 컨트롤러에 특정 모드(또는 모드에 파라미터를 추가한 상황)를 매핑하고 추가적인 메서드 수준의 어노테이션은 특정 포틀릿 요청 파라미터로 주요 매핑을 '제한'한다.

Tip
타 입 수준의 @RequestMapping은 Controller의 평범함 구현체에도 사용할 것이다. 이 경우에 요청을 처리하는 코드는 전통적인 handle(Action|Render)Request 시그니처를 따를 것이지만 컨트롤러의 매핑은 @RequestMapping 어노테이션으로 표현할 것이다. 이는 SimpleFormController같은 미리 만들어진(pre-built) Controller 기반 클래스에서도 동작한다.

다음 부분에서 어노테이션이 붙은 핸들러 메서드에 기반한 컨트롤러를 다룰 것이다.

다음은 이 어노테이션을 사용한 PetPortal 예제 어플리케이션의 폼 컨트롤러 예제이다.

@Controller
@RequestMapping("EDIT")
@SessionAttributes("site")
public class PetSitesEditController {

  private Properties petSites;

  public void setPetSites(Properties petSites) {
    this.petSites = petSites;
  }

  @ModelAttribute("petSites")
  public Properties getPetSites() {
    return this.petSites;
  }

  @RequestMapping  // 기본값 (action=list)
  public String showPetSites() {
    return "petSitesEdit";
  }

  @RequestMapping(params = "action=add")  // 렌더링 단계
  public String showSiteForm(Model model) {
    // 최초의 폼과 오류를 다시 보여주는 데 사용한다
    if (!model.containsAttribute("site")) {
      model.addAttribute("site", new PetSite());
    }
    return "petSitesAdd";
  }

  @RequestMapping(params = "action=add")  // 액션 단계
  public void populateSite(
      @ModelAttribute("site") PetSite petSite, BindingResult result,
      SessionStatus status, ActionResponse response) {

    new PetSiteValidator().validate(petSite, result);
    if (!result.hasErrors()) {
      this.petSites.put(petSite.getName(), petSite.getUrl());
      status.setComplete();
      response.setRenderParameter("action", "list");
    }
  }

  @RequestMapping(params = "action=delete")
  public void removeSite(@RequestParam("site") String site, ActionResponse response) {
    this.petSites.remove(site);
    response.setRenderParameter("action", "list");
  }
}


19.9.4 지원하는 핸들러 메서드 아규먼트
@RequestMapping 어노테이션이 붙은 핸들러 메서드는 아주 유연한 시그니처를 가질 수 있게 한다. 이 메서드는 임의의 순서(원한다면 커맨드 객체뒤에 와야 하는 대응되는 유효성 검사 결과를 제외하고)로 다음 타입의 인자를 가질 것이다.

  • 요청 또는 응답 객체(포틀릿 API). PortletRequest / ActionRequest / RenderRequest같은 특정 요청/응답 타입을 선택할 것이다. 명시적으로 선언한 action/render 인자도 핸들러 메서드에 특정 요청 타입을 매핑하는데 사용한다.(action과 render 요청을 구별하는 어떤 정보도 주어지지 않은 경우에)
  • PortletSession 타입의 세션 객체 (포틀릿 API). 이 타입의 인자는 대응되는 세션에 존재하게 강제할 것이다. 그 결과 이러한 인자는 null이 되지 않을 것이다.
  • org.springframework.web.context.request.WebRequest 나 org.springframework.web.context.request.NativeWebRequest. 네이티브 서블릿/포틀릿 API에 의존하지 않고 요청/세션 속성 접근과 마찬가지로 일반적인 요청 파라미터의 접근을 허용한다.
  • 현재 요청 로케일에 대한 java.util.Locale (포틀릿 환경의 포탈(portal) 로케일)
  • 요청의 내용에 접근하는 java.io.InputStream / java.io.Reader. 이는 포틀릿 API가 노출하는 로우(raw) InputStream/Reader가 될 것이다.
  • 응답의 내용을 생성하는 java.io.OutputStream / java.io.Writer. 이는 포틀릿 API가 노출하는 로우(raw) OutputStream/Writer가 될 것이다.
  • 특정 포틀릿 요청 파라미터에 접근하는 @RequestParam 어노테이션이 붙은 파라미터들. 파라미터 값을 선언한 메서드 인자 타입으로 변환할 것이다.
  • 웹 뷰에 노출될 암시적인(implicit) 모델을 풍부하게 하는 java.util.Map / org.springframework.ui.Model / org.springframework.ui.ModelMap.
  • @InitBinder 메서드나 HandlerAdapter 설정에 의존하면서 커스터마이징할 수 있는 타입 변환으로 빈 프로퍼티나 필드로 파라미터에 바인드할 Command/form 객체. (AnnotationMethodHandlerAdapter에 "webBindingInitializer" 프로퍼티를 봐라.) 이러한 커맨드 객체와 그 유효성검사 결과는 기본적으로 프로퍼티 표시법(notation)으로 정규화되지 않은 커맨드 클래스를 사용해서 모델 속성으로 노출될 것이다.(예를 들면 "mypackage.OrderAddress" 타입의 "orderAddress") 특정 모델 속성 이름을 선언하려면 파라미터 수준의 ModelAttribute 어노테이션을 지정해라.
  • 앞의 command/form 객체(바로 앞의 인자)에 대한 org.springframework.validation.Errors / org.springframework.validation.BindingResult 유효성검사 결과.
  • 폼 처리를 완료로 표시하는 org.springframework.web.bind.support.SessionStatus 산태 핸들. (핸들러 타입 수준의 @SessionAttributes 어노테이션이 가리키는 세션 속성을 정리한다.)
핸들러 메서드는 다음의 반환 타입을 지원한다.

  • 커맨드객체와 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자 메서드의 결과로 암묵적으로 풍부해진 모델을 가진 ModelAndView 객체.
  • RequestToViewNameTranslator로 암묵적으로 결정된 뷰 이름과 커맨드객체와 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자 메서드의 결과로 암묵적으로 풍부해진 모델을 가진 Model 객체.
  • RequestToViewNameTranslator 로 암묵적으로 결정한 뷰이름을 가진 모델을 노출하는 Map 객체로 모델은 커맨드 객체와 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자 메서드의 결과로 암묵적으로 풍부해진다.
  • 커맨드 객체와 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자 메서드로 암묵적으로 결정된 모델을 가진 View 객체. Model 인자를 선언해서(앞부분 참고) 핸들러 메서드도 모델을 프로그래밍적으로 강화할 수도 있다.
  • 뷰 이름으로 해석되는 String 값으로커맨드 객체와 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자 메서드로 암묵적으로 결정된 모델을 가진다. Model 인자를 선언해서(앞부분 참고) 핸들러 메서드도 모델을 프로그래밍적으로 강화할 수도 있다.
  • 메서드가 직접 응답을 처리하면 void이다. (예를 들면 응답 내용을 직접 작성하는 등)
  • 다 른 모든 반환 타입은 메서드 수준의 @ModelAttribute로 지정한 속성명을 사용해서(그렇지 않으면 방환 타입의 클래스에 기반한 기본 속성명) 뷰로 노출하는 단일 모델 속성으로 간주할 것이다. 모델은 커맨드 객체와 @ModelAttribute 어노테이션이 붙은 참조 데이터 접근자 메서드의 결과로 암묵적으로 풍부해진다.


19.9.5 @RequestParam로 요청 파라미터를 메서드 파라미터로 바인딩하기
컨트롤러에서 요청 파라미터를 메서드 파라미터로 바인딘하는데 @RequestParam 어노테이션을 사용한다.

PetPortal 예제 어플리케이션의 다음 코드는 @RequestParam의 사용방법을 보여준다.

@Controller
@RequestMapping("EDIT")
@SessionAttributes("site")
public class PetSitesEditController {

  // ...

  public void removeSite(@RequestParam("site") String site, ActionResponse response) {
    this.petSites.remove(site);
    response.setRenderParameter("action", "list");
  }

  // ...
}

이 어노테이션을 사용한 파라미터들은 기본적으로 필수값이지만 @RequestParam의 required 속성을 false로 지정해서 해당 파라미터를 선택적인 값으로 지정할 수 있다. (예시: @RequestParam(value="id", required=false))


19.9.6 @ModelAttribute이 붙은 모델에서 데이터로의 링크 제공하기
@ModelAttribute 은 컨트롤러에서 두가지 사용 시나리오를 가진다. 메서드 파라미터에 사용한 경우 모델 속성을 어노트에션이 붙은 특정 메서드 파라미터에 매핑할 때 사용한다.(다음 예제의 populateSite() 메서드를 봐라.) 이것이 컨트롤러가 폼에서 들어온 데이터를 가진 객체에 대한 참조를 얻는 방법이다. 게다가 파라미터를 일반적인 java.lang.Object가 아니라 폼 지원 객체(form backing object)의 특정 타입으로 선언할 수 있으므로 타입 안정성을 높혀준다.

@ModelAttribute 을 모델에 대한 참조 데이터(reference data)를 제공하려고 메서드 수준에서 사용할 수도 있다. (다음 예제의 getPetSites()를 봐라.) 이 사용방법에서는 메서드 시그니처가 위의 @RequestMapping 어노테이션에서 문서화된 것과 같은 타입을 담고 있을 수 있다.

Note: @ModelAttribute 어노테이션이 붙은 메서드는 선택된 @RequestMapping 어노테이션이 붙은 핸들러 메서드 이전에 실행될 것이다. 이 메서드들은 효율적으로 특성 속성(때로는 데이터베이스에서 가져온)을 가진 암묵적인 모델을 미리 존재하게 한다. 이러한 속성은 선택된 핸들러 메서드에서 @ModelAttribute 어노테이션이 붙은 핸들러 메서드(잠재적으로 바인딩과 유효성검사를 적용해서)를 통해서 이미 접근했을 수 있다.

다음 예제 코드는 이 어노테이션의 두가지 사용방법을 보여준다.

@Controller
@RequestMapping("EDIT")
@SessionAttributes("site")
public class PetSitesEditController {

  // ...

  @ModelAttribute("petSites")
  public Properties getPetSites() {
    return this.petSites;
  }

  @RequestMapping(params = "action=add")  // action 단계
  public void populateSite(
      @ModelAttribute("site") PetSite petSite, BindingResult result,
      SessionStatus status, ActionResponse response) {

    new PetSiteValidator().validate(petSite, result);
    if (!result.hasErrors()) {
      this.petSites.put(petSite.getName(), petSite.getUrl());
      status.setComplete();
      response.setRenderParameter("action", "list");
    }
  }
}


19.9.7 @SessionAttributes로 세션에 저장할 속성 지정하기
최 상위 @SessionAttributes 어노테이션은 특정 핸들러가 사용하는 세션 속성을 선언한다. 이는 보통 모델 속성의 이름이나 모델 속성의 타입의 목록이 될 것이고 이 모델 속성은 투명하게 세션이나 대화식 스토리지에에 저장해야 하고 뒤이은 요청간에 폼지원 빈으로 제공한다.

다음 코드는 이 어노테이션의 사용방법을 보여준다.

@Controller
@RequestMapping("EDIT")
@SessionAttributes("site")
public class PetSitesEditController {
  // ...
}


19.9.8 WebDataBinder 초기화 커스터마이징
스 프링의 WebDataBinder를 통해서 PropertyEditor 등으로 요청 파라미터 바인딩을 커스터마이징하려면 컨트롤러내에서 @InitBinder 어노테이션이 붙은 메서드를 사용하거나 커스텀 WebBindingInitializer를 제공해서 설정을 외부로 내낼 수 있다(externalize).


19.9.8.1 @InitBinder로 데이터 바인딩 커스터마이징하기
@InitBinder 어노테이션이 붙은 컨트롤러 메서드로 컨트롤러 클래스에서 웹 데이터 바인딩을 직접 설정할 수 있다. @InitBinder는 어노테이션이 붙은 핸들러 메서드의 커맨드와 폼 객체 아규먼트를 유지하는데 사용할 WebDataBinder를 초기화하는 메서드를 식별한다.

이러한 init-binder 메서드는 @RequestMapping이 지원하는 아규먼트 중 커맨드/폼 객체와 이에 대응되는 유효성검사 결과 객체를 제외한 모든 아규먼트를 지원한다. init-binder 메서드는 반드시 반환값을 갖지 않아야 한다. 그러므로 일반적으로 void로 선언한다. 전형적인 아규먼트는 컨텍스트에 특화된 에디터를 등록하는 코드를 허용하는 WebRequest나 java.util.Locale를 조합한 WebDataBinder를 포함한다.

다음 예제는 모든 java.util.Date 폼 프로퍼티에 대한 CustomDateEditor를 설정하는 @InitBinder의 사용방법을 보여준다.

@Controller
public class MyFormController {

  @InitBinder
  public void initBinder(WebDataBinder binder) {
    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    dateFormat.setLenient(false);
    binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
  }

  // ...
}


19.9.8.2 커스텀 WebBindingInitializer 설정
데 이터 바인딩 초기화를 외부화하려고 WebBindingInitializer 인터페이스의 커스텀 구현체를 제공할 수 있고 AnnotationMethodHandlerAdapter의 커스텀 빈 설정을 제공해서 활성화하므로 기본 설정을 오버라이딩한다.


19.10 포틀릿 어플리케이션 배포
스프링 포틀릿 MVC 어플리케이션을 배포하는 과정은 JSR-168 포틀릿 어플리케이션을 배포하는 과정과 다르지 않다. 하지만 이 부분은 좀 헷갈리므로 여기서 간략히 얘기하고자 한다.

보 통 포탈(portal)/포틀릿 컨테이너는 서블릿 컨테이터에서 웹앱(webapp)에서 실행되고 포틀릿은 서블릿 컨데이터의 다른 웹앱에서 실행된다. 포틀릿 컨테이너 웹앱이 포틀릿 웹앱을 호출하게 하려면 portlet.xml 파일에 정의한 포틀릿 서비스에 접근할 수 있는 잘 알려진 서블릿을 호출하는 교차컨텍스트(cross-context)를 구성해야 한다.

JSR-168 명세가 이 부분이 어떻게 이뤄지는지 정확히 명시하지 않으므로 각 포틀릿 컨테이너는 이 부분에 대한 자신만의 메카니즘을 가진다. 이 메카니즘은 보통 포틀릿 웹 앱 자체를 변경하는 종류의 "배포 과정"을 포함하고 있고 배포 과정후에 포틀릿 컨테이너 내에 포틀릿을 등록한다.

포틀릿 컨테이너가 호출할 유명한 서블릿을 주입하기 위해 포틀릿 웹앱의 web.xml를 최소한으로 수정한다. 일부의 경우 단일 서블릿이 웹앱의 모든 포틀릿을 서비스하지만 각 포틀릿마다 서블릿 인스턴스가 존재하는 경우도 있다.

일부 포틀릿 컨테이너도 웹앱에 라이브러리나 설정 파일을 주입할 것이다. 포틀릿 컨테이너도 웹앱에서 사용할 수 있는 Portlet JSP Tag Library 구현체를 구성해야 한다.

배포(deployment)를 이해하는데 중요한 요점은 대상 포탈(porta)이 필요하고 포탈과 만나도록 해야한다는 것이다.(보통은 자동화된 배포과정으로 제공한다.) 이 과정에 대한 포탈(portal)의 문서를 자세히 읽어봐라.

포틀릿을 배포하고 나면 최종 web.xml 파일이 정상정인지 살펴봐라. 일부 오래된 포탈은 ViewRendererServlet 정의를 잘못되게 하므로 포틀릿 렌더링을 깨뜨린다.
2013/05/02 02:11 2013/05/02 02:11