[스프링] MVC 구조 이해

gyeol·2023년 9월 5일

스프링

목록 보기
24/50
post-thumbnail

김영한 님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술'을 듣고 적은 글입니다.

스프링 MVC 구조


기존에 우리가 만들었던 FrontController 구조와 유사하다.

  • FrontController -> DispacherServlet
  • handlerMappingMap -> HandlerMapping
  • MyHandlerAdapter -> HandlerAdapter
  • ModelView -> ModelAndView
  • ViewResolver -> ViewResolver
  • MyView -> View

DispacherServlet

DispacherServlet은 부모 클래스에서 HttpServlet을 상속받아 사용한다.
DispacherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet
스프링 부트는 DispacherServlet을 서블릿으로 자동 등록하면서 모든 경로에 urlPatterns = "/" 에 대해 매핑한다.

<요청 흐름>

  • 서블릿이 호출되면 HttpServlt이 제공하는 service()가 호출된다는 것을 잊지말자
  • HttpServletservice() 호출. 이때 FrameworkServlet에서 오버라이드 해놓았기에 FrameworkServletservice() 호출됨.
  • FrameworkServlet.service()를 시작으로 DispacherServlet.doDispatch() 호출

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);
    // Spring Framework에서 사용되는 메서드.클라이언트 요청에 대한 처리기(Handler)를 찾을 수 없는 경우 호출.
    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. 핸들러 조회 : URL에 매핑된 핸들러 조회
  2. 핸들러 어댑터 조회 : 핸들러 실행할 수 있는 핸들러 어댑터 조회
  3. 핸들러 어댑터 실행
  4. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러 실행
  5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환
  6. ViewResolver 호출
  7. View 반환 : 논리 이름을 물리 이름으로 변환. 렌더링 역할을 하는 뷰 객체 반환
  8. 뷰 렌더링

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

지금은 애노테이션 기반 스프링 컨트롤러를 사용하지만 예전에는 Controller 인터페이스를 사용했다. Controller인터페이스와 @Controller는 완전히 다르다.

Controller 인터페이스

org.springframework.web.servlet.mvc.Controller

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

이 인터페이스를 호출하려면 어떻게 해야할까?
HandlerMappingHandlerAdapter가 필요하다.

  • 핸들러 매핑 : 매핑을 통해 컨트롤러(핸들러) 찾기
  • 핸들러 어댑터 : 찾은 핸들러를 실행시킬 어댑터 필요

HandlerMapping


0번에 해당하지 않으면 1번을 적용한다.

HandlerAdapter


위의 방식과 동일하다.

예시)

package hello.servlet.web.springmvc.old;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.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");
    }
}

❶ 핸들러 매핑으로 핸들러 조회

  • HandlerMapping을 순서대로 실행해 핸들러 찾음
  • 빈 이름으로 핸들러를 찾아야 하기에 빈 이름으로 핸들러 찾아주는 BeanNameUrlHandlerMapping이 실행에 성공. 핸들러인 OldController 반환

❷ 핸들러 어댑터 조회

  • HandlerAdaptersupports() 순서대로 호출
  • SimpleControllerHandlerAdapterController 인터페이스를 지원하기에 대상이 됨

❸ 핸들러 어댑터 실행

  • 디스패처 서블릿이 조회한 SimpleControllerHandlerAdapter를 실행하면서 핸들러 정보도 함께 넘겨줌
  • SimpleControllerHandlerAdapter는 핸들러인 OldController를 내부에서 실행하고 그 결과 반환

결론
HandlerMapping = BeanNameUrlHandlerMapping
HandlerAdapter = SimpleControllerHandlerAdapter

뷰 리졸버

스프링 부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동 등록한다. 이때, application.properties에 동록한 spring.mvc.view.prefix, spring.mvc.view.suffix 설정 정보를 사용해 등록한다.

스프링 부트가 자동 등록하는 뷰 리졸버


InternalResourceViewResolverInternalResourceView를 반환한다. 이는 JSP처럼 forward()를 호출해 처리할 수 있는 경우에 사용한다.

참고

InternalResourceViewResolver 는 만약 JSTL 라이브러리가 있으면 InternalResourceView 를 상속받은 JstlView 를 반환한다. JstlView 는 JSTL 태그 사용시 약간의 부가 기능이 추가된다.

다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 forward() 통해서 해당 JSP로 이동(실행)해야 렌더링이 된다. JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링된다.

스프링 MVC

@RequestMapping

가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter 이다.

RequestMappingHandlerMapping은 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.

  • @RequestMapping : 요청 정보 매핑. 해당 URL이 호출되면 이 메서드 호출
  • @Controller : 스프링이 자동으로 스프링 빈으로 등록. 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식.
package hello.servlet.web.springmvc.v1;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
/*
@Component // 덕분에 스프링 빈으로 등록
@RequestMapping
*/

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

이런 방식으로 회원 목록과 저장 클래스들도 만들어준다. 이때 ModelView로 반환하면 안된다. 꼭 ModelAndView로 바꿔주자.

하지만 이런 방식으로 하면 @RequestMapping이 클래스 단위가 아닌 메서드 단위로 붙은 것을 알 수 있다. 따라서 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.

유연한 컨트롤러

package hello.servlet.web.springmvc.v2;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;
/**
 * 클래스 단위 -> 메서드 단위
 * @RequestMapping 클래스 레벨과 메서드 레벨 조합
 */
@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 member = new Member(username, age);
        memberRepository.save(member);

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

        return mav;
    }

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

        ModelAndView mav = new ModelAndView("members");
        mav.addObject("members", members);

        return mav;
    }
}

이방식도 좋지만, 항상 모델뷰 객체를 생성해서 넘겨줘야하는 등의 번거로움이 존재한다. 더 코드를 줄여보자 !

실용적인 컨트롤러

package hello.servlet.web.springmvc.v3;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    //@RequestMapping(value = "/new-form", method = RequestMethod.GET)//get인 경우에만 호출
    @GetMapping("/new-form")
    public String newForm() {
        return "new-form";
    }

    //@RequestMapping(value = "/save", method = RequestMethod.POST) //저장은 get이 아닌 Post가 맞음
    @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";
    }

    //@RequestMapping(method = RequestMethod.GET)
    @GetMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();

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

코드가 더욱더 간결해지고 깔끔해졌다.

그리고 우리는 여태 GET, POST 방식을 구분하지 않았다. 그렇기에 GET으로 보내든 POST로 보내든 결과는 출력된다. 하지만 각 메서드의 속성에 맞게 방식에 제한을 두는게 더 좋을 것이다. 그래서 우리는 @RequestMapping 혹은 @GetMapping, @PostMapping을 사용해 제한을 두었다.

원래는 @RequestMapping의 method 속성을 이용해 GET, POST 등을 지정해줬지만, 더 편리하게 사용하기 위해 @GetMapping 같은 친구들이 등장하게 되었다. @GetMapping이나 @PostMapping의 내부를 열어보면 @RequestMapping이 들어있는 것을 확인할 수 있다.

profile
공부 기록 공간 '◡'

0개의 댓글