컨트롤러는 MVC의 세 가지 컴포넌트 중에서 가장 많은 책임을 지고 있다. 

 

 서블링시 넘겨주는 HTTP요청은 HttpServletRequest 오브젝트에 담겨 있다. 컨트롤러가 이를 이용해 사용자의 요청을 파악하려면 클라이언트 호스트, 포트 ,URI, 쿼리 스트링, 폼파라미터, 쿠키, 헤더 ,세션을 비롯해서 서블릿 컨테이너가 요청 애트리뷰트로 전달해주는 것까지 매우 다양한 정보를 참고해야 한다.

 HttpServletRequest에서 필요한 사용자 요청정보를 추출했다고 해서 끝이 아니다. 사용자가 바르게 요청을 보냈는지도 검증해야 한다.  

 

  사용자 요청을 모두 분석한 후에 서비스 계층의 비즈니스 로직을 담당하는 메소드를 불러서 요청에 따른 작업을 수행할 수 있다. 이 과정에서 컨트롤러의 역할은 서비스 계층의 메소드를 선정하는 것과 메소드가 필요로 하는 파라미터 타입에 맞게 요청 정보를 변환해 주는 것이다.

 

 대부분 서비스계층 메소드는 리턴값이나 예외를 이용해 결과를 돌려준다. 때로는 간단한 숫자나 문자열일 수도 있지만, 때로는 복잡한 오브젝트나 컬렉션,배열일 수도 있다. 

 

 컨트롤러는 서비스계층의 메소드가 돌려준 결과를 보고 어떤 뷰를 보여줘야 하는지 결정해야 한다. 때로는 페이지가 바뀌도록 리다이렉트해줘야 한다. 

 

 뷰 선택이 끝나면 다음은 뷰에 출력할 내용을 모델에 적절한 이름으로 넣어줘야 한다. 서비스 계층 메서드가 돌려주는 값이 그대로 들어가기도 하지만, 때로는 컨트롤러가 모델 정보를 생성할 수도 있다.URL 파라미터를 통해서 페이지가 바뀌어도 검색조건이나 페이지 번호 등을 유지해야 한다면, HTML에 들어갈 URL이나 폼의 히든 필드에 저장할 파라미터 정보 등을 가공해서 모델에 넣어주기도 한다.

 

 상테를 세션에 저장하는 경우도 있다. 이어지는 요청에서도 계속 유지돼야 하는 정보가 있고, 이를 DB나 URL파라미터, 쿠키에 저장하기 어려운 경우엔 HTTP세션에 정보를 저장해두는 것도 컨트롤러의 책임이다. 때로는 더 이상 필요 없어진 세션의 오브젝트를 제거해주는 작업도 필요하다. 이 작업을 빼먹으면 HTTP 세션에 필요없는 오브젝트가 세션이 끝날 떄까지 누적되는 일종의 메모리 누수 버그가 발생할 수도 있다.

 

 이런 모든 작업을 컨트롤러 메소드하나에 모두 담아두는 건 비효율적이며 객체지향적이라고 보기도 힘들다. 스프링 MVC가 컨트롤러 모델을 미리 제한하지 않고 어댑터 패턴을 사용해서라도 컨트롤러의 종류를 필요에 따라 확장할 수 있도록 만든이유가 이 때문이다. 우리는 스프링 MVC가 제공하는 컨트롤러의 종류와 그에 따른 핸들러 어댑터를 알아보고, 필요에 따라 컨트롤러를 설계하고 만드는 방법도 살펴보자. 그리고 URL과 핸들러를 연결해주는 핸들러 매핑에 대해서도 도 알아보자.

 

  1. 컨트롤러의 종류와 핸들러 어댑터

스프링 MVC가 지원하는 컨트롤러의 종류는 4가지다. 각 컨트롤러를 DispatcherServlet에 연결해주는 핸들러 어댑터가 하나씩 있어야 하므로, 핸들러 어댑터도 4개다. 이 중에서 SimpleServletHandlerAdapter를 제외한 세 개의 핸들러 어댑터는 DispatcherServlet에 디폴트 전략으로 설정됨. 

 

 Servlet과 SimpleServletHandlerAdapter

첫 번째 컨트롤러 타입은 바로 표준 서블릿이다. 표준 서블릿 인터페이스인 javax.sevlet.Servlet을 구현한 서블릿 클래스를 스프링 MVC컨트롤러로 사용할 수 있다. 

 

 서블릿을 컨트롤러로 사용할 떄 장점은 서블릿 클래스 코드를 그대로 유지하면서 스프링 빈으로 등록된다는 점.

따라서 서블릿 코드를 점진적으로 스프링 앱에 맞게 포팅할 때 유용. 서블릿에서 직접 헬퍼 클래스의 오브젝트를 만들어서 사용했던 코드를 일단 DI방식으로 사용하도록 변경하는 것이 첫 번째 작업.

 이후 단계적으로 서블릿에서 담당했떤 기능을 다른 종류의 컨트롤러로 이전하는 작업을 진행할수있음.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ServletControllerTest extends AbstractDispatcherServletTest{
    @@Test
    public void helloServletController() throws ServletException,IOException{
        setClasses(SimpleServletHandlerAdapter.class, HelloServlet.class);
        initRequest("/hello").addParameter("name","Spring");
        
        assertThat(runService().getContentAsString(),is("Hello Spring"))
    }
    @Component("/hello")
    static class HelloServlet extends HttpServlet{
        protected void doGet(HttpServletRequest req, HttpServletResponse resp){
            String name=req.getParameter("name");
            resp.getWriter().print("Hello" +name);
        }
    }
}
 
 
cs

AbstractDispatcherServletTest를 사용해서 XML 설정파일 만들지 않고 스프링 MVC에서 컨트롤러 동작테스트 가능.

서블릿 타입의 컨트롤러는 모델과 뷰를 리턴하지 않는다. 스프링 MVC의 모델과 뷰라는 개념을 알지 못하는 표준 서블릿을 그대로 사용한 것이기 때문. 그래서 결과는 서블릿에서 HttpServelResponse에 넣어준 정보를 확인하는 방법을 사용.

 

 DispatcherServlet은 컨트롤러가 ModelAndView 타입의 오브젝트 대신 null을 리턴하면 뷰를 호출하는 과정을 생략하고 작업을 마치게 되어 있다. 서블릿 컨트롤러처럼 직접 HttpServletResponse에 결과 넣는 컨트롤러가 있기 때문

 

HttpRequestHandler와 HttpRequestHandlerAdapter

HttpRequestHandler는 인터페이스로 정의된 컨트롤러 타입이다. 이 인터페이스를 구현해서 컨트롤러를 만든다.

 

 스프링은 HttpRequestHandler를 이용해서 자바의 RMI(원격 메소드 호출)를 대체할 수 있는 Http 기반의 가벼운 원격 호출 서비스인 HTTP Invoker를 제공한다. 

 HTTPRequestHandler는 이렇게 모델과 뷰 개념이 없는 HTTP 기반의 RMI와 같은 로우레벨 서비스 개발할 떄 이용할수 있다.

 

Controller와 SimpleControllerHandlerAdapter

 

 SimpleControllerHandlerAdapter에 의해 실행되는 Controller 타입 컨트롤러는 위의 인터페이스를 구현해서 만든다. 

컨트롤러는 DispathcerServlet이 컨트롤러와 주고받는 정보를 그대로 메소드의 파라미터와 리턴 값으로 갖고있다.

따라서 스프링 MVC의 가장 대표적인 컨트롤러 타입이다.

 Controller 타입의 컨트롤러는 Controller 인터페이스를 구현하기만 하면 되기 때문에, 특정 클래스를 상속하도록 강제하는 여타 MVC 프레임워크 컨트롤러보다 유연하게 컨트롤러 클래스를 설계할 수 있다는 장점. 

하지만 이 실제로 인터페이서 직접구현해 컨트롤러 만드는건 권장X. 

 웹브라우저를 클라이언트로 갖는 컨트롤러로서의 필수 기능이 구현되어 있는 AbstractControlloer를 상속해 컨트롤러 만드는게 편리하기 때문.  AbstractController는  Controller 인터페이스를 구현한 Controller 타입의 컨트롤러.

 

 AbstractController가 제공하는 웹개발에 유용하게 쓸 수 있는 프로퍼티들

  • synchronizeOnSession: HTTP 세션에 대한 동기화 여부 결정하는 프로퍼티. 사용자가 자신의 HTTP 세션에 동시 접근하는 것을 막아줌. 이 프로퍼티를 사용하면 가장 안전하게 HTTP 세션을 조작하는 컨트롤러 만들 수 있다.
  • supportedMethods: 컨트롤러가 허용하는 HTTP 메소드(GET,POST)를 지정할 수 있다.

AnnotationMethodHandlerAdapter

컨트롤러의 타입이 정해져 있지 않음. 다른 핸들러 어댑터는 특정 인터페이스를 구현한 컨트롤러만을 지원한다.

 반면에 AnnotationMethodHandlerAdapter는 컨트롤러 타입에 제한이 없다.  대신 클래스와 메소드에 붙은 몇가지 어노테이션의 정보와 메서드 이름, 파라미터 ,리턴 타입에 대한 규칙 등을 종합적으로 분석해서 컨트롤러를 선별하고 호출 방식을 결정한다. 

 다른 특징은 컨트롤러 하나가 하나 이상의 URL에 매핑될 수 있다는 점. 여타 컨트롤러는 특정 인터페이스를 구현하면 그 인터페이스의 대표 메서드를 통해 컨트롤러가 호출되므로 특별 경우 제외시 URL당 하나의 컨트롤러가 매핑되는 구조. 이렇게 하면 컨트롤러는 단순해지지만 웹 요청 개수 늘어나면 컨트롤러의 숫자도 급격하게 늘어남.

 

 AnnotationMethodHandlerAdapter를 도입하면서 URL의 매핑을 컨트롤러 단위가 아니라 메서드 단위로 가능하게 했다. 컨트롤러 클래스 하나에 여러 개의 컨트롤러 메서드를 넣을 수 있으니 당연히 하나의 컨트롤러가 여러 개의 URL을 매핑 받아서 처리할 수 있다. 그렇게 메소드 단위로 컨트롤러 로직을 넣으려면 유연한 방식으로 매핑 정보등을 지정해줘야 하기 때문에 어노테이션이 필요.

 

 AnnotationMethodHandlerAdapter는 여타 핸들러 어댑터와는 다르게 DefaultAnootaionHandlerMapping 핸들러 매핑과 함께 사용해야 한다. 두 가지 모두 동일한 어노테이션을 사용하기 때문.

 두 가지는 스프링 MVC에서 가장 인기있는 컨트롤러 작성방법. 

 

 

@RequestParam은 1개의 HTTP 요청 파라미터를 받기 위해서 사용한다. @RequestParam은 필수 여부가 true이기 때문에 기본적으로 반드시 해당 파라미터가 전송되어야 한다. 해당 파라미터가 전송되지 않으면 400 Error를 유발하게 된다. 그렇기 때문에 반드시 필요한 변수가 아니라면 required의 값을 false로 설정해둘 수 있으며 해당 Parameter를 사용하지 않고 요청을 보낼 경우에 default로 받을 값을 defaultValue 옵션을 통해 설정할 수도 있다.

 

 

1
2
3
4
5
6
7
8
9
@Controller
public class HelloController{
 
    @RequestMapping("/hello")
    public String hello(@RequestParam("name"String name,ModelMap map){
        map.put("mesaage","Hello" + name);
        return "/WEB-INF/view/hello.jsp";
    }
}
cs

 

2. 핸들러 매핑

핸들러 매핑은 HTTP요청정보를 이용해서 이를 처리할 핸들러 오브젝트, 즉 컨트롤러를 찾아주는 기능을 가진 DispatcherServlet의 전략이다. 핸들러 매핑은 컨트롤러의 타입과는 상관없다. 하나의 핸들러 매핑 전략이 여러 가지 타입의 컨트롤러를 선택할 수 있다는 뜻이다. 

 

디폴트 핸들러 매핑은 BeanNameUrlHandlerMappin과 DefaultAnnotationHandlerMapping이다.

 

BeanNameUrlHandlerMapping

빈의 이름에 들어 있는 URL을 HTTP요청의 URL과 비교해서 일치하는 빈을 찾아준다. 

ex) <bean name="/s* class="springbook...Controller"> 

위 선언은 /s로 시작하는 /s,/s1,/sabcd 같은 URL에 매핑된다.

빠르고 쉽게 URL매핑정보를 지정할 수 있다. 반면 컨트롤러의 개수가 많아지면 URL정보가 XML빈 선언이나 클래스 어노테이션 등에 분산되어 나타나므로 전체적인 매핑구조를 한눈에 파악하고 관리하기 불편. 

 

ControllerBeanNameHandlerMapping

빈의 아이디나 빈 이름을 ㅁ이용해 매핑해주는 전략.

 

ControllerClassNameHandlerMapping

빈 이름 대신 클래스 이름을 URL에 매핑해주는 핸들러 매핑 클래스.

 

SimpleUrlHandlerMapping

URL과 컨트롤러 매핑정보를 한곳에 모아놓을 수 있는 핸들러 매핑 전략. 매핑정보는 SimpleUrlHandlerMapping 빈의 프로퍼티에 넣어준다. 

장점은 매핑정보가 한 곳에 모여 있기 때문에 URL을 관리하기 편하다는 것. 단점은 매핑할 컨트롤러 빈의 이름을 직접 적어줘야 하기 떄문에 오타 등 오류가 발생할 가능성 있음. 

 

DefaultAnnotationHandlerMapping

@RequestMapping이라는 어노테이션을 컨트롤러 클래스나 메서드에 직접 부여하고 이를 이용해 매핑하는 전략. 

메서드 단위로 URL을 매핑해줄 수 있어서 컨트롤러 개수를 획기적으로 줄일수있음. 또한 URL뿐 아니라 GET/POST와 같은 HTTP메서드, 심지어 파라미터와 HTTP헤더정보까지 매핑에 활용가능. 같은 URL이지만 GET과 POST를 따로 분리하거나, 특정 파라미터가 지정됐을 때만 따로 분리하는 식의 컨트롤러 매핑이 가능. 

다른 핸들러 매핑방식이라면 컨트롤러의 코드에서 처리해야 할 작업을 어노테이션으로 대신할 수 있어서 편리

 

 반면 매핑 어노테이션의 사용 정책과 작성 기준을 잘만들지않으면, 개발자마다 제멋대로 매핑 방식 적용해서 매핑정보가 지저분해지고 관리하기 힘들어질 수 있다. 

 

3.핸들러 인터셉터

핸들러 매핑의 역할은 기본적으로 URL과 요청정보로부터 컨트롤러 빈을 찾아주는 것이다. 한가지 더 중요한 기능은 핸들러 인터셉터를 적용해주는 것. 핸들러 인터셉터는 DispatcherServlet이 컨트롤러를 호출하기 전과 후에 요청과 응답을 참조하거나 가공할 수 있는 일종의 필터. 

 

 핸들러 매핑의 역할은 URL로부터 컨트롤러만 찾아주는 것이 아니다. 핸들러 매핑은 DispatcherServlet으로부터 매핑 작업을 요청받으면 그 결과로 핸들러 실행 체인을 돌려준다. 이 핸들러 실행 체인은 하나 이상의 핸들러 인터셉터를 거쳐서 컨트롤러가 실행될 수 있도록 구성되어 있다. 핸들러 인터셉터를 전혀 등록하지않으면 바로 컨트롤러실행됨.

반면 하나 이상핸들러 인터셉터 지정하면 순서에 따라 인터셉터 거친후에 컨트롤러 호출됨.

 

 핸들러인터셉트는 HttpServletRequest,HttpServletResponse뿐 아니라, 실행될 컨트롤러 빈 오브젝트, 컨트롤러가 돌려주는 ModelAndView, 발생한 예외 등을 제공받을 수 있기 때문에 정교하고 편리하게 인터셉터만들 수 있다. 또한 핸들러 인터셉터 자체가 스프링 빈이기 때문에 DI를 통해 다른 빈활용가능. 

 

 HandlerInterceptor

핸들러 인터셉터는 HandlerInterceptor 인터페이스를 구현해서 만듬. 이 인터페이스 안에는 다음과 같은 세 개의 메서드가 포함되어 있다.

 

preHandle: 컨트롤러 호출되기 전 실행

postHandle: 컨트롤러 실행하고 난 후 호출됨. 

afterCompletion: 모든 뷰에서 최종 결과를 생성하는 일을 포함한 모든 작업이 다 완료된 후에 실행.

 

4.컨트롤러 확장

 커스텀 컨트롤러 인터페이스와 핸들러 어댑터 개발

 

앞에서 Controller 인터페이스를 구현해서 만들었던 기반 컨트롤러 SimpleController를 이번에는 핸들러 어댑터를 이용해 호출 가능한 인터페이스로 정의해서 독자적인 컨트롤러 인터페이스로 만들어보자. 

 기존의 SimpleController는 모든 개별 클래스가 상속할 기반 클래스이므로 viewName이나 requiredParams 같은 공통 프로퍼티로 정의해놓을 수 있었다. 하지만 인터페이스로 정의하는 새로운 컨트롤러 타입에는 그런방식 사용X.

그래서 대신 관례를 만들어서 비슷한 설정이 가능하게 해본다.

 

public interface SimpleController{

     void control ( Map<String, String> params, Map<String, Object> model);

}

 기존 기반클래스 방식은 기반클래스에 미리 준비해둔 프로퍼티를 이용해서 필수 파라미터 목록과 뷰 이름을 지정할 수 있게 했다. 그렇지만 인터페이스를 이용하는 경우는 어떻게? 어노테이션을 사용.

 어노테이션은 미리 약속된 관례를 따라서 특정 위치에 어노테이션을 부여해주고 엘리먼트 값을 이용해 설정 정보를 넣는 식으로 활용 가능.

1
2
3
4
5
6
7
8
9
10
11
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface ViewName{
    String value();
}
 
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface RequiredParams{
    String[] value();
}
cs
1
2
3
4
5
6
7
8
9
 
public class HelloController implements SimpleController{
    @ViewName("/WEB-INF/view/hello.jsp")
    @RequiredParams({"name"})
    public void control(Map<String,String> param,Map<String,Object> model){
        model.put("message","Hello "+params.get("name"));
    }
}
 
cs

SimpleController 인터페이스를 구현하고 두개의 설정용 어노테이션을 부여한 메서드를 가진 컨트롤러 HelloController. 뷰이름과 필수 파라미터 목록은 어노테이션으로 지정한다. 모델 맵은 미리 만들어진 것을 전달 받아서 별도 생성필요없이 정보 넣으면 됨. 

 이렇게 만들어진 HelloController 컨트롤러를 스프링 MVC에서 사용할 수 있도록 SimpleController 타입 컨트롤러를 지원하는 핸들러 어댑터를 만들어보자. (생략)

 

 

 

 

+ Recent posts