프로그래밍 공방

[Spring] 5. 스프링 MVC - 구조 이해 본문

개발/스프링

[Spring] 5. 스프링 MVC - 구조 이해

hyosupsong 2021. 10. 27. 04:21

스프링 MVC 구조

이전에 포스팅한 글의 MVC 구조와 비교하면 아래 내용들이 변경되었다

- FrontController -> DispatcherServlet
- handlerMappingMap -> HandlerMapping
- MyHandlerAdapter -> HandlerAdapter
- ModelView -> ModelAndView
- viewResolver -> ViewResolver
- MyView -> View

DispatcherServlet 구조

스프링 MVC도 Front Controller 패턴으로 구현되어 있고 스프링 MVC의 Front Controller가 DispatcherServlet 이다
DispatcherServlet도 부모 클래스에서 HttpServlet을 상속 받고 서블릿으로 동작한다

* 스프링 부트는 DispatcherServlet을 서블릿으로 자동 등록하면서 모든 경로(urlPatterns="/")에 대해 매핑한다

DispatcherServlet의 핵심 메소드인 doDispatch의 코드를 분석해보면 아래와 같다(예외 처리, 인터셉터 기능 등은 제외)

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

스프링 MVC의 동작 순서

1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러를 조회한다
2. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다
3. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다
4. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다
5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다
6. viewResolver 호출 : 뷰 리졸버를 찾고 실행한다
    - JSP의 경우 : InternalResourceViewResolver가 자동 등록되고 사용된다
7. View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다
    - JSP의 경우 InternalResourceView(JstlView)를 반환하는데, 내부에 forward() 로직이 있다
8. 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링 한다

스프링 제공 인터페이스

스프링 MVC는 아래 인터페이스 목록들을 확장 가능할 수 있게 제공하여 DispatcherServlet 코드의 변경 없이 원하는 기능을 변경하거나 확장할 수 있다

주요 인터페이스 목록

- 핸들러 매핑 : org.springframework.web.servlet.HandlerMapping
- 핸들러 어댑터 : org.springframework.web.servlet.HandlerAdapter
- 뷰 리졸버 : org.springframework.web.servlet.ViewResolver
- 뷰 : org.springframework.web.servlet.View

핸들러 매핑과 핸들러 어댑터

과거에 주로 사용했던 기능들에 대해 간단히 알아보자

Controller 인터페이스

org.springframework.web.servlet.mvc.Controller

public interface Controller {
    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}

초기에는 위 컨트롤러 인터페이스를 아래와 같이 구현해서 사용했었다

@Component("/springmvc/old-controller")
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return new ModelAndView("new-form");
    }
}

* @Component("/springmvc/old-controller")로 매핑할 url과 동일한 이름으로 스프링 빈을 등록해주었다

위 컨트롤러가 호출되려면 아래 2가지가 필요하다

HandlerMapping : 요청 들어온 url을 처리하는 핸들러를 찾을 수 있어야 한다
HandlerAdapter : 핸들러를 실행할 수 있는 핸들러 어댑터를 찾아야 한다

스프링에는 아래와 같이 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다

HandlerMapping

0 = RequestMappingHandlerMapping : 어노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름이 URL과 동일한 핸들러를 찾는다

HandlerAdapter

0 = RequestMappingHandlerAdapter : 어노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스를 구현한 핸들러를 처리

* HandlerMapping, HandlerAdapter 모두 순서대로 찾고 없으면 다음 순서로 넘어간다

HttpRequestHandler

아래와 같이 서블릿과 가장 유사한 형태의 핸들러이다

public interface HttpRequestHandler {
     void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

View Resolver

아래 OldController 코드에서 보이듯이 View를 사용할 수 있도록 ModelAndView 코드를 추가해서 사용한다

@Component("/springmvc/old-controller")
public class OldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        return new ModelAndView("new-form");
    }
}

* 스프링 부트는 InternalResourceViewResolver라는 View Resolver를 자동으로 등록하는데 이때 application.properties에 등록한 spring.mvc.view.prefix, spring.mvc.view.suffix 설정 정보를 사용해서 등록한다

스프링 부트는 아래 뷰 리졸버 등을 자동으로 등록해서 사용한다

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

* InternalResourceViewResolver는 InternalResourceView를 반환하는데 이 View는 JSP처럼 forward()를 호출해서 처리할 수 있는 경우에 사용한다(view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP를 실행한다)

참고 : Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver를 등록해야 한다. 최근에는 라이브러리만 추가하면 스프링 부트가 자동으로 등록해준다

@RequestMapping

과거에는 스프링을 사용하더라도 MVC 웹 기술 등은 다른 프레임 워크를 사용했었지만 @RequestMapping 기반의 어노테이션 컨트롤러가 등장하면서 MVC 부분도 스프링을 사용하게 되었다

@RequestMapping 어노테이션을 사용하게 된다면 컨트롤러를 아래와 같이 작성할 수 있다

@Controller
public class SpringMemberSaveController {

    /* ... */

    @RequestMapping("/springmvc/members/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
        
        /* ... */

        ModelAndView modelAndView = new ModelAndView("save-result");
        modelAndView.addObject("member", member);

        return modelAndView;
    }
}

@Controller
스프링 MVC에서 어노테이션 기반 컨트롤러로 인식한다

@RequestMapping
- 요청 정보를 매핑해서 해당 URL이 호출되면 이 메서드가 호출된다(RequestMappingHandlerMapping)
- 어노테이션을 기반으로 동작하기 때문에 메서드의 이름은 임의로 지으면 된다
* RequestMappingHandlerMapping은 아래와 같이 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다. 따라서 @Controller 대신 @Component 와 @RequestMapping을 붙여주면 동일하게 동작한다

public class RequestMappingHandlerMapping extends RequestMappingInfoHandlerMapping implements MatchableHandlerMapping, EmbeddedValueResolverAware {

    @Override
    protected boolean isHandler(Class<?> beanType) {
        return (AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) || 
                AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class));
    }
}

ModelAndView
- 모델과 뷰 정보를 담아서 반환

addObject
- ModelAndView를 통해 Model 데이터를 추가할 때는 addObject를 사용하면 된다

스프링 MVC - 컨트롤러 통합

@RequestMapping을 보면 클래스 단위가 아닌 메서드 단위에 적용되므로 연관된 컨트롤러 클래스를 아래와 같이 하나로 통합할 수 있다

@Controller
@RequestMapping("/springmvc/members")
public class SpringMemberController {

    /* ... */
    
    @RequestMapping("/new-form")
    public ModelAndView newForm() {
        /* ... */
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
        /* ... */
    }

    @RequestMapping
    public ModelAndView members() {
        /* ... */
    }
}

@RequestMapping
- 기존과 동일하게 메서드에만  @RequestMapping을 사용하는 경우 URL 부분에 중복이 생길 수 있는데
  클래스 레벨에 @RequestMapping을 붙여서 중복되는 URL을 조합할 수 있다

스프링 MVC - 실용적인 방식

위에서는 ModelAndView를 생성해서 반환하기 때문에 불편한 부분이 있다. 스프링 MVC는 개발자가 편리하게 개발할 수 있도록 수많은 편의 기능을 제공한다
* 실무에서는 아래 방식을 주로 사용

@Controller
@RequestMapping("/springmvc/members")
public class SpringMemberController {
    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 member = new Member(username, age);
        memberRepository.save(member);

        model.addAttribute("member", member);

        return "save-result";
    }

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

        model.addAttribute("members", members);

        return "members";
    }
}

Model 파라미터
- HttpServletRequest 등을 사용하게 되면 서블릿에 종속적이게 되므로 Model을 파라미터로 받아서 사용한다

ViewName
- View의 논리 이름을 직접 반환할 수 있다

@RequestParam
- request.getParameter 대신 @RequestParam을 사용해서 HTTP 요청 파라미터를 받을 수 있다
* GET 쿼리 파라미터, POST Form 방식 모두 지원

@GetMapping, @PostMapping
- 기존에 사용하던 @RequestMapping은 method를 따로 @RequestMapping(method = RequestMethod.GET)와 같이 지정해주지 않으면 GET, POST 등 모든 타입의 요청을 동일하게 지원한다
이 부분을 @GetMapping, @PostMapping 등을 통해 더 편리하게 사용할 수 있다
* 어노테이션을 들어가보면 내부적으로 @RequestMapping을 가지고 있다