RE-Heat 개발자 일지

스프링 MVC 1편 - [5] 스프링 MVC 구조 이해 본문

백엔드/스프링

스프링 MVC 1편 - [5] 스프링 MVC 구조 이해

RE-Heat 2023. 6. 29. 21:19

출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., -

www.inflearn.com

인프런 김영한님의 스프링 MVC 1편 강의를 듣고 정리한 내용입니다.

 

[1] 스프링 MVC 전체 구조

직접 만든 스프링 MVC와 스프링 MVC 구조 비교

 

■ 직접 만든 프레임워크 => 스프링 MVC 비교 
FrontController => DispatcherServlet 
handlerMappingMap => HandlerMapping 
MyHandlerAdapter => HandlerAdapter 
ModelView => ModelAndView 
viewResolver => ViewResolver
MyView => View

사실상 구조 자체는 동일. 이름만 바뀐 수준

 

DispathcerServlet의 구조

  •  디스패처 서블릿이 스프링 MVC의 핵심
  •  스프링 MVC도 우리가 만든 프론트 컨트롤러 패턴으로 구현됨. 
    • DispatcherServlet도 우리가 구현한 FrontControllerServlet과 마찬가지로 HttpServlet을 사용.
    • DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet 몇 단계를 거침
  • DispatcherServlet은 모든 경로(urlPatterns="/")에 대해 핸들러 어댑터·핸들러를 매핑해준다.
  • 서블릿이 호출되면 HttpServlet의 service()가 자동 호출 -> 결국, DispatcherServlet의 핵심인 doDisptach()에 도달

 

DispatcherSerlvet.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. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환.
  5. viewResolver 호출: 뷰 리졸버를 찾고 실행.
  6. View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환
  7. 뷰 렌더링: 뷰를 통해서 뷰를 렌더링한다.

 

[2] 핸들러 매핑과 핸들러 어댑터

핸들러 매핑 : 해당하는 핸들러(컨트롤러) 객체를 찾아 반환하는 역할

    스프링 빈 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요

핸들러 어댑터 : 컨트롤러를 실행하고 그 결과를 ModelAndView 규격에 맞춰 반환

    컨트롤러 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾아 실행해야 함.

 

HandlerMapping 순서

0 = RequestMappingHandlerMapping : 어노테이션 기반의 컨트롤러인 @RequestMapping에서 찾는다
1 = BeanNameUrlHandlerMapping    : 스프링 빈 이름으로 찾는다

HandlerAdapter 순서

0 = RequestMappingHandlerAdapter   : 어노테이션 기반의 컨트롤러인 @RequestMapping에서 찾는다
1 = HttpRequestHandlerAdapter      : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스 처리

 

OldController

@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 null;
    }
}

1] 컨트롤러가 /spring/mvc/old-controller라는 이름의 빈으로 등록 -> 2] 빈의 이름으로 URL을 매핑

 

OldController의 핸들러 매핑, 어댑터는

HandlerMapping = BeanNameUrlHandlerMapping, HandlerAdpater = SimpleControllerHandler Adapter 순

 

MyHttpRequestHandler

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

HandlerMapping = BeanNameUrlHandlerMapping, HandlerAdpater = HttpReqeustHandlerAdapter

 

가장 우선순위가 높은 핸들러 매핑 : RequestMappinghandlerMapping

핸들러 어댑터 : RequestMappinghandlerAdapter

현재 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터.

참고 : @RequestMapping의 앞글자를 따서 이름을 붙임.

 

[3] 뷰 리졸버

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

OldController가 ModelAndView 객체에 논리이름을 담아 반환할 수 있게 변경.

application.properties prefix와 suffix 추가해 뷰 리졸버 실행

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

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

 

실제로 자동 등록되는 InternalResourceViewResolver의 모습

 

뷰 리졸버 동작 방식 - 스프링 MVC

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

뷰 리졸버도 스프링에서 등록한 뷰 리졸버들을 탐색해 우선순위에 따라 뷰 리졸버를 사용함.

동작 순서

1. 핸들러 어댑터 호출 -> 핸들러 어댑터 통해 "new-form"이라는 논리 뷰 이름 획득

2. ViewResolver 호출

  BeanNameViewResolver : new-form이라는 스프링 빈이 등록되지 않았으므로 PASS

  InternalResourceViewResolver : InternalResourceView를 반환

3. InternalResourceView는 JSP처럼 fowar()를 호출해서 처리할 수 있는 경우에 사용

4. view.render()가 호출되고 InternalResourceView는 forward()를 사용해 JSP 실행

 

참고] JSP는 forward()로 해당 JSP로 이동해야 실행. 타임리프 등 그 외 뷰 템플릿은 forward() 없이 바로 렌더링 됨.

 

[4] 스프링 MVC - 시작하기

@RequestMapping

애노테이션을 활용해 매우 유연하고 실용적인 컨트롤러를 만듦. 이게 @RequestMapping 실무에서는 거의 100% 이 방식을 사용한다.

참고] 과거엔 MVC부분이 취약해 스트럿츠 + 스프링을 사용하는 방법이 애용됐으나, @RequestMapping 기반의 애노테이션 컨트롤러가 도입되면서 스프링이 대세로 자리매김함.

 

SpringMemberFormControllerV1

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

 

SpringMemberSaveControllerV1

@Controller
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 member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save-result");
        mv.addObject("member", member);
        return mv;
    }
}

 

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

 

@Controller : 스프링이 자동으로 스프링 빈으로 등록. 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식

@RequestMapping : 요청정보 매핑. 해당 URL이 호출되면 이 메서드가 호출됨. 애노테이션을 기준으로 호출되기 때문에 메소드 이름은 임의로 지어도 무방.

 

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

 

방법 1] @Controller를 클래스 레벨에 넣기

        2] 클래스 레벨에 @Component @RequestMapping 붙이기

        3] ServletApplication에 Bean을 직접 등록하기

방법 3번.

  

[5] 스프링 MVC - 컨트롤러 통합

SpringMemberFormControllerV1 + SpringMemberSaveControllerV1 + SpringMemberListControllerV1을 하나로 통합 

 

@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 mv = new ModelAndView("save-result");
        mv.addObject("member", member);
        return mv;
    }

    @RequestMapping
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);

        return mv;
    }
}

회원가입 폼 + 회원가입 등록 + 회원가입 목록보기를 한 클래스에 담음

 

[6] 스프링 MVC - 실용적인 방식

SpringMemberControllerV3

@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 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";
    }
}

1. @RequestMapping에 method 도입

  • 1] @RequestMapping(value = "/new-form", method = RequestMethod.GET)
    •  GET을 명시해 POST 방식 등 다른 방식으로 못 쓰게 막음
  • 2] @GetMapping 
    • 번잡한 method=RequestMethod.GET을 제거하기 위해 애노테이션을 수정함.

=> GET, POST 뿐만 아니라 PUT, DELETE, PATCH를 위한 애노테이션도 존재

 

2. 매개변수 - 파라미터

@RequestParam

HttpServletRequest의 값을 쉽게 꺼내올 수 있음

 

기존방식

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

새로운 방식

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

@RequestParam("username")은 request.getParamerter("username")과 거의 같은 코드라고 보면 됨.

 

3. 매개변수 - Model

파라미터로 Model 객체를 받을 수 있음. 스프링이 제공.

model.addAtrribute("member", member) 이런 식으로 저장해 jsp파일로 forward 가능

 

4. ViewName 직접 반환

ModelAndView 객체를 반환하던 기존 버전과는 달리 논리이름만 반환 가능.

 

: String username = request.getParamater("username)