Spring MVC

Structure

spring_mvc_cycle

기존에 직접 구현한 서블릿 방식의 MVC 프레임워크와 Spring MVC 프레임워크를 봤을 때 큰차이는 없지만, Spring MVC는 이를 효율적으로 사용할 수 있게 제공한다.

SpringMVC 에서는 Dispatch Servlet이 Front Controller 역할을 수행한다. Dispatch Servlet 또한 내부적으로 HttpServlet을 상속하고 있으며 urlPatterns=”/” 로 기본 적용되어 있어 모든 http request 요청이 dispatch servlet으로 오게 된다.

DispatcherServlet에서 가장 중요한 메소드가 있는데, DispatcherServlet.doDispatch이라는 메소드이다.

DispatcherServlet.doDispatch()

이 메소드에는 front controller에서 수행하는 모든 로직이 다 담겨 있다.

전체코드

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;
    // 1. 핸들러 조회
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    // 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    processDispatchResult(processedRequest, response, mappedHandler, mv,dispatchException);
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
    // 뷰 렌더링 호출
    render(mv, request, response);
}
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    View view;
    String viewName = mv.getViewName();
    // 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
    // 8. 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
}
  1. Handler 조회
HandlerExecutionChain mappedHandler = null;
ModelAndView mv = null;
// 1. 핸들러 조회
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
    noHandlerFound(processedRequest, response);
    return;
}

핸들러 매핑을 통해 request url에 맞는 handler을 조회한다.

  1. Handler Adapter 조회
// 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

핸들러에 맞는 핸들러 어댑터 조회

3~5. Handler Adapter, Handler 실행 및 ModelAndView 반환

// 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

핸들러 어댑터, 핸들러 실행 및 ModelAndView 객체를 반환받는다.

6~7. View Resolver로 View 반환

// 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);

ModelAndView를 인자로 전달해서, View Resolver, View 객체를 반환받는다.

JSP의 경우 InternalResourceViewResolver, InternalResourceView 객체가 반화된다. InternalResourceView 객체에 jsp forward 메소드가 있다.

  1. Render
// 8. 뷰 렌더링
view.render(mv.getModelInternal(), request, response);

뷰를 이용해서 렌더링을 수행한다.

Spring MVC 에서는 Handler Mapping, Handler Adapter, View Resolver, View 에 대해 Interface로 제공하고 있어 확장성이 매우 높다. 그래서 웬만한 사용되는 모든 기능에 대해 구현되어 있어 따로 추가로 사용자 정의 클래스를 생성할 필요가 없다.

Handler Mapping & Handler Adapter

Controller Interface

Annotation 방식의 controller을 이용하기 전에는 interface 기반의 controller을 활용하였다.

@Component("/springmvc/old-controller")
public class OldController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return new ModelAndView("new-form");
    }
}

직접 @Component을 이용해서 Spring Bean으로 등록하고, handleRequest 메소드를 구현하는 Controller이다.

다음과 같이 Spring Bean으로 등록된 Controller는 어떻게 호출 될까?

Built-in Handler Mapping & Handler Adapter

Handler Mapping

  1. RequestMappingHandlerMapping: @ReguestMapping 기반의 컨트롤러
  2. BeanNameUrlHandlerMapping: Spring Bean 이름으로 컨트롤러 검색

Handler Adapter

  1. RequestMappingHandlerAdapter: @ReguestMapping 기반의 컨트롤러
  2. HttpRequestHandlerAdapter : HttpRequestHandler 처리
  3. SimpleControllerHandlerAdapter: Controller 인터페이스

Spring에서는 위와 같이 기본적으로 Handler Mapping 과 Handler Adapter에 대해 미리 구현해놓았다.

그래서 위의 Controller Interface 방식의 경우, BeanNameUrlHandlerMapping 과 SimpleControllerHandlerAdapter 방식으로 호출된다.

HttpRequestHandler

해당 방식으로도 핸들러를 생성할 수 있다.

@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {
    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MyHttpRequestHandler.handleRequest");
    }
}

위의 handler는 BeanNameUrlHandlerMapping 과 HttpRequestHandlerAdapter 방식으로 호출된다.

@RequestMapping

Spring MVC에서는 대부분의 Controller는 Annotation 방식으로 생성하기 때문에, 위의 방식들은 참고만 하자

View Resolver

controller에서 ModelAndView 객체를 반환하면 View Resolver에 ModelAndView 객체를 전달해서 View 객체를 반환받는다.

@Component("/springmvc/old-controller")
public class OldController implements Controller {

    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return new ModelAndView("new-form");
    }
}

이렇게 controller에서 ModelAndView를 반환하도록 설계 해놓으면 dispath servlet은 ModelAndView을 이용해서 렌더링을 실시한다.

application.properties

spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

JSP 기반의 InternalResourceViewResolver는 application.properies 파일에 설정된 prefix 와 suffix 을 이용해서 물리적 파일 경로를 만들어 내게 된다.

위의 예제의 경우, new-form 이 “/WEB-INF/views/new-form.jsp” 로 바뀌게 되는 것이다.

호출 방식

  1. BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다.
  2. InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.

View Resolver에 대해서도 Spring에서 다양한 방식의 View Resolver을 지원한다.

JSP 처럼 내부 forward 메소드를 호출해야하는 경우 InternalResourceViewResolver을 호출하게 되고, forward 과정이 필요 없는 View Resolver의 경우 바로 렌더링 된다.

Spring MVC 기반의 Controller

SpringMVC 기반의 Controller도 단계별로 사용자 친화적인 Controller을 위해 개선할 수 있다.

V1

@Controller, @RequestMapping을 이용해서 Controller을 생성한다.

SpringMemberFormControllerV1

@Controller
public class SpringMemberFormControllerV1 {
@RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process(){
        return new ModelAndView("new-form");
    }
}

@Controller annotation을 사용하게 되면, Component Scan에 의해 자동으로 Spring Bean으로 등록 되며, Annotation 기반 controller로 인식된다.

@RequestMapping을 이용해서 url pattern을 지정한다.

RequestMappingHandlerMapping은 Spring Bean 중에서 @RequestMapping이나 @Controller로 명시되어 있는 클래스를 핸들러 매핑정보를 인식한다. 따라서 위처럼 @Controller가 명시되어 있거나

아래와 같이 @RequestMapping 명시되어 있는 클래스에 대해 수동적으로 Spring Bean으로 등록해도 Controller로 인식할 수 있다.

@RequestMapping
public class SpringMemberFormControllerV1 {
    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}
@Bean
SpringMemberFormControllerV1 springMemberFormControllerV1() {
    return new SpringMemberFormControllerV1();
}

SpringMemberSaveControllerV1

public class SpringMemberSaveControllerV1 {
    private MemberRepository memberRepository=MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response){
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member newMember=new Member();
        newMember.setUsername(username);
        newMember.setAge(age);

        memberRepository.save(newMember);

        //Model에 데이터 보관
        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member",newMember);
        return mv;
    }
}

기존에 설계한 controller와 매우 유사하다. ModelAndView를 사용하는 부분만 차이가 있다.

SpringMemberListControllerV1

@Controller
public class SpringMemberListControllerV1 {
    private MemberRepository memberRepository=MemberRepository.getInstance();

    @RequestMapping("/springmvc/v1/members")
    public ModelAndView process(){
        List<Member> members=memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members",members);
        return mv;
    }
}

V2

V1 Controller들을 보면 @RequestMapping이 메소드 단위로 설정되어 있는 것을 확인할 수 있다. 이를 통해 각각의 Controller들을 하나의 클래스로 통합할 수 있음을 알 수 있다.

@Controller
@RequestMapping("/springmvc/v2/members")
public class SpringMemberControllerV2 {
    private MemberRepository memberRepository=MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm(){
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response){
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member newMember=new Member();
        newMember.setUsername(username);
        newMember.setAge(age);

        memberRepository.save(newMember);

        //Model에 데이터 보관
        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member",newMember);
        return mv;
    }

    @RequestMapping
    public ModelAndView members(){
        List<Member> members=memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members",members);
        return mv;
    }
}

또한, @RequestMapping을 클래스 단위로 넣고, 메소드 단위로 @RequestMapping을 설정하게 되면 클래스 레벨의 url pattern과 메소드 레벨의 url pattern이 조합된다.

V3

V2 Controller는 이전에 MVC 프레임워크 설계의 Front controller V3와 같이 ModelAndView 객체를 controller가 생성해야 되다는 문제점이 있다. 이를 개선하기 윌해 Spring에서는 다양한 방식으로 반환하는 것을 허용한다.

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    private MemberRepository memberRepository=MemberRepository.getInstance();

    @GetMapping("/new-form")
    public String newForm(){
        return "new-form";
    }

    @PostMapping("/save")
    public String save(
            @RequestParam("username") String username,
            @RequestParam("age") int age,
            Model model){

        Member newMember=new Member();
        newMember.setUsername(username);
        newMember.setAge(age);

        memberRepository.save(newMember);

        model.addAttribute("member", newMember);
        return "save-result";
    }

    @GetMapping
    public String members(Model model){
        List<Member> members=memberRepository.findAll();

        model.addAttribute("members",members);
        return "members";
    }
}

위를 보면 String 기반의 View Name을 반환해도 정상적으로 Controller들이 호출되는 것을 확인할 수 있다.

Model

Spring에서 내부적으로 Model 클래스를 지원한다. Model을 이용해서 Controller는 데이터를 넣어서 View에 전달할 수 있다.

@RequestParam

기존에 request의 parameter 정보를 가져오기 위해 아래와 같이 request 객체를 전달하고, getParameter 메소드를 이용해서 parameter에 접근했다.

@RequestMapping("/springmvc/v1/members/save")
public ModelAndView process(HttpServletRequest request, HttpServletResponse response){
    String username = request.getParameter("username");
    int age = Integer.parseInt(request.getParameter("age"));
    ...
}

하지만, @RequestParam annotation을 활용하면 spring이 request 객체에서 해당 parameter을 가져와서 넣어준다.

@RequestMapping("/springmvc/v3/members/save")
public ModelAndView process(
    @RequestParam("username") String username,
    @RequestParam("age") int age,
    Model model
){
}

@GetMapping, @PostMapping

@RequestMapping은 GET,POST 방식 모두에 대한 요청을 받아들이게 된다. 이를 통해 부적절한 요청도 수락해야하는 문제가 발생한다. 그래서 Spring에서는 Http Request Method를 제한하기 위해 Get방식의 @GetMapping, Post 방식의 @PostMapping을 지원한다.

위 Annotation들을 살펴 보면 @RequestMapping(method=RequestMethod.GET), @RequestMapping(method=RequestMethod.POST) annotation이 들어 있는 것을 확인 할 수 있다.

References

link: inflearn

link:springmvc

댓글남기기