AbstractController

상속 구조
사용자 삽입 이미지하는 일
모든 컨트롤러들의 기본이 되는 클래스로써 위 그림에 표시되어 있는 일들을 합니다.
여기서 한 가지 주목할 것은 Controller 인터페이스를 구현함에 따라 구현해야할 handleRequest 입니다.
이 메소드는 final로 다음과 같이 구현되어 있습니다.

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

        // Delegate to WebContentGenerator for checking and preparing.
        checkAndPrepare(request, response, this instanceof LastModified);

        // Execute handleRequestInternal in synchronized block if required.
        if (this.synchronizeOnSession) {
            HttpSession session = request.getSession(false);
            if (session != null) {
                Object mutex = WebUtils.getSessionMutex(session);
                synchronized (mutex) {
                    return handleRequestInternal(request, response);
                }
            }
        }
       
        return handleRequestInternal(request, response);
    }

이 메소드와 관련된 자세한 설명은 KSUG 제 2회 세미나에서 영회형이 설명해주셨지만 Spring MVC 118쪽에도 적혀있습니다. 워크 플로우와 메소드들의 라이프 사이클을 지키면서도 상속 받아 사용하는 녀석들에 때라 행동을 다르게 정의 할 수 있도록 Open-Closed Principle 을 구현한 사례에 해당합니다.

Workflow
1. DispatcherServelet에 의해 handleRequest() 호출됨.
2. 지원하는 요청의 타입을 조사(만약 지원하는 요청이 아닐 경우 ServletException 발생)
3. 세션이 필오하면 가져오려고 시도함.(없을 경우 ServletException 발생)
4. cacheSeconds 속성에 따라 필요하다면 캐슁 헤더를 설정함.
5. handleRequestInternal() 추상 메소드를 호출(이때 설정에 따라 HttpSession을 동기화 함)

Non-String DataBinding 테스트하기

참조 : Spring MVC

객체가 속성으로 String 타입이 아닌 속성을 가지고 있을 때 데이터 바인딩을 하려면 별도의 조취가 취해져야 합니다. request에 담고 있는 데이터는 모두 문자열이기 때문에 이전 글에서 살펴보았던 간단한 DataBinding일 경우에는 정말 간단하게 바인딩을 할 수 있었습니다. 하지만 이번 경우에는 PropertyEditor의 도움을 받아서 역시 간단하게 문자열을 특정 타입으로 변환하여 바인딩 해줍니다.

다음의 표는 ServletRequestDataBinder가 기본으로 사용하는 PropertyEditor들 입니다.
사용자 삽입 이미지따라서 위 표의 Result에 있는 타입들은 별도의 PropertyEditor를 바인더에 등록하지 않더라도 알아서 문자열을 해당 타입으로 변환하여 바인딩해줍니다.

간단한 클래스 작성
[#M_ more.. | less.. | public class DefaultPropertyClass {

    private int intProperty;
    private Integer integerProperty;
    private Class classProperty;
    private URL urlProperty;
    private String[] strings = new String[10];

    public Class getClassProperty() {
        return classProperty;
    }

    public void setClassProperty(Class classProperty) {
        this.classProperty = classProperty;
    }

    public Integer getIntegerProperty() {
        return integerProperty;
    }

    public void setIntegerProperty(Integer integerProperty) {
        this.integerProperty = integerProperty;
    }

    public int getIntProperty() {
        return intProperty;
    }

    public void setIntProperty(int intProperty) {
        this.intProperty = intProperty;
    }

    public URL getUrlProperty() {
        return urlProperty;
    }

    public void setUrlProperty(URL urlProperty) {
        this.urlProperty = urlProperty;
    }

    public String[] getStrings() {
        return strings;
    }

    public void setStrings(String[] strings) {
        this.strings = strings;
    }
}_M#]
테스트 클래스 작성
[#M_ more.. | less.. | public class DefaultPropertyClassTest {

    private DefaultPropertyClass propertyClass;

    private MockHttpServletRequest request;

    private ServletRequestDataBinder binder;

    @Before
    public void setUp() {
        propertyClass = new DefaultPropertyClass();
        request = new MockHttpServletRequest();
        binder = new ServletRequestDataBinder(propertyClass);
    }

    @Test
    public void testBinding() throws MalformedURLException {

        request.addParameter(“intProperty”, “34”);
        request.addParameter(“integerProperty”, “200”);
        request.addParameter(“classProperty”, “java.lang.String”);
        request.addParameter(“urlProperty”, “http://www.example.com/”);
        request.addParameter(“strings[1]”, “a, b, c”);
        binder.bind(request);
        assertEquals(34, propertyClass.getIntProperty());
        assertEquals(new Integer(200), propertyClass.getIntegerProperty());
        assertEquals(String.class, propertyClass.getClassProperty());
        assertEquals(new URL(“http://www.example.com/”), propertyClass.getUrlProperty());
        assertEquals(“a, b, c”, propertyClass.getStrings()[1]);

    }
}_M#]

간단한 DataBinding 테스트하기

화면에서 입력되는 값을 객체의 프로퍼티로 세팅해주는 역할을 데이터 바인딩이라고 합니다. 그리고 Spring MVC에서는 ServletRequestDataBindier와 PropertiEditor를 사용하여 폼에서 입력 된 값을 바인딩합니다.

가장 간단한 경우가 String 타입의 속성만을 가지고 있는 객체로 바인딩하는 것입니다.

먼저 String 타입을 가지고 있는 커맨드 객체를 만듭니다. 모델 객체를 커맨드 객체로 사용할 것이라면 굳이 만들 필요는 없습니다.

public class Message {

    private String title;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

}

이 때 JavaBeans 스펙에 맞게 getter와 setter가 정의되어 있어야 합니다.

다음 ServletRequestDataBinder를 사용하여 테스트를 작성 합니다.

public class MessageTest {

    private Message message;
    private ServletRequestDataBinder binder;
    private MockHttpServletRequest request;

    @Before
    public void setUp() {
        message = new Message();
        binder = new ServletRequestDataBinder(message, “message”);
        request = new MockHttpServletRequest();
    }

    @Test
    public void testBinding() {
        String title = “새로운 메시지 제목”;
        request.addParameter(“title”, title);

        binder.bind(request);

        assertEquals(title, message.getTitle());
    }
}

JUnit 테스트를 실행하여 결과를 확인할 수 있습니다.
사용자 삽입 이미지
BaseCommandController 컨트롤러의 하위 클래스들은 모두 위에서 사용한 bind 메소드를 사용하며 Binder객체를 생성할 때 사용한 Command 객체의 이름은 바인딩 할 때 에러가 발생하면 그 에러를 기록할 Errors 객체에서 사용하게 됩니다. 아무런 이름도 주지 않으면 기본값으로 taget 이라는 이름을 가집니다.

    @Test
    public void testCommandName() {
        binder = new ServletRequestDataBinder(message);
        assertEquals(“target”, binder.getObjectName());
    }

MultiactionController

AbstractController를 상속받아서 간단하게 구현하는 Controller들이 많이 있습니다. 그러한 것들 중에는 서로 관련이 있는 컨트롤러들도 있습니다. 예를 들어 검색결과 리스트를 가져오는 컨트롤러, 전체 목록을 가져오는 컨트롤러, 목록에서 한 개의 아이템에 대한 정보를 가져오는 컨트롤러가 있을 수 있습니다. 이러한 것들을 하나의 컨트롤러에서 처리할 수 있습니다.

MultiactionController의 장점
– 컨트롤러의 갯수가 줄어듭니다.
– 여러 처리를 논리적인 그룹으로 묶어서 하나의 클래스에 담을 수 있습니다.
– 액션 메소드의 유동적인 맵핑이 가능합니다.

MultiactionController의 단점
– 바인딩과 Valitor를 사용할 수 있지만 폼 처리 work flow가 정의되어 있지 않습니다.
    – 따라서 바인딩과 Validation을 요청을 처리할 메소드 내부에서 하기(?)를 권하고 있습니다.
– 쉽게 커질 우려가 있습니다.
– 리플렉션을 사용하기 때문에 컴파일 에러를 확인할 수 없습니다.

사용하는 방법
1. MultiActionController  상속받기
public class IssueController extends MultiActionController {

}

2. 요청을 처리할 액션 메소드 구현
– ModelAndView 객체를 반환해야 합니다.
– HttpServletRequest와 HttpServletResponse 파라미터를 가지고 있어야 합니다.
– 옵션으로 HttpSession 또는 Command 객체를 파라미터로 가질 수 있습니다.
    public ModelAndView list(HttpServletRequest request, HttpServletResponse response){
       return new ModelAndView(“issue/list”);
   }

3. 등록하기
– MethodNameResolver bean 등록합니다. 참조
    – InternalPathMethodNameResolver를 default로 사용합니다. 따라서 이 녀석을 사용할 때는 굳이 등록하거나 DI하지 않아도 됩니다.
– 위에서 구현한 컨트롤러 baen 등록하기
    <bean name=”/issue/*”  class=”net.agilejava.nayoung.controller.IssueController” />
– 이 때 *을 사용하여 /issue/list.html, /issue/find.html 과 같은 요청들을 모두 위의 컨트롤러에서 처리하도록 합니다.

궁금한 점
– Binding과 Validation을 할 수는 있는데 화면에 바인딩 하거나 Validation할 때 발생한 Error를 못 보여 주는 것이 단점인 것 같은데 이 걸 어떻게 해결 할 수 있을지…

SimpleFormController’s onSubmit()

오버로딩을 사용해서 같은 이름의 메소드가 세 개 있었습니다.

보통은 onSubmit(Object command) 이 녀석만 사용했었는데요. 세션에 객체 하나를 담고 싶어서 아래와 같은 코드가 onSubmit() 메소드 안에 추가 되어야 했습니다.

request.getSession().setAttribute(“user”, member);

그러나… request 인자가 onSubmit() 메소드에 없는 것입니다. 그래서 이를 어쩌나..했는데 찬욱군이 onSubmit() 메소드가 세 개가 있는데 그중에 request 객체를 받아오는 녀석이 있다고 알려줘서 찾아봤습니다.

Spring MVC 155쪽에 나와있는 그림을 보니 어떤 메소드 들이 있고 어떻게 동작하는지 명확히 알 수 있었습니다.
사용자 삽입 이미지인자가 제일 많은 녀석 부터 시작해서 하나 인 녀석 순으로 내부에서 호출하게 되는 것 같습니다. 이 걸 코드로 표현하면 아래 처럼 될 것입니다.

protected ModelAndView onSubmit(HttpServletRequest request, HttpServletResponse response, Object command, BindException errors) throws Exception {
    //할 일
    onSubmit(command, errors);
}

protected ModelAndView onSubmit(Object command, BindException errors)
throws Exception {
    //할 일
    onSubmit(command);
}

protected ModelAndView onSubmit(Object command)
throws Exception {
    //할 일
    doSubmitAction(formBean);
}

이 중에서 필요한 인자에 따라 필요한 녀석을 오버라이딩해서 사용하면 될 것 같습니다. 사용자(개발자) 편의를 위한 계층화라고 생각되네요.