Spring

[Spring] 스프링 MVC 구조 이해하기

이덩우 2023. 7. 11. 00:04

이전 포스팅에서는 프론트 컨트롤러 패턴을 도입한 MVC 프레임워크를 버전 1부터 5까지, 점진적으로 발전시키며 개발해봤다.

결과적으로 V5에서는 어댑터 패턴을 도입해, 보다 유연한 MVC 프레임워크를 만들 수 있었다.

지속적인 개선과 발전으로 만들어진 프레임워크 V5는 스프링 MVC와 매우 유사한 형태까지 도달했다.

사용하던 용어들이 어떻게 매칭되는지, 스프링 MVC는 어떻게 동작하는지 알아보고

직접 프레임워크 V5를 스프링 MVC 구조로 만들어보자.

 

이전 포스팅 : https://dong-woo.tistory.com/80

 

[Spring] 프론트 컨트롤러를 도입한 MVC 프레임워크 만들기

이전 포스팅에서 서블릿 + JSP를 활용한 MVC 패턴으로 웹 애플리케이션을 설계해봤다. 컨트롤러와 뷰의 역할을 나누어 설계함으로써. JSP파일에서는 오직 뷰 렌더링에만 집중할 수 있었다. 하지만

dong-woo.tistory.com

 


 

-  스프링  MVC 전체 구조

직접 만든 MVC 프레임워크 구조
스프링 MVC 구조

  • 직접 만든 V5 버전의 프레임워크와 스프링 MVC 프레임워크의 차이점은 뭘까?
직접 만든 프레임워크 스프링 MVC
FrontController DispatcherServlet
handlerMappingMap HandlerMapping
MyHandlerAdapter HandlerAdapter
ModelView ModelAndView
viewResolver ViewResolver
MyView View
  • V5를 만들 때 의도적으로 스프링 MVC와 이름을 비슷하게 만들었다.
  • FrontController의 역할을 스프링 MVC에선 DispatcherServlet이 한다는 점을 제외하고는 비슷하다.
  • DispatcherServlet도 부모 클래스에서 HttpServlet을 상속 받아 사용하기 때문에 서블릿으로 동작한다.
  • 스프링 부트는 이 DispatcherServlet을 서블릿으로 자동으로 등록하면서 모든경로(urlPatterns="/")에 대해서 매핑한다.
  • 자, 요청 흐름을 살펴보자.
    1. 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출된다.
    2. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 service()를 오버라이드 해두었다.
    3. 따라서 service()에 의해 여러 메소드가 호출되면서 DispatcherServlet.doDispatch()가 호출된다
    4. doDispatch()가 핵심이다. 해당 메소드에서 핸들러 조회, 핸들러 어댑터 조회, 핸들러 실행, ModelAndView 반환 작업이 발생한다. 이어서 뷰 리졸버를 통해 View를 반환받고 렌더링 하는 과정이 순차적으로 발생한다.
  • 위의 흐름을 바탕으로 전체적인 구조를 살펴보자.
    1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
    2. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
    3. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다.
    4. 핸들러(컨트롤러) 실행 : 핸들러 어댑터가 실제 핸들러(컨트롤러)를 실행한다.
    5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다. 이게 어댑터의 역할이다! 실제 핸들러가 ModelAndView를 반환하지 않더라도 데이터를 가공해 ModelAndView로 반환해준다.
    6. ViewResolver 호출 : 뷰 리졸버를 찾고 실행한다.
    7. View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
    8. 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링 한다.

 


 

- 핸들러 & 핸들러 어댑터

  • 스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었다. 개발자가 직접 핸들러 매핑과 핸들러 어댑터를 만드는 일은 거의 없다.
  • 스프링이 자동으로 등록하는 핸들러 매핑과 핸들러 어댑터는 무엇이 있을까?
  • HandlerMapping
    1. RequestMappinghHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
    2. BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.
  • HandlerAdapter
    1.  RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
    2. HttpRequestHandlerAdapter : HttpRequestHandler 처리
    3. SimpleControllerHandlerAdapter : 구 버전인 Controller 인터페이스에서 사용
  • 이해가 잘 안될 수 있다. 스프링이 자동으로 핸들러 매핑을 등록한다는 것은, 컨트롤러로 인식하고 핸들러 매핑에 등록해둔다는 것이다!
  • 실제 실무에서는 99%로 핸들러와 핸들러 어댑터 모두 @RequestMapping 방식을 이용한다. 정말 간단하기 때문이다.

 


 

- View Resolver

  • 스프링은 뷰 리졸버 또한 제공한다. 제공하는 뷰 리졸버를 사용하기 위해서는 application.properties에 몇가지 설정이 필요하다.
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

논리 이름을 물리 이름으로 매핑해주기 위해 앞, 뒤로 붙여줄 전체 경로를 지정해주는 과정이다.

  • 지금은 JSP에 대한 뷰 리졸버를 다뤄봤지만 실제로는 아래와 같은 우선순위로 뷰 리졸버를 자동으로 등록한다.
    1. BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환 (엑셀 파일 등)
    2. InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
  • 참고로, 다른 뷰 템플릿은 실제 뷰를 렌더링하지만 JSP의 경우 forward() 를 통해서 JSP로 이동해야 렌더링이 된다. JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링 된다.
  • 추후 사용할 타임리프(Thymeleaf) 뷰 템플릿을 사용하면 ThymeleafViewResolver를 등록해야 한다. 최근에는 라이브러리만 추가하면 스프링 부트가 이런 작업도 모두 자동화해준다.

 


 

- 스프링 MVC로 전환

스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작해서, 매우 유연하고 실용적이다. 바로 @RequestMapping 애노테이션을 사용하는 컨트롤러이다. 

앞서 보았듯이 가장 우선순위가 높은 핸들러 매핑과 핸들러 어댑터는 @RequestMapping 애노테이션 기반이었다!

자 이제 본격적으로 애노테이션 기반의 컨트롤러를 만들어보자.

지금까지 만들었던 프레임워크에서 사용했던 컨트롤러를 @RequestMapping 기반의 스프링 MVC 컨트롤러로 변경해보자.

- V1

// 회원 등록 폼 컨트롤러

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}
// 회원 저장 컨트롤러

@Controller
public class SpringMemberSaveControllerV1 {
    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;
    }

}
// 전체 회원 목록 조회 컨트롤러

@Controller
public class SpringMemberListControllerV1 {

    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를 사용한 이유는 간단하게 컨트롤러로 인식하기 위함도 있지만 한 가지 이유가 더 있다.
  • RequestMappingHandlerMapping을 통해 핸들러를 찾는 방식을 실무에서는 99.9% 사용한다고 했다.
  • 이녀석은 스프링 빈 중에서 @Controller 애노테이션이 클래스 레벨에 붙어있는 경우에 매핑 정보로 인식한다.
  • 결국 getHandler()를 사용하기 위함이다!
  • 직접 만든 프레임워크와 비교해보면, 기존에는 핸들러 매핑 정보도 직접 만들고, 핸들러 어댑터도 찾아와서 연결하고, ModelView를 반환받고 뷰 렌더링하는 과정을 하나하나 직접 했지만, 이제는 @Controller를 클래스 레벨에 붙이고 요청 URL을 @RequestMapping을 통해 적어두고, 로직을 작성한 뒤 ModelAndView를 반환만해주면 나머지 과정은 스프링 MVC에서 자동으로 해준다.
  • 너무 편해졌다.
  • 하지만 코드를 작성하기 귀찮은 개발자의 욕심은 끝이 없다. 조금 더 간단하게 만들어보자.

- V2

// 컨트롤러 통합

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

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


}
  • 기존에는 비슷한 도메인에서 일하는 컨트롤러들이 쪼개져있었다.
  • 어느정도 연관성있는 컨트롤러들은 간편하게 하나의 컨트롤러로 통합할 수 있다!
  • 추가로 클래스 레벨에 @RequestMapping으로 공통되는 URL 정보를 매핑해두고, 논리적인 URL만 각각 따로 매핑하는 모습을 볼 수 있다.
  • 하나로 통합해 간편해졌지만, @RequestMapping을 두번 쓰는것도 마음에 안든다.
  • 게다가 메소드마다 ModelAndView 객체를 생성하고 반환해야 하는게 번거롭다.
  • 조금 더 바꿔보자.

- V3

// 최종

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

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


}
  • 완벽해졌다!
  • 이제는 메소드들의 서블릿 종속성또한 없어졌다. @RequestParam 애노테이션으로 간단하게 요청으로 넘어온 데이터를 확인할 수 있다.
  • 추가로, @RequestMapping은 요청이 GET인지, POST인지 구분하지 않고 다 받아준다. 이는 분명 허술한 점이 생길 것이다.
  • 따라서 명시적으로 @RequestMapping@GetMapping, @PostMapping 으로 바꿔서 애노테이션의 중복도 없애고 원하는 HTTP 메소드만 받을 수 있도록 만들었다.

 


 

- 정리

  • 서블릿과 JSP만 사용해서 만들었던 웹 애플리케이션부터 MVC 패턴의 도입, 프론트 컨트롤러의 도입 등을 거쳐 정말 여러가지의 버전으로 발전시키면서 현재의 @RequestMapping, @Controller 기반의 스프링 MVC 까지 도착했다.
  • 프로젝트를 할때는 단지 '스프링의 사용법이 이런거구나'  라며 넘겼던 부분들, 왜 이렇게 중간과정 다 건너뛰고 몇줄의 코드만으로 동작하게 되는지 등에 대한 의문이 모두 해결되는 과정이었다. 
  • 맨바닥부터 직접 하나하나 발전시키며 현재의 스프링 MVC 구조까지 왔기에 왜 이런 코드가 쓰였고 중간과정에서 생략되는 부분은 어디서 처리되는지 모두 이해하게 되는 좋은 경험이었다!

 

 

 

출처 : 인프런, 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술