스프링 MVC-스프링 MVC의 구조

스프링 MVC

수제

스프링mvc

위의 사진이 우리가 지금까지 만든 최종버전의 MVC 모델이고, 아래의 사진이 스프링 MVC 프레임워크의 구조이다.
커리큘럼을 따라간 결과 거의 똑같은 구조로 완성된 것을 볼 수 있다. 이제부터 스프링 MVC의 각 요소에 대해 알아보자.

DispatcherServlet

DispatcherServlet은 스프링 MVC의 프론트 컨트롤러 역할을 한다. HttpServlet을 상속받아서 서블릿으로 동작하며, 서블릿이 호출되면 내부의 service()를 필두로 여러 메서드와 함께 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);
    }

위의 코드가 예외처리, 인터셉터 기능을 제외한 doDispatch() 메서드이다. 위의 MVC 구조 사진에 나온 순서대로 doDispatch() 메서드 내부에서 실행되는 것을 볼 수 있다.
정확한 동작 순서는 다음과 같다.

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

핸들러 매핑과 어댑터

현재 자주 사용되는 어노테이션 방식이 아닌, Controller 인터페이스를 구현하는 간단한 컨트롤러를 통해 핸들러 매핑과 어댑터가 어떻게 사용되는지 알아보자.

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

위 코드가 간단하게 구현한 컨트롤러이다. 해당 컨트롤러의 URI를 입력하여 웹에서 호출한 결과 정상작동하는 것을 확인할 수 있었다. 어떻게 정상작동 할 수 있었을까?

이 컨트롤러가 호출되기 위해선 두 가지 과정이 필요하다. 핸들러 매핑에서 해당 핸들러를 찾을 수 있어야 하고, 해당 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다.
스프링 부트에서 핸들러 매핑과 어댑터는 간단하게 축약하여 다음과 같은 순서로 실행된다.
HandlerMapping

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

HandlerAdapter

0 = RequestMappingHandlerAdapter : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리
2 = SimpleControllerHandlerAdapter : Controller 인터페이스(애노테이션X, 과거에 사용) 처리

핸들러 매핑과 핸들러 어댑터 모두 순서대로 찾은 후 없다면 다음 순서로 넘어간다.
위 컨트롤러가 실행된 배경은 다음과 같다. 먼저 핸들러 매핑에서 0번이 실행된다. 애노테이션 기반으로 컨트롤러를 구현하지 않았으므로 당연히 찾지 못한다.
그 다음 순서인 1번이 실행되고, 요청에서 보낸 URI와 핸들러의 빈 이름이 동일하므로 핸들러를 찾아낸다.
그 후 핸들러 어댑터를 찾는다. 똑같이 0번을 실행, 찾지 못한다. 이후 1번을 실행하고 HttpRequestHandler가 아니므로 찾지 못한다. 2번을 실행하면 Controller 인터페이스를 지원하므로 해당 어댑터 내부에서 위의 컨트롤러를 실행하고 결과를 반환한다.

또 다른 예시로 Controller 인터페이스 대신 서블릿과 가장 유사한 형태의 핸들러인 HttpRequestHandler를 구현한 컨트롤러도 만들어보자.

  
package hello.servlet.web.springmvc.old;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpRequestHandler;

import java.io.IOException;

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

위의 코드는 핸들러 매핑은 빈 이름으로 똑같이 처리되고, 핸들러 어댑터를 찾을 때 HttpRequestHandler를 구현했으으로 1번인 HttpRequestHandlerAdapter가 해당 컨트롤러를 실행하게 된다.

ViewResolver

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

위에서 본 컨트롤러의 코드에 return new ModelAndView(“new-form”); 라는 문장이 추가되었다. 그냥 실행하면 작동하지 않지만, application.properties에 prefix와 suffix 설정을 해주고 실행하면 정상 작동한다.
이것은 ViewResolver에서 “new-form”이라는 논리경로를 application.properties에서 설정을 통해 절대경로로 바꿔 연결해주었기 때문인데, 자세한 실행 방식은 다음과 같다.

  1. 핸들러 어댑터 호출
    핸들러 어댑터를 통해 new-form 이라는 논리 뷰 이름을 획득한다.
  2. ViewResolver 호출
    new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출한다. BeanNameViewResolver 는 new-form 이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다. InternalResourceViewResolver 가 호출된다.
  3. InternalResourceViewResolver
    이 뷰 리졸버는 InternalResourceView 를 반환한다.
  4. 뷰 - InternalResourceView
    InternalResourceView 는 JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용한다.
  5. view.render()
    view.render() 가 호출되고 InternalResourceView 는 forward() 를 사용해서 JSP를 실행한다.

@RequestMapping

스프링은 어노테이션을 통한 MVC 프레임워크를 통해 유연하면서도 강력한 컨트롤러를 만들어냈다. 지금까지 최종버전으로 리팩토링했던 회원의 등록,조회,저장 컨트롤러를 @RequestMapping을 이용하는 방식으로 바꿔보자.

  
package hello.servlet.web.springmvc.v3;

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.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    MemberRepository memberRepository=MemberRepository.getInstance();
    @GetMapping(value = "/new-form")
    public String newForm() {
        return "new-form";
    }
    @GetMapping()
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();
        model.addAttribute("members", members);
        return "members";
    }
    @PostMapping(value = "/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";
    }
}

어노테이션 방식으로 구현한 컨트롤러이다. 무엇이 바뀌었는지 하나씩 알아보자.

가장 큰 변경점은 3개로 나뉘어져 있던 컨트롤러가 하나로 합쳐진 것이다.
클래스 내부에서 어노테이션을 통해 경로를 지정해줄 수 있기 때문에 컨트롤러를 하나로 통합할 수 있었다.
또한 “/springmvc/v3/members”까지는 중복되던 경로를 클래스 단에서 @RequestMapping으로 입력해주면, 나머지 메서드 단에서는 논리 경로만 입력하여 사용할 수 있다.
두 번째는 이제 ModelView를 반환하는 것이 아닌 View의 논리 이름을 반환하는 방식으로 바뀌었다. 어노테이션 기반의 컨트롤러는 인터페이스로 고정되어 있지 않고, 매우 유연하기 때문에 ModelAndView를 반환하는 방식도, String으로 view의 논리 이름을 반환하는 방식도 모두 지원한다.
세 번째는 save에서 @RequestParam 어노테이션을 사용하여 이제 request의 파라미터를 직접 매개변수로 받을 수 있게 되었다. 또한 Model을 파라미터로 받아 Model에 직접 접근할 수 있게 되었다.
마지막으로 HTTP 메서드를 제한할 수 있게 되었다. @RequestMapping 대신 @GetMapping, @PostMapping을 사용하면 해당하는 HTTP 메서드의 요청만이 컨트롤러에 접근할 수 있게 된다.
HTTP 파트에서 배웠듯이 GET의 경우 여러번 접근해도 똑같은 결과가 나오는, 즉 내부 데이터에 변화가 없는 상태여야 하는데 그러하지 못한 메서드에 GET으로 접근할 수 없게 하는 등의 용도로 사용할 수 있다.

어노테이션 방식을 사용하는 것으로 굉장히 많은 장점이 있고, 코드도 가독성이 좋고 깔끔해진 것을 볼 수 있었다. 스프링 MVC의 강력함의 편린을 본 느낌이다.
다음 포스팅에선 이외에 스프링 MVC의 여러 기능들을 알아보도록 하겠다.