숑숑이의 개발일기
article thumbnail

스프링 MVC 전체 구조

우리가 여태 직접 만들었던 프레임워크와 스프링 MVC를 비교하면 아래와 같다.

  • FrontController => DispatcherServlet
  • handlerMappingMap => HandlerMapping
  • MyHandlerAdapter => HandlerAdapter
  • viewResolver => ViewResolver
  • MyView => View

Spring MVC의 동작 순서

  1. 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
  2. 핸들러를 실행할 수 있는 핸들러 어댑터를 조회하고, 실행한다.
  3. 핸들러 어댑터가 실제 핸들러를 실행한다.
  4. 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환하여 반환한다.
  5. viewResolver를 찾고 실행한다.
  6. 뷰 리졸버는 뷰의 논리이름을 물리이름으로 변환, 뷰 객체를 반환한다.
  7. 뷰를 통하여 뷰를 렌더링 한다.

 

이제 핵심적인 구조를 파악해보도록 한다.

DispatcherServlet의 구조

스프링 MVC도 프론트 컨트롤러 패턴으로 구현되어 있는데, 이것이 바로 디스패처 서블릿이다. 그리고 이 디스패처 서블릿이 바로 스프링 MVC의 핵심이다.

 

DispatcherServlet도 부모 클래스에서 HttpServlet을 상속 받아서 사용하고, 서블릿으로 동작한다.

 

스프링 부트는 DispatcherServlet을 서블릿으로 자동 등록하면서 모든 경로(urlPatterns="/")에 대해 매핑한다. 참고로, 더 자세한 경로가 우선순위가 높으므로 기존에 등록한 서블릿도 함께 동작한다.

 

  1. 서블릿이 호출되면 HttpServlet이 제공하는 service() 메서드 호출
  2. DispatcherServlet의 부모인 FrameworkServlet에서 service() 메서드를 오버라이드 해두어 FrameworkServlet.service() 메서드가 호출
  3. 이를 시작으로 여러 메서드가 호출되면서 결국 DispatcherServlet.doDispatch() 메서드가 호출됨.

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

지금은 전혀 사용하지 않지만, 과거에 주로 사용했던 스프링이 제공하는 간단한 컨트롤러가 있다. 스프링도 처음에는 아래와 같은 딱딱한 형식의 컨트롤러를 제공했고, 참고로 이 Controller 인터페이스는 @Controller 애노테이션과는 다르다.

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

 

해당 인터페이스를 활용하여 간단하게 구현해보도록 한다.

 

참고로 org.springframework.web.servlet.mvc.Controller에 있는 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");
    }
}
  • 해당 컨트롤러는 /springmvc/old-controller 라는 이름으로 스프링 빈으로 등록되었다.
  • 빈의 이름인 /springmvc/old-controller로 URL을 매핑할 것.

 

해당 컨트롤러가 호출되려면 다음 2가지가 필요하다.

  • HandlerMapping(핸들러 매핑) : 핸들러 매핑에서 이 컨트롤러를 찾을 수 있어야 한다.
  • HandlerAdapter(핸들러 어댑터) : 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 어댑터가 필요하다.

스프링 부트가 자동 등록하는 핸들러 매핑과 핸들러 어댑터가 존재한다. 실제로는 더 많이 존재하지만, 중요한 부분을 위주로 설명하기 위해 일부 생략한다.

 

스프링 부트가 자동 등록하는 HandlerMapping

  • 0순위 : RequestMappingHandlerMapping : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
  • 1순위 : BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾음

 

스프링 부트가 자동 등록하는 HandlerAdapter

  • 0순위 : RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
  • 1순위 : HttpRequestHandlerAdapter : HttpRequestHandler 처리
  • 2순위 : SimpleControllerHandlerAdapter : Controller 인터페이스 처리

모두 순서대로 찾고 없으면 다음 순위로 넘어간다.

정리하자면 위의 코드인 OldController 실행시 사용된 객체는 다음과 같다.

  • HandlerMapping => BeanNameUrlHandlerMapping
  • HandlerAdapter => SimpleControllerHandlerAdapter

이어서 설명하겠지만, 잘 살펴보면 가장 우선순위가 높은 핸들러 매핑과 어댑터는 RequestMappingHadlerMapping, Adapter이다. 이것이 바로 지금 스프링에서 주로 사용하는 애노테이션 기반의 컨트롤러를 지원하는 매핑과 어댑터라고 알고있자.

 

뷰 리졸버

우선 기존의 OldController에서 View를 사용할 수 있도록 아래의 코드를 추가해준다.

return new ModelAndView("new-form");

 

 

그리고 application.properties에 아래의 코드를 추가한다.

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

 

 

스프링 부트는 위의 설정 정보를 사용하여 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록한다. 

 

스프링 부트가 자동 등록하는 뷰 리졸버는 아래와 같다. (실제로는 더많음)

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

 

스프링 MVC

스프링은 애노테이션을 활용한 매우 유연하고, 실용적인 컨트롤러를 만들었다. 이것이 바로 @RequestMapping 애노테이션을 사용하는 컨트롤러이다.

 

이제 본격적으로 애노테이션 기반의 컨트롤러를 사용해보도록 한다.

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process() {
        return new ModelAndView("new-form");
    }
}
  • @Controller : 스프링이 자동으로 스프링 빈으로 등록한다. @Controller 애노테이션 내부에는 @Component 애노테이션이 있어 컴포넌트 스캔의 대상이 된다.
  • @RequestMapping : 해당 url이 호출되면 이 메서드가 호출된다. 메서드의 이름은 임의로 지으면 된다.
스프링 부트 3.0부터는 클래스레벨에 @Controller가 있어야 스프링 컨트롤러로 인식한다.

 

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

그리고 스프링이 제공하는 ModelAndView를 통해 Model에 데이터를 추가할 수 있다. 이때에는 addObject() 메서드를 사용하여 데이터를 담으면 된다.

 

이제 V1에서 클래스 단위로 통합하여보도록 한다.

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;

@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()
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();
        ModelAndView mv = new ModelAndView("members");
        mv.addObject("members", members);
        return mv;
    }

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

 

위 글은 김영한 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술의 일부 내용을 정리한 것입니다.
profile

숑숑이의 개발일기

@숑숑-

풀스택 개발자 준비중입니다