Whiteship's Note


스프링 MVC form 태그 써 보셨어요?

Spring/Chapter 13 : 2008.11.26 15:14


귿이에요.

<form:checkboxes items="${allAuthorities}" path="authorities" delimiter="<br/>" itemLabel="name"  itemValue="id" />

단 한 줄로..


저렇게 출력해줍니다. 괜찮죠? EL로 넘겨준 allAuthorities 이 녀석은 List 타입으로 도메인 객체 타입의 객체들을 담고 있죠. 흠... 화면에 보이는 값이 어째 좀 '사용자 비친화적(and 개발자 친화적)'입니다. name 말고 note를 출력하도록 할까요? 아~~주 간단합니다.

<form:checkboxes items="${allAuthorities}" path="authorities" delimiter="<br/>" itemLabel="note"  itemValue="id" />

JSP에서 단어 하나만 바꿔주면 되죠.


짜잔... OSAF의 커스텀 태그는 스프링 form 태그를 기반으로 만들었으며, 정형적인 화면 개발 속도를 극대화 할 수 있도록 만들어 두었습니다.

다음에는 PropertyEditor 활용법을 살펴보겠습니다.
top


오랜만에 스프링 MVC 다시 정리

Spring/Chapter 13 : 2008.10.19 13:19


오늘 오후 네 시에 스터디가 있어서 오랜만에 13장을 다시 정리해봤습니다. 그 중 몇 개만 정리해둡니다.

MultiActionController 사용 방법은 두 가지
- 상속
- 위임

WebApplicationContext가 관리하는 빈
- 컨트롤러(controller)
- 핸들러 맵핑(handler mappings)
- 뷰 리졸버(view resolver)
- 로케일 리졸버(locale resolver)
- 테마 리졸버(theme resolver)
- 멀티파트 파일 리졸버(multipart file resolver)
- 예외 처리 리졸버(Handler exception resolver)

애노테이션 기반 컨트롤러 설정시 필요한 빈(자동 등록해줌)
- DefaultAnnotaionHandlerMapping
- AnnotationMethodHandlerAdapter

@RequestMapping 사용 방법
- 클래스 레벨
- 메소드 레벨(MAC와 비슷한 효과)
- 클래스 + 메소드 레벨 혼합(클레스 레벨에 Ant 패턴 사용해서 거르고, 메소드 레벨로 세부적으로.)

요청 처리 메소드 인자
- Servlet API(Session 사용시 Thread-safety 문제가 생기면, AnnotationMethodHandlerAdapter의 synchronizeOnSession 속성을 true로 설정.)
- WebRequest, NativeWebRequest
- Locale
- InputStream/Reader, OutputStream/Writer
- @RequestParam
- Map, Model, ModelMap
- Command/form objects
- Errors/BindingResult
- SessionStatus

요청 처리 메소드 반환 타입
- ModelAndView
- Model (뷰 이름은 CoC 사용)
- Map (위와 동일)
- View (모델은 커맨드 객체와 @ModelAttribute를 사용한 메소드가 반환하는 객체)
- String (위와 동일)
- void (응답을 response 객체를 사용해서 직접 처리하거나, CoC 사용)
- Other return type (해당 객체를 model attribute로 뷰에서 사용가능)

@RequestParam
- 요청 매개변수 바인딩

@ModelAttribute
- 메서드 매개변수 레벨: 모델 속성을 특정 메서드 매개변수로 맵핑할 때 사용.
- 메서드 레벨: 화면에서 사용할 implicite object를 제공할 때 사용.

@SessionAttributes
- @ModelAttribute의 이름 목록을 지니고 있다. 해당 모델 객체들을 세션에 저장하여 여러 요청에서 공통으로 사용.

@InitBinder
- 커스텀 프로퍼티 에디터 등록.
top


찾았다. WebBindingInitializer

Spring/Chapter 13 : 2008.04.22 18:52


이 녀석이 였구나.. 여러 컨트롤러에 PropertyEditor 적용할 때 필요한 녀석이..

레퍼런스를 저 키워드로 뒤지면 다음과 같은 코드가 나옵니다.

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="cacheSeconds" value="0" />
    <property name="webBindingInitializer">
        <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
    </property>
</bean>

헤헷 이것만 가지고는 뭐.. 어쩌라는 건지 알 수 없죠. 저 클래스를 찾아봐야 합니다. 저 클래스를 찾는 방법은 여러 방법이 있지만, 제가 올려드리죠.

public class ClinicBindingInitializer implements WebBindingInitializer {

    @Autowired
    private Clinic clinic;

    public void initBinder(WebDataBinder binder, WebRequest request) {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
        dateFormat.setLenient(false);
        binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(false));
        binder.registerCustomEditor(PetType.class, new PetTypeEditor(this.clinic));
    }

}

우왕~~ 귿이다~~ 이제 컨트롤러 마다 똑같은 프로퍼티 에디터 등록 안해도 되겠당.
top


Annotation-based controller configuration

Spring/Chapter 13 : 2007.11.28 23:51


0. 컴포넌트 스캐너 등록하기

우선, 컴포넌트 스캔 기능을 사용해서 @Controller 애노테이션이 붙어있는 클래스들을 bean으로 인식하도록 해야합니다. 따라서 context:component-scan 엘리먼트로 컨트롤러들이 위치한 패키지를 명시해 줍니다.

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

1. 컨트롤러 작성하기

완전 POJO로 컨트롤러를 작성할 수 있습니다. 획기적이네요. 일단 컨트롤러로 사용할 클래스는 이제 더이상 아무런 클래스도 상속받지 않아도 됩니다. 정말 그야말로 POJO입니다. 이 POJO에다가 @Controller 애노테이션을 붙여주면 컨트롤러가 됩니다.

2. Request Mapping 하기

원래는 Handler Mapper가 하던 일인데, 이제는 이것도 애노테이션이 해줍니다. @RequestMapping 애노테이션으로 해당 클래스 또는 메소드가 처리할 요청을 명시해주면 됩니다.

3. Form Controller로 사용하기

클래스 선언부에 @RequestMapping 애노테이션으로 폼을 요청할 Request를 설정해줍니다. 이 애노테이션이 붙어있는 클래스 안의 메소드 위에 같은 애노테이션을 사용하여 Request의 method에 따라 호출될 메소드를 설정할 수 있습니다.

@SessionAttributes 엘리먼트는 Session에 담을 attribute 를 나타냅니다. 주로 여러 폼에 걸쳐서 보여줄 데이터를 명시합니다.

@ModelAttribute는 두 가지 경우에 사용할 수 있는데, 메소드 위에 사용하면 폼에서 참조할 객체(Reference Data)를 나타낼 때 사용합니다. 아래의 예제에서 populatePetTypes() 메소드가 그 예에 해당합니다. 또 다른 경우는 메소드의 매개변수 앞에 이 애노테이션을 사용할 경우 인데, 이 때는 폼에 입력된 정보를 해당 애노테이션이 붙어있는 객체로 맵핑해 줍니다. processSubmit() 메소드의 인자를 보시면 됩니다.

@RequestParam은 Request의 특정 파라미터의 값을 가져옵니다. 물론 자동으로 해당 값을 이 애노테이션이 붙어있는 타입으로 변환해 줍니다. 기본타입만 가능하겠죠.

@Controller
@RequestMapping("/editPet.do")
@SessionAttributes("pet")
public class EditPetForm {

    private final Clinic clinic;

    @Autowired
    public EditPetForm(Clinic clinic) {
        this.clinic = clinic;
    }

    @ModelAttribute("types")
    public Collection<PetType> populatePetTypes() {
        return this.clinic.getPetTypes();
    }

    @RequestMapping(method = RequestMethod.GET)
    public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
        Pet pet = this.clinic.loadPet(petId);
        model.addAttribute("pet", pet);
        return "petForm";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String processSubmit(@ModelAttribute("pet") Pet pet, BindingResult result,
            SessionStatus status) {
        new PetValidator().validate(pet, result);
        if (result.hasErrors()) {
            return "petForm";
        }
        else {
            this.clinic.storePet(pet);
            status.setComplete();
            return "redirect:owner.do?ownerId=" + pet.getOwner().getId();
        }
    }

}

4. MultiActionController로 사용하기

@RequestMapping 애노테이션으로 처리할 URL을 메소드 위에 표기해 줍니다. 물론 class위에는 적을 필요가없겠죠. 메소드 단위니까요.

뷰는 CoC에 따라 요청 URL을 보고 판단합니다.
뷰를 명시할 수 있는 다른 방법이 있는지 궁금해지네요. 이 부분은 살펴봐야겠습니다.
ModelAndView 객체를 리턴타입으로 사용할 수 있다면 간단해 지겠지만 말이죠.

@Controller
public class ClinicController {

    private final Clinic clinic;

    @Autowired
    public ClinicController(Clinic clinic) {
        this.clinic = clinic;
    }

    @RequestMapping("/welcome.do")
    public void welcomeHandler() {
    }

    @RequestMapping("/vets.do")
    public ModelMap vetsHandler() {
        return new ModelMap(this.clinic.getVets());
    }

    @RequestMapping("/owner.do")
    public ModelMap ownerHandler(@RequestParam("ownerId") int ownerId) {
        return new ModelMap(this.clinic.loadOwner(ownerId));
    }

}

5. 커스텀 프로퍼티 에디터 등록하기.

다음과 같이 @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));
    }

    // ...
}

해당 바인더는 커맨드나 폼 객체 그리고 그에 따른 에러 객체를 제외한 @RequestMapping이 지원하는 모든 아규먼트에 적용됩니다. 흠.. 폼에서 사용자가 입력한 값은 이걸로 바인딩 하지 않는다는 건가?? -_-?? 이상하네.. 이 부분도 공부나 추가 정보가 필요함.

커스텀 프로퍼티 에디터 등록을 xml에서 하기

WebBindingInitializer 인터페이스 구현체에 프로퍼티 에디터를 등록해서 다음과 같이 설정해주는 듯 합니다. 이 것 역시 어떻게 구현하는지 공부가 필요함.

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
    <property name="cacheSeconds" value="0" />
    <property name="webBindingInitializer">
        <bean class="org.springframework.samples.petclinic.web.ClinicBindingInitializer" />
    </property>
</bean>

top


13.11. Convention over configuration 3

Spring/Chapter 13 : 2007.06.19 16:47


마지막으로 살펴볼 CoC는 ModelAndView 객체의 View에 해당하는 논리적인 View의 이름에 관련된 것입니다.
요청 -> view 이름 :: DefaultRequestToViewNameTranslator
앞에서 살펴봤던 예제 코드를 다시 살펴보겠습니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView("issue/list")
            .addObject(issueService.getAll());
    }

여기서 논리적인 뷰의 이름으로 issue/list 를 넘겨주고 있습니다. 이 이름은 다음과 같은 ViewReslover에 의해 WEB-INF/jsp/issue/list.jsp 를 랜더링 하게 됩니다.

    <bean id="viewResolver"
        class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass"
            value="org.springframework.web.servlet.view.JstlView" />
        <property name="prefix" value="/WEB-INF/jsp/" />
        <property name="suffix" value=".jsp" />
    </bean>

DefaultRequestToViewNameTranslator 를 사용하면 요청을 바탕으로 논리적인 View 이름을 생성하여 ViewResolver에 넘겨주게 됩니다. 특히나 이 클래스는 Spring의 DispatcherServlet  이 기본으로 가지고 있기 때문에 명시적으로 선언하지 않아도 됩니다.

샘플
 http://localhost:8080/gamecast/display.html -> display
 http://localhost:8080/gamecast/displayShoppingCart.html -> displayShoppingCart
 http://localhost:8080/gamecast/admin/index.html -> admin/index

위의 ModelAndView 코드를 다음과 같이 수정할 수 있습니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView()
            .addObject(issueService.getAll());
    }

이렇게 보니까 ModelAndView 라는 이름이 어색하지 않게 보이네요. :)
top


13.11. Convention over configuration 2

Spring/Chapter 13 : 2007.06.19 16:27


이번에는 MVC중에서 M 즉 모델에 적용할 수 있는 Convention을 살펴보겠습니다.

Spring의 컨트롤러에서 모델을 넘겨줄 때 ModelAndView 객체를 사용합니다. 보통은 다음과 같이 사용합니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView("issue/list", "issueList", issueService.getAll());
    }

또는 뷰와 모델을 명확하게 구분하기 위해 다음과 같이 사용합니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView("issue/list")
            .addObject("issueList", issueService.getAll());
    }

갑자기 기억나는 이야기지만 객체의 이름은 ModelAndView인데 View먼저 등록하니까 이름을 ViewAndModel로 해야하는 것 아니였나.. 라는 글이 생각납니다.

Anyway addObject 메소드가 오버로딩으로 또 다른 메소드 하나가 더 존재합니다. 그것을 사용하면 코드를 다음과 같이 변경 할 수 있습니다.

    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
        return new ModelAndView("issue/list")
            .addObject(issueService.getAll());
    }

모델의 이름이 생략됐습니다.

바로 이겁니다. 모델의 이름을 생략할 수 있습니다. 생략하고 보내면 View에서는 그럼 이 모델을 어떤 이름으로 찾게 될 것인가?

if(객체타입 == List || Set || 배열){
    객체의 camel-case 맨 뒤에 List를 붙인 이름을 가지게 됩니다.
} else {
    객체의 camel-case 이름을 가지게 됩니다.
}

샘플
x.y.Foo 타입의 객체는 foo 라는 이름을 가지게 됩니다.
x.y.z.FooBar 타입의 객체는 fooBar 라는 이름을 가지게 됩니다.
List<Foo> 타입의 객체는 fooList 라는 이름을 가지게 됩니다.
FooBar[] 타입의 객체는 fooBarList 라는 이름을 가지게 됩니다.

이 메소드를 사용할 때 한 가지 주의할 것이 있습니다.
Note: Empty Collections are not added to the model when using this method because we cannot correctly determine the true convention name. View code should check for null rather than for empty collections as is already done by JSTL tags.
비어있는 콜렉션을 추가할 수 없다는 군요.

top


13.11. Convention over configuration 1

Spring/Chapter 13 : 2007.06.19 16:07


Spring 2.0에 추가된 기능으로 네이밍으로 규약을 정하고 그것을 지키기만 하면 설정을 대폭 줄일 수 있는 방법이 마련되었습니다.
Request -> 컨트롤러 규약 :: ControllerClassNameHandlerMapping
즉 핸들러 맵핑을 통해 매번 각각의 요청을 처리할 컨트롤러들을 등록했었습니다. 보통 다음과 같은 소스코드를 확인하실 수 있습니다.
사용자 삽입 이미지

ControllerClassNameHandlerMapping 핸들러를 등록하면 다음과 같은 규약을 사용할 수 있습니다.
if(WelcomeController == MultiActionController){
    WelcomeController 는 /welcome/* 요청을 처리하게 됩니다.
} else {
    WelcomeController 는 /welcome* 요청을 처리하게 됩니다.
}

사용하는 방법은 간단합니다. ControllerClassNameHandlerMapping 을 등록해주기만 하면 됩니다.

    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

    <bean class="org.springframework.web.servlet.mvc.support.ControllerClassNameHandlerMapping"/>

BeanNameUrlHandlerMapping은 CoC를 사용하지 않고 직접 url과 매핑 시켜줄 컨트롤러들을 위해 등록해 줍니다.

이제부터는 요청을 처리하는 컨트롤러를 찾으려고 XXX-sevlet.xml에서 검색하지 않아도 됩니다. 그냥 요청의 이름만 보면 해당 요청을 어디서 처리할 지 알 수 있습니다.

    <bean class="net.agilejava.nayoung.controller.IssueController" />

컨트롤러를 등록할 때 name이나 id를 주지 않아도 되기 때문에 컨트롤러를 등록할 때 '이 컨트롤러가 처리하는 url이 어떤거였더라..' 라는 고민을 하지 않아도 됩니다.



top


13.10. Handling exceptions

Spring/Chapter 13 : 2007.05.21 11:35


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

먼저 컨트롤러든 서비스 단이든 벨리데이션 할 때든 에러를 발생시킵니다. 예외 처리 할 것도 없기 때문에 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를 찾게 됩니다.



top


Intercepting requests 예제

Spring/Chapter 13 : 2007.05.17 12:36


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

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으로 암묵적으로[각주:1] 사용하고 있었습니다.

이 녀석을 위에 보시다시피 명시적으로 표기해 주고 순서를 두 번째로 지정합니다. 다음으로는 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. 실행


로그인 안 한 상태에서 파일 업로드 페이지로 가려고 하면 => 로그인 페이지로 이동
로그인 한 상태라에서 파일 업로드 페이지로 가려고 하면 => 파일 업로드 페이지 요청을 처리합니다.
  1. 명시적으로 handleMapping을 등록하지 않으면 DispacherSevlet이 알아서 이녀석을 만들어서 사용하기 떄문이빈다. [본문으로]
top


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

Spring/Chapter 13 : 2007.05.14 17:23


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)에 잘 들어갔습니다.

top


13.8. Spring's multipart (fileupload) support

Spring/Chapter 13 : 2007.05.12 21:48


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;
    }
}

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


13.7. Using themes

Spring/Chapter 13 : 2007.05.12 21:08


13.7.1. Introduction

테마를 적용하기 위해서는 전역적으로 사용할 스타일 시트(CSS)와 그림 파일등의 자원을 정의하는 것이 필요합니다.

13.7.2. Defining themes

ThemeSource 를 사용하면 되는데 XmlWebApplicationContext 가 ThemeSource 를 구현하고 있으며 내부 구현체로는 ResourceBundleThemeSource 를 기본으로 사용하고 있습니다.

사용자 삽입 이미지

ResourceBundleThemeSource는 classpath에 들어있는 프로퍼티 파일을 읽어 들여서 theme을 설정할 때 사용합니다.

styleSheet=/themes/cool/style.css
background=/themes/cool/img/coolBg.jpg

위와 같이 프로퍼티 파일에 설정해 두고 view에서는 spring:theme 태그를 사용하여 key에 해당하는 이름으로 각 요소들을 사용할 수 있습니다.

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<html>
   <head>
      <link rel="stylesheet" href="<spring:theme code="styleSheet"/>" type="text/css"/>
   </head>
   <body background="<spring:theme code="background"/>">
      ...
   </body>
</html>

13.7.3. Theme resolvers

마지막으로 저렇게 만들어둔 여러개의 Theme 중에 어떤 Theme를 사용할지 선택해야 합니다. 이때 ThemeResolver를 사용하면 됩니다.

Class

Description

FixedThemeResolver

defaultThemeName 속성을 사용해서 고정적인 테마를 설정합니다.

SessionThemeResolver

각 세션에 테마에 대한 정보를 유지합니다. 세션 간에 공유되지는 않습니다.

CookieThemeResolver

선택된 테마에 대한 정보를 쿠키에 유지 합니다.




top


13.6. Using locales

Spring/Chapter 13 : 2007.05.12 18:22


Request가 들어오면 Dispatcher는 Locale resolver를 찾게 되고 만약에 있으면 Locale을 설정할 때 사용합니다. RequestContext.getLocale() 메소드를 사용해서 Locale resolver에 의해 설정 된 Locale을 가져올 수 있습니다.

13.6.1. AcceptHeaderLocaleResolver

사용자의 브라우져에서 보내진 request의 헤더에 accept-language 부분에서 Locale을 읽어들입니다. 사용자의 OS locale을 나타냅니다.

13.6.2. CookieLocaleResolver

사용자 컴터에 쿠키를 남아 있다면 그 쿠키에 설정한 Locale을 읽어 들입니다.
다음과 같은 속성을 설정할 수 있습니다.

Property

Default

Description

cookieName

classname + LOCALE

쿠키 이름

cookieMaxAge

Integer.MAX_INT

쿠키 살려둘 시간.

-1로 해두면 브라우저를 닫을 때 없어집니다.

cookiePath

/

Path를 지정해 주면 해당 하는 path와 그 하위 path에서만 참조할 수 있습니다.


13.6.3. SessionLocaleResolver

requst가 가지고 있는 session으로 부터 locale 정보를 가져옵니다.

13.6.4. LocaleChangeInterceptor

HandlerMapping에 인터셉터를 등록하여 특정 locale의 요청을 가로채서 특정 파라미터에 넘어 온 값으로 locale 을 알아낼 수 있습니다.
<bean id="localeChangeInterceptor"
      class="org.springframework.web.servlet.i18n.LocaleChangeInterceptor">
    <property name="paramName" value="siteLanguage"/>
</bean>

<bean id="localeResolver"
      class="org.springframework.web.servlet.i18n.CookieLocaleResolver"/>

<bean id="urlMapping"
      class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="localeChangeInterceptor"/>
        </list>
    </property>
    <property name="mappings">
        <value>/**/*.view=someController</value>
    </property>
</bean>

여기서는 모든 .view로 끝나는 요청을 가로채서 siteLanguage라는 request의 파라미터를 조사 하여 locale을 알 수 있게 됩니다.
top


13.5.2. Chaining ViewResolvers

Spring/Chapter 13 : 2007.05.12 16:18


다수의 ViewResolver들을 등록해 둘 수 있습니다. 그 때 특정 view 이름을 해석할 View Resolver들을 순서대로 나열하는 것을 Chaining ViewResolver라고 하는 것 같습니다. ViewResolver들의 순서는 Ordered 인터페이스를 사용하여 설정할 수 있습니다.

레퍼런스의 예를 보겠습니다.
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
  <property name="prefix" value="/WEB-INF/jsp/"/>
  <property name="suffix" value=".jsp"/>
</bean>

<bean id="excelViewResolver" class="org.springframework.web.servlet.view.XmlViewResolver">
  <property name="order" value="1"/>
  <property name="location" value="/WEB-INF/views.xml"/>
</bean>

<!-- in views.xml -->

<beans>
  <bean name="report" class="org.springframework.example.ReportExcelView"/>
</beans>

이렇게 등록 해두면 InternalResourceViewResolver는 특성상 ViewReslove 체인의 맨 마지막에 위치 하기 때문에 굳이 ExcelViewResolver에 order 속성에 숫자를 주지 않아도 되겠죠.

만약에 해당 ViewResolver에서 적당한 view를 찾지 못하면 그 다음 ViewResolver를 사용하여 찾게 됩니다. 끝까지 못찾으면 Exception이 발생합니다.

InternalResouceViewResolver와 VelocityViewResolver, FreeMarkerViewResolver 들은 맨 뒤에 위치 해야 합니다.

top


13.5.1. Resolving views - the ViewResolver interface

Spring/Chapter 13 : 2007.05.12 15:48


Spring이 제공하는 ViewResolver들은 다음과 같습니다.

ViewResolver

Description

AbstractCachingViewResolver

View 들을 cashing하는 기능을 제공합니다.

XmlViewResolver

ViewResolver 의 구현체로 XML파일을 사용합니다.

/WEB-INF/views.xml 을 기본 설정파일로 사용합니다.

ResourceBundleViewResolver

ViewResolver 의 구현체로 리소스 파일을 사용합니다.

views.properties 를 기본 리소스 파일로 사용합니다.

UrlBasedViewResolver

ViewResolver 의 구현체로 특별한 맵핑 정보 없이 의미상 view 이름을 URL로 사용합니다.

View 이름과 실제 view 자원과의 이름이 같을 때 사용할 수 있습니다.

InternalResourceViewResolver

UrlBasedViewResolver 를 상속 받았으며   InternalResourceView(Servlet, JSP)를 사용할 수 있습니다.

VelocityViewResolver /

FreeMarkerViewResolver

UrlBasedViewResolver 를 상속 받았으며 VelocityView FreeMarkerView를 사용할 수 있습니다.


사용하는 View 기술에 따라 적절한 ViewResolver를 선택해야겠습니다.
JSP를 사용하려면 UrlBasefViewResolver를 사용해도 되고 그 하위에 있는 InternalResourceVIewResolver를 사용할 수 있겠습니다. 하지만 JSTL을 사용하려면 InternalResourceVIewResolver를 사용해야겠네요. :)

JSP 사용할 때
<bean id="viewResolver"
      class="org.springframework.web.servlet.view.UrlBasedViewResolver">
    <property name="prefix" value="/WEB-INF/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

JSP를 JSTL와 함께 사용할 때
<bean id="jspViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
  <property name="prefix" value="/WEB-INF/jsp/"/>
  <property name="suffix" value=".jsp"/>
</bean>

여러 view 기술들을 사용할 때
<bean id="viewResolver"
      class="org.springframework.web.servlet.view.ResourceBundleViewResolver">
    <property name="basename" value="views"/>
    <property name="defaultParentView" value="parentView"/>
</bean>

basename에 설정한 값의 ResourceBundle을 찾아서 각각의 view를 [viewname].class 는 뷰 기술, [viewname].url 은 실제 뷰 URL로 정의 해둔 프로퍼티 파일을 사용하여 찾아 줍니다.

ViewResolver들의 상위에 있는 AbstractCachingViewResolver 이 클레스가 캐슁 기능을 제공하기 때문에 이 클레스의 하위 클레스들은 엄청난 성능 향상을 맛볼 수 있습니다.
캐슁 기능을 끄고 싶을 때는 cache 속성을 false로 하면 됩니다.
런타임시 특정 view를 다시 읽어야 한다면 removeFromCache(String viewName, Locale loc) 메소드를 사용할 수 있습니다.
사용자 삽입 이미지



top


13.5. Views and resolving them

Spring/Chapter 13 : 2007.05.12 15:05


핸들러(controller)는 요청을 처리 한 뒤 ModelAndView 객체를 넘겨 줍니다. 이 때 이 객체에 view의 이름을 같이 넘겨 주는데 이 이름으로 실제 view를 찾아 주는 역할을 하는 것이 View Resolver입니다.

13.5.1. Resolving views - the ViewResolver interface

Spring 에서 제공하는 다양한 ViewResolver들을 살펴 봅니다.

13.5.2. Chaining ViewResolvers

여러 개의 viewResolver를 사용할 수 있습니다.

13.5.3. Redirecting to views

리다이렉트 하는 방법들이 나와있는데 pass하겠습니당;;

top


13.4.3. Intercepting requests - the HandlerInterceptor interface

Spring/Chapter 13 : 2007.05.12 14:26


HandlerMapping 안에 interceptor를 만들어서 HandlerMapping에 들어오는 요청들 중에 일부를 처리하는 Handler 하테 요청을 세 가지 시점(넘기기 전과 후, 완료 된 후)에 특정 작업을 추가할 수 있습니다.

레퍼런스에 나와있는 예제는 특정 시간 사이에 들어오는 요청이 아니면 모두 특정 페이지로 요청을 넘겨 버리게 됩니다.

먼저 인터셉터를 만들려면 HandlerInterceptor 인터페이스를 구현해야 합니다.
사용자 삽입 이미지
구현해야 할 메소드는 세 개이며 저 중에서 원 하는 것만 구현해서 사용할 수 있도록 어댑터 클레스를 제공합니다.

package samples;

public class TimeBasedAccessInterceptor extends HandlerInterceptorAdapter {

    private int openingTime;
    private int closingTime;

    public void setOpeningTime(int openingTime) {
        this.openingTime = openingTime;
    }

    public void setClosingTime(int closingTime) {
        this.closingTime = closingTime;
    }

    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {

        Calendar cal = Calendar.getInstance();
        int hour = cal.get(HOUR_OF_DAY);
        if (openingTime <= hour < closingTime) {
            return true;
        } else {
            response.sendRedirect("http://host.com/outsideOfficeHours.html");
            return false;
        }
    }
}

예제에서는 이 어댑터를 사용하여 구현하였으며 preHandler 메소드를 사용하여 요청을 핸들러에게 넘기기 전에 작업을 합니다. 작업의 내용은 현재 요청이 들어온 시간이 특정 시간 사이라면 true, 아니면 http://host.com/outsideOfficeHours.html 여기로 요청을 넘기고 false를 리턴합니다.

인터셉터를 사용하려면 HandlerMapping의 peopertie에 등록해 주면 됩니다.
<beans>
    <bean id="handlerMapping"
          class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="interceptors">
            <list>
                <ref bean="officeHoursInterceptor"/>
            </list>
        </property>
        <property name="mappings">
            <value>
                /*.form=editAccountFormController
                /*.view=editAccountFormController
            </value>
        </property>
    </bean>

    <bean id="officeHoursInterceptor"
          class="samples.TimeBasedAccessInterceptor">
        <property name="openingTime" value="9"/>
        <property name="closingTime" value="18"/>
    </bean>
<beans>

요청을 처리하기 전에 로그인 되어 있는지 확인하는 로직을 이렇게 구현해도 될 것 같네요.
top


13.4.2. SimpleUrlHandlerMapping

Spring/Chapter 13 : 2007.05.11 18:06


이 HandlerMapping은 Ant 스타일의 path 매칭(특수 문자 사용을 말하는 듯 합니다.)을 사용할 수 있습니다.

<web-app>
    ...
    <servlet>
        <servlet-name>sample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <!-- maps the sample dispatcher to *.form -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>

    <!-- maps the sample dispatcher to *.html -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.html</url-pattern>
    </servlet-mapping>
    ...
</web-app>

위와 같이 설정해두면 .form 과 .html 로 끝나는 request들을 sample-context.xml 이라는 DispatcherServlet에서 처리하게 됩니다.

<beans>
       
    <!-- no 'id' required, HandlerMapping beans are automatically detected by the DispatcherServlet -->
    <bean class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
        <property name="mappings">
            <value>
                /*/account.form=editAccountFormController
                /*/editaccount.form=editAccountFormController
                /ex/view*.html=helpController
                /**/help.html=helpController
            </value>
        </property>
    </bean>

    <bean id="helpController"
          class="org.springframework.web.servlet.mvc.UrlFilenameViewController"/>

    <bean id="editAccountFormController"
          class="org.springframework.web.servlet.mvc.SimpleFormController">
        <property name="formView" value="account"/>
        <property name="successView" value="account-created"/>
        <property name="commandName" value="Account"/>
        <property name="commandClass" value="samples.Account"/>
    </bean>
<beans>

위의 설정 파일에서 ant 스타일의 문자를 사용하여 다수의 요청을 하나의 특정 컨트롤러에서 처리하도록 설정하였습니다.

ant 스타일에 대한 내용은 여기서 확인할 수 있습니다.
  • ? 는 문자 하나에 매칭하고
  • * 는 빵개에서 여러개의 문자에 매칭하고
  • ** 는 빵개에서 여러개의 디렉토리에 매칭합니다.
사용자 삽입 이미지

top


13.4.1. BeanNameUrlHandlerMapping

Spring/Chapter 13 : 2007.05.11 17:52


전달 받은 Request를 application context에 정의해둔 특정 bean의 이름으로 매핑시킵니다.

예를 들어 http://samples.com/editaccount.form 이라는 request를 처리할 컨트롤러를 다음과 같이 등록할 수 있습니다.
<beans>
  <bean id="handlerMapping" class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

  <bean name="/editaccount.form" class="org.springframework.web.servlet.mvc.SimpleFormController">
    <property name="formView" value="account"/>
    <property name="successView" value="account-created"/>
    <property name="commandName" value="account"/>
    <property name="commandClass" value="samples.Account"/>
  </bean>
<beans>


formView 는 입력받을 view 이름이고 seccessView 는 성공적으로 폼에서 객체를 입력받은 뒤 submit 할 view 이름이고 commandName은 폼에서 입력받은 값들을 바인딩할 객체이며 그 객체의 타입은 commandClass에서 지정합니다.

~~~.form 로 끝나는 요청을 distpacher에서 받아서 위에 설정해둔 handler Mapping을 사용하도록 하려면 다음과 같이 web.xml에 설정해야 합니다.
<web-app>
    ...
    <servlet>
        <servlet-name>sample</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

   <!-- maps the sample dispatcher to *.form -->
    <servlet-mapping>
        <servlet-name>sample</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>
    ...
</web-app>

BeanNameUrlHandlerMapping을 사용할 때는 굳이 위의 예제 처럼 bean으로 등록해줄 필요가 없습니다. 아무런 HandlerMapping도 등록되어 있지 않다면 Dispatcher가 알아서 BeanNameUrlHandlerMapping을 만들어서 사용하기 때문입니다.

사용자 삽입 이미지


top


13.4. Handler mappings

Spring/Chapter 13 : 2007.05.11 13:37


사용자 삽입 이미지

DispatcherServlet 과의 관계를 나타내면 다음과 위와 같이 여러 개의 handlerMapping 객체를 List 형태로 가지고 있습니다. Dispatcher Servlet은 위 그림에 표현하지 않은 다른 객체들도 여럿 가지고 있습니다.

저 중에 딱 Dispatcher Servelt과 HandlerMapping 하나와의 관계를 보겠습니다.
사용자 삽입 이미지
요청을 처리할 커맨드 객체를 찾아서 HandlerExecutionChain 객체에 감싸서 넘겨 줍니다.

HandlerMapping들의 상위 클레스인 AbstractHandlerMapping 과 하위 클레스들의 관계를 살펴보겠습니다.
사용자 삽입 이미지
위와 같은 상속구조를 가지며 HandlerMapping을 아무것도 설정하지 않을 떄는 BeanNameUrlHandlerMapping을 사용합니다.

AbstractHandlerMapping에서 설정할 수 있는 설정들은 다음과 같습니다.
interceptors :: 사용할 인터셉터의 리스트
defaultHandler :: 매칭 하는 핸들러 없을 때 사용할 기본 핸들러
order: 여러 개의 HandlerMapping이 있을 때 그것 들 중에서 어떤 걸로 먼저 핸들러 찾을 지 설정할 때 사용
alwaysUseFullPath :: true로 설정하면 request의 전체 경로를 사용하고, false면 현재 servlet이 매핑 되어 있는 위치에서 부터 경로를 사용합니다.
urlPathHelpe :r: 보통 기본 값 그대로 둔다고 하네요.(using this property, you can tweak the UrlPathHelper used when inspecting URLs)
urlDecode :: 기본값은 false입니다. hadler mapping에서 적당한 핸들러를 찾기 전에 Request URL을 디코딩 하고 싶을 때 ture로 설정합니다.
lazyInitHandlers :: 싱글톤 스콥의 핸들러들의 객체를 필요로 할 때 생성하고 싶을 때 설정합니다.
top


13.3.4. Command controllers

Spring/Chapter 13 : 2007.05.11 12:20


AbstractCommandController :: 특정 객체로 request의 파라미터를 바인딩 할 수 있습니다. 폼 기능은 제공하지 않지만 validation을 할 수 있으며 바인딩 객체를 사용하여 원하는 일을 할 수 있습니다.

AbstractFormController :: submit 기능을 제공하기 위한 추상 클레스 입니다. 사용자의 화면의 폼에 입력할 값을 채우면 각각의 필드로 바인딩하고 validation 할 수 있으며 폼을 보여줄 view와 입력이 성공적으로 완료 했을 때 보여줄 success view를 구현해야 합니다.

SimpleFormController :: 위에 있는 폼 컨트롤러 보다 진보한 것으로 폼 입력을 받을 커맨드 객체와 폼 view, success view를 설정할 수 있습니다.
이 녀석 라이프 사이클이 굉장히 복잡한 것 같던데 레퍼런스에는 설명이 없군요.

AbstractWizardFormController ::
이 클레스를 상속하는 클레스는 validatePage(), processFinish(), processCancel() 세 개의 메소드를 구현해야 합니다. 모델 객체를 view에게 넘겨 줄 때 referenceData(..) 메소드를 재정의 하여 사용할 수 있습니다.
getTargetPage() 를 사용하여 페이지를 넘길 수 있으며, 기본 바인딩과 validation을 재정의 하고 싶다면 onBindAndValidate()를 사용합니다.
getTargetPage() 에서 현재 페이지가 검증에 실패하면 앞이나 뒤로 움직일 수 있도록 setAllowDirtyBack() 와 setAllowDirtyForward()를 사용할 수 있습니다.
top


13.3.3. The MultiActionController

Spring/Chapter 13 : 2007.05.10 16:25


MultuActionController는 여러 개의 요청을 처리할 수 있는 컨트롤러로 요청을 특정 메소드로 매핑합니다. 이 때 메소드를 찾을 수 있도록 도와주는 세 개의 Resolver들이 있습니다.

먼저 MultiActionController 에 설정할 수 있는 속성들을 살펴보겠습니다.
  • methodNameResolver :: 사용 할 methodNameResolver를 등록합니다.
  • delegate :: you define a delegate object, on which methods resolved by the MethodNameResolver will be invoked.(영.. 해석이;;)
MultiActionController에 정의할 수 있는 메소드의 기본 형태는 아래와 같습니다.
public [ModelAndView | Map | void] anyMeaningfulName(HttpServletRequest, HttpServletResponse [, Exception | AnyObject]);

리턴타입은 세 가지가 있으며 인자로는 기본적으로 request와 reponse객체를 받으며 메소드에서 발생할 Exception을 처리할 메소드를 정의할 때 사용하며 AnyObject는 request가 바인딩 될 아무 타입의 객체나 사용할 수 있습니다.

다음은 MethodNameResolver 들을 살표보겠습니다.
  • ParameterMethodNameResolver :: request의 파라미터로 메소드를 맵핑합니다. 예를 들어 http://www.sf.net/index.view?testParam=testIt 이런 요청이 들어오면 testIt 메소드를 찾아서 호출합니다.
  • InternalPathMethodNameResolver :: request에서 파일이름에 해당하는 부분으로 메소드를 맵핑합니다. 예를 들어 http://www.sf.net/testing.view 요청이 들어오면 testing 메소드를 호출합니다.
  • PropertiesMethodNameResolver :: 사용자가 정해놓은 요청과(key)와 메소드(value)를 보토 판단합니다.
레퍼런스의 예제를 보겠습니다.
<bean id="propsResolver" class="org....mvc.multiaction.PropertiesMethodNameResolver">
  <property name="mappings">
    <value>
        /index/welcome.html=retrieveIndex
        /**/notwelcome.html=retrieveIndex
        /*/user?.html=retrieveIndex
    </value>
  </property>
</bean>

<bean id="paramMultiController" class="org....mvc.multiaction.MultiActionController">
    <property name="methodNameResolver" ref="propsResolver"/>
    <property name="delegate" ref="sampleDelegate"/>
</bean>

MultiActionController와 AbstractController 그리고 MethodNameResolver와의 관계는 아래와 같습니다.
사용자 삽입 이미지


top


13.3.2. Other simple controllers

Spring/Chapter 13 : 2007.05.10 11:28


ParameterizableViewController 를 사용하면 AbstractController 와 기능은 같지만 veiw 이름을 Configuration 파일에서 설정할 수 있습니다.
사용자 삽입 이미지
위의 인터페이스를 보면 viewName이라는 멤버 변수가 있고 setter injection 사용하여 설정 파일로 부터 view 이름을 설정 할 수 있다는 것을 알 수 있습니다.
사용자 삽입 이미지

AbstractController와의 관계는 다음과 같습니다. 실제 구현한 코드도 예상 했던 것 만큼이나 매우 간결합니다.

 public class ParameterizableViewController extends AbstractController {
   
   private String viewName;


   /**
    * Set the name of the view to delegate to.
    */
   public void setViewName(String viewName) {
       this.viewName = viewName;
   }

   /**
    * Return the name of the view to delegate to.
    */
   public String getViewName() {
       return viewName;
   }

   protected void initApplicationContext() {
       if (this.viewName == null) {
           throw new IllegalArgumentException("viewName is required");
       }
   }


   /**
    * Return a ModelAndView object with the specified view name.
    */
   protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response)
           throws Exception {

       return new ModelAndView(getViewName());
   }

}

'Spring > Chapter 13' 카테고리의 다른 글

13.4.2. SimpleUrlHandlerMapping  (0) 2007.05.11
13.4.1. BeanNameUrlHandlerMapping  (0) 2007.05.11
13.4. Handler mappings  (0) 2007.05.11
13.3.4. Command controllers  (0) 2007.05.11
13.3.3. The MultiActionController  (2) 2007.05.10
13.3.2. Other simple controllers  (4) 2007.05.10
13.3.1. AbstractController and WebContentGenerator  (0) 2007.05.10
13.3. Controllers  (0) 2007.05.09
13.2. The DispatcherServlet  (0) 2007.05.07
13.1. Introduction  (0) 2007.05.07
13. Web MVC framework  (0) 2007.05.07
top


13.3.1. AbstractController and WebContentGenerator

Spring/Chapter 13 : 2007.05.10 08:41


다양한 컨트롤러들의 가장 기반이 되는 구현체인 AbstractController에서 설정할 수 있는 속성들은 다음과 같습니다.

Feature

Explanation

supportedMethods

어떤 형식의 요청을 받아 들일지 설정합니다. 보통은 GET 이나 POST 둘 중 하나며 원하는 메서드 이름으로 바꿀 수 도 있습니다. 지원 하지 않는 요청의 경우 ServletException이 발생합니다.

requiresSession

해당 컨트롤러가 작업을 할 때 Http session이 필요한지 설정합니다.

만약 필요로 하는데도 session을 담고 있는 request가 오지 않으면 SevletException이 발생합니다.

synchronizeSession

사용자의 Session을 동기화 처리해주고 싶을 때 사용합니다.

cacheSeconds

이 컨트롤러가 Http reponse에 캐쉬를 남기고 싶을 때 사용합니다.

기본값은 -1로 캐쉬를 남기지 않습니다.

useExpiresHeader

생성할 response HTTP 1.0과 호환 가능한 “Expires” 헤더를 기술합니다.

기본값은 true입니다.

useCacheHeader

생성할 responseHTTP 1.1과 호환 가능한 “Cache-Control” 헤더를 기술합니다.

기본값은 true입니다.


AbstractController를 사용하는 방법은 간단합니다. 다음과 같이 handleRequest 메소드를 구현해주면 됩니다.

package samples;

public class SampleController extends AbstractController {

    public ModelAndView handleRequestInternal(
        HttpServletRequest request,
        HttpServletResponse response) throws Exception {

        ModelAndView mav = new ModelAndView("hello");
        mav.addObject("message", "Hello World!");
        return mav;       
    }
}

AbstractController와 Controller 간의 관계는 다음과 같습니다.
사용자 삽입 이미지

이 컨트롤러는 view 이름을 소스 코드 내부에 위치 해야 하는 단점이 있습니다. 이렇게 되면 view가 바뀔 때 마다 소스 코드를 수정해줘야 하죠.

'Spring > Chapter 13' 카테고리의 다른 글

13.4.2. SimpleUrlHandlerMapping  (0) 2007.05.11
13.4.1. BeanNameUrlHandlerMapping  (0) 2007.05.11
13.4. Handler mappings  (0) 2007.05.11
13.3.4. Command controllers  (0) 2007.05.11
13.3.3. The MultiActionController  (2) 2007.05.10
13.3.2. Other simple controllers  (4) 2007.05.10
13.3.1. AbstractController and WebContentGenerator  (0) 2007.05.10
13.3. Controllers  (0) 2007.05.09
13.2. The DispatcherServlet  (0) 2007.05.07
13.1. Introduction  (0) 2007.05.07
13. Web MVC framework  (0) 2007.05.07
top


13.3. Controllers

Spring/Chapter 13 : 2007.05.09 23:12


컨트롤러는 MVC중 C에 해당하며 사용자의 입력을 받은 뒤 뷰에 보여주기 적당한 모델로 바꿔준다.

Spring은 다양한 컨트롤러들을 제공하는데 크게 폼 컨트롤러, 커맨드 기반 컨트롤러, 마법사 스타일의 컨트롤러로 나뉩니다.

Spring 컨트롤러의 가장 상위에 위치한 Controller 인터페이스는 다음과 같습니다.
public interface Controller {

    /**
     * Process the request and return a ModelAndView object which the DispatcherServlet
     * will render.
     */
    ModelAndView handleRequest(
        HttpServletRequest request,
        HttpServletResponse response) throws Exception;

}

모든 Controller 구현체들은 재사용 가능하며, Thread-safe 해야합니다.

Workflow
사용자 삽입 이미지
Request 요청이 들어오면 DisptacherServlet에서 Locale, Theme 등등을 확인하고 HandlerMapping을 사용하여 요청을 담당할 컨트롤러를 물색합니다.

요청을 처리할 컨트롤러를 찾으면 그 컨트롤러의 handleRequest 메소드에게 HttpServletRequest와 HttpServletReponse 객체를 인자로 넘겨주며 호출합니다.

해당 컨트롤러는 요청을 처리하고 넘겨줄 ModelAndView 객체가 있다면 DispatcherServlet에게 해당 객체를 넘겨줍니다.

Notes on design and testing

Servlet API에 종속되어 기존 Servlet 의 기능을 모두 사용할 수 있습니다.

Servlet API에 의해 제공되는 HttpServletRequest 와 HttpServletResponse의 Mock 객체를 사용하여 handleRequest 메소드를 테스트 할 수 있습니다.

'Spring > Chapter 13' 카테고리의 다른 글

13.4.2. SimpleUrlHandlerMapping  (0) 2007.05.11
13.4.1. BeanNameUrlHandlerMapping  (0) 2007.05.11
13.4. Handler mappings  (0) 2007.05.11
13.3.4. Command controllers  (0) 2007.05.11
13.3.3. The MultiActionController  (2) 2007.05.10
13.3.2. Other simple controllers  (4) 2007.05.10
13.3.1. AbstractController and WebContentGenerator  (0) 2007.05.10
13.3. Controllers  (0) 2007.05.09
13.2. The DispatcherServlet  (0) 2007.05.07
13.1. Introduction  (0) 2007.05.07
13. Web MVC framework  (0) 2007.05.07
top


13.2. The DispatcherServlet

Spring/Chapter 13 : 2007.05.07 23:03


<web-app>

    <servlet>
        <servlet-name>example</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>example</servlet-name>
        <url-pattern>*.form</url-pattern>
    </servlet-mapping>

</web-app>

이렇게 web.xml에서 DispatcherServlet을 등록해 두면 .form 으로 끝나는 url은 모두 'example' 이라는 DispatcherServlet이 담당하게 됩니다.

사용자 삽입 이미지

(1)Front Controller 패턴을 사용하여 모든 요청을 하나의 서브릿에서 받은 뒤
(2)각각의 요청을 처리할 컨트롤러를 Handler Mapping을 기반으로 찾아서 요청을 넘기게 됩니다.
(3)컨트롤러에서 요청을 처리한 뒤 ModelAndView 객체를 다시 Dispatcher에게 넘기면
(4)Dispatcher는 받은 객체의 view 이름과 ViewResolver를 사용하여 해당 응답을 랜더링할 view에게 Model 객체들을 넘기게 되고 그것을 바탕으로 HTML 을 만들고
(5)다시 Dispatcher에게 제어권을 넘기면
(6)Reponse에 방금 만들어낸 HTML을 실어 보냅니다.

Spring 프레임워크는 맨 위에 있는 web.xml의 내용을 바탕으로 Dispatcher를 초기화 할 때
"Dispatcher의 이름"-servlet.xml 파일을 WEB-INF 폴더에서 찾고 그 안에 등록한 모든 bean들을 생성합니다.
each DispatcherServlet has its own WebApplicationContext, which inherits all the beans already defined in the root WebApplicationContext. These inherited beans defined can be overridden in the servlet-specific scope, and new scope-specific beans can be defined local to a given servlet instance.
위의 레퍼런스 인용구에서 볼 수 있듯이 DispatcherSevlet이 WebApplicationContext를 가지고 있고 여기에 설정할 수 있는 특별한 bean들은 다음과 같습니다.

Bean type

Explanation

Controllers

MVC C에 해당하는 각종 컨트롤러들

Handler mappings

요청을 처리할 컨트롤러들 명시

View resolvers

View 이름을 풀 수 있는 컴포넌트들

Locale resolver

국제화 지원하기 위해 client locale 알아낸다

Theme resolver

테마 사용할 때 사용

multipart file resolver

Http 폼에서 파일 업로드 할 때 사용

Handler exception resolver(s)

특정 예외가 발생할 때의 보여줄 view 등록



'Spring > Chapter 13' 카테고리의 다른 글

13.4.2. SimpleUrlHandlerMapping  (0) 2007.05.11
13.4.1. BeanNameUrlHandlerMapping  (0) 2007.05.11
13.4. Handler mappings  (0) 2007.05.11
13.3.4. Command controllers  (0) 2007.05.11
13.3.3. The MultiActionController  (2) 2007.05.10
13.3.2. Other simple controllers  (4) 2007.05.10
13.3.1. AbstractController and WebContentGenerator  (0) 2007.05.10
13.3. Controllers  (0) 2007.05.09
13.2. The DispatcherServlet  (0) 2007.05.07
13.1. Introduction  (0) 2007.05.07
13. Web MVC framework  (0) 2007.05.07
top


13.1. Introduction

Spring/Chapter 13 : 2007.05.07 22:02


Spring MVC 의 특징
  • 다른 web MVC 프레임워크와 마찬가지로 모든 요청을 DispatcherServlet 을 사용하여 요청을 처리할 핸들러에게 dispatch 합니다.
  • 요청을 처리할 핸들러(Controller)로는 가장 심플한 Controller 인터페이스 부터 여러 경우에 사용할 수 있는 구현체들을 제공합니다.
  • 어떤 객체라도 폼에 입력되는 값을 받아들이는 객체(폼 객체, Command)로 사용할 수 있습니다.
  • view resolution이 매우 유연(flexible)합니다.
  • ModelAndView 객체를 사용하여 view 와 model(맵 형태)을 담게 됩니다.
  • ModelAndView에 있는 view이름을 다양한 ViewResolver 를 사용하여 적당한 view에 매핑할 수 있습니다.
13.1.1. Pluggability of other MVC implementations

스트럿츠나 웹워크 같은 다른 웹 MVC 프레임워크로 Spring MVC를 대체 할 수 있습니다.

13.1.2. Features of Spring Web MVC
  • Clear separation of roles - controller, validator, command object, form object, model object, DispatcherServlet, handler mapping, view resolver, 기타 등등. 여러 책임을 각각의 객체로 나눠 놨기 때문에 보다 객체 지향적인 MVC 구현이 가능합니다.
  • Powerful and straightforward configuration - 웹 컨트롤러들 부터 비즈니스 객체까지 모든 객체들을 컨텍스트에 빈으로 등록하여 사용할 수 있습니다.
  • Adaptability, non-intrusiveness - 각각 상황에 맞는 컨트롤러를 골라서 사용할 수 있습니다.
  • Reusable business code - 비즈니스 객체를 커맨드 객체로 재사용할 수 있습니다.
  • Customizable binding and validation - 애플리케이션 차원에서 데이터를 바인딩하고 검증할 수 있습니다.
  • Customizable handler mapping and view resolution - 다양한 방법으로 핸들러 맵핑과 뷰 리졸버를 사용할 수 있습니다.
  • Flexible model transfer - 모델이 name/value 쌍의 맵으로 구성되기 때문에 여러 view간에 이동할 때 사용하기 편합니다.
  • 여러 view 기술들과 연동할 수 있습니다.
  • spring 태그를 제공합니다.
  • form 태그를 제공합니다.
  • session 과 request Scope을 사용할 수 있습니다.

'Spring > Chapter 13' 카테고리의 다른 글

13.4.2. SimpleUrlHandlerMapping  (0) 2007.05.11
13.4.1. BeanNameUrlHandlerMapping  (0) 2007.05.11
13.4. Handler mappings  (0) 2007.05.11
13.3.4. Command controllers  (0) 2007.05.11
13.3.3. The MultiActionController  (2) 2007.05.10
13.3.2. Other simple controllers  (4) 2007.05.10
13.3.1. AbstractController and WebContentGenerator  (0) 2007.05.10
13.3. Controllers  (0) 2007.05.09
13.2. The DispatcherServlet  (0) 2007.05.07
13.1. Introduction  (0) 2007.05.07
13. Web MVC framework  (0) 2007.05.07
top


13. Web MVC framework

Spring/Chapter 13 : 2007.05.07 18:47


13.1. Introduction

Spring MVC는 DispatcherServlet  를 중심으로 만들어졌습니다.
모든 객체가 Command 객체로 사용될 수 있습니다.
View Resolution이 정말 유연하다.

13.2. The DispatcherServlet

Spring MVC 프레임워크도 다른 request 중심의 웹 MVC 프레임워크와 마찬가지로 중앙 서브릿에서 요청(request)을 디스패칭하여 웹 애플리케이션의 다른 기능들을 제공합니다.

13.3. Controllers

MVC에서 "C"에 해당하며 Service 인터페이스를 사용하여 애플리케이션의 기능을 제공합니다. ModelAdnView 객체와 더불어 Spring MVC를 구성하는 기본 요소 입니다.

13.4. Handler mappings

Handler mapping을 사용하여 웹에서 들어온 요청을 처리할 적당한 핸들러(컨트롤러)로 매핑 시킵니다.

13.5. Views and resolving them

컨트롤러에서 요청을 처리한 뒤 ModelAndView 객체에 넘기는 View에 대한 정보를 풀어제낄 녀석과 VIew에 대해 살펴봅니다.

13.6. Using locales

DispatcherServlet 을 사용하여 국제화를 위해 사용자의 Locale을 읽을 수 있습니다.

13.7. Using themes

웹에 특정 테마를 적용하고 싶다면 리소스 파일과 <spring:theme/> 태그를 사용 할 수 있습니다.

13.8. Spring's multipart (fileupload) support

파일 업로드 기능을 지웝합니다.

13.9. Using Spring's form tag library

Spring의 form 태그 사용법

13.10. Handling exceptions

웹에서 요청에서 에외가 발생했을 때 그것을 처리할 핸들러를 지정할 수 있습니다.

13.11. Convention over configuration

네이밍 규약만 잘 지킨다면 상당량의 설정 내용을 줄일 수 있습니다. Spring에서 지원하는 CoC를 설명합니다.

13.12. Further Resources

Spring 사이트에 있는 Spring MVC Step-by-Step
Expert Spring Web MVC and WebFlow

'Spring > Chapter 13' 카테고리의 다른 글

13.4.2. SimpleUrlHandlerMapping  (0) 2007.05.11
13.4.1. BeanNameUrlHandlerMapping  (0) 2007.05.11
13.4. Handler mappings  (0) 2007.05.11
13.3.4. Command controllers  (0) 2007.05.11
13.3.3. The MultiActionController  (2) 2007.05.10
13.3.2. Other simple controllers  (4) 2007.05.10
13.3.1. AbstractController and WebContentGenerator  (0) 2007.05.10
13.3. Controllers  (0) 2007.05.09
13.2. The DispatcherServlet  (0) 2007.05.07
13.1. Introduction  (0) 2007.05.07
13. Web MVC framework  (0) 2007.05.07
top