[Spring MVC] 그래서 컨트롤러 메서드는 누가 호출하는데요? (Dispatcher Servlet, Handler Mapping, Handler Adapter, Argument Resolver)

dooboocookie·2023년 4월 21일
1

🤔궁금증❓

public int sum(int a, int b) {
	return a + b;
}
  • 위와 같은 메서드를 호출할 때 다음과 같이 호출할 것이다.
int result = sum(1, 2);
  • sum이라는 메서드를 호출하는 부분에서
    • 매개변수가 2개라는 것
    • 첫번째 매개변수 int, 두번째 매개변수 int
    • retun 타입이 int라는 것
  • 라는 내용을 이미 다 알고 그에 맞게 호출한다.

하지만, Spring MVC에서는?

/plays POST로 요청만 보냈을 뿐인데요?

@Controller
public class WebController {

    private final RacingCarService racingCarService;

    public WebController(final RacingCarService racingCarService) {
        this.racingCarService = racingCarService;
    }

    @PostMapping("/plays")
    public ResponseEntity<ResultResponse> play(@RequestBody final NamesAndCountRequest namesAndCount) {
        ResultResponse resultResponse = racingCarService.playGame(namesAndCount);
        return ResponseEntity.status(HttpStatus.CREATED)
                .contentType(MediaType.APPLICATION_JSON)
                .body(resultResponse);
    }

}
  • 이런 컨트롤러의 메서드를 어딘가에선 호출을 해야되는데, 호출을 Spring 내부에서하게 된다.
ResponseEntity<ResultResponse> result = ㅈwebController.play(namesAndCountRequest);
  • 이런식으로 호출을 하는 부분이 Spring 내부에 존재하게 된다는 건데...
  • Spring은 미리 작성되어 있는 프레임워크인건데, 개발자가 어떤 매개변수를 몇개 넣어서 어떤 리턴 타입으로 구현할 것을 예상을 한것일까...

이 글에서는 이 궁금증을 기반으로 Spring이 특정 요청을 처리하여 응답을 주는 과정을 가볍게 살펴볼 것이다.

🔢 요청 ➡️ 응답 과정

Servlet

  • 과거의 정적인 웹페이지만을 관리할 때는, 이미 만들어진 웹 페이지를 응답하였다.

  • 이후에 사용자의요청을 처리하여 그에 따른 동적인 응답을 반환하는 역할을 하는 Servlet이 등장하게 된다.
    • ex. 로그인한 회원에 따른 마이페이지를 보여준다던지...

(사진 출처 : https://tecoble.techcourse.co.kr/post/2021-05-23-servlet-servletcontainer/)

  • 아래는 javax.servlet.http.HttpServlet을 상속하여 요청에 대해서 처리를 담당하는 Servlet 클래스이다.
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class HelloServlet extends HttpServlet {
    // ...
    
    @Override
    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        // GET 요청을 처리
    }
    
    // ...

    @Override
    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        // POST 요청을 처리
    }
    
    // ...

    @Override
    protected void service(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
        // GET, POST, DELETE, PUT, ... 모든 요청을 처리
    }
    
    // ...
    
}
  • 특정 요청 시, 필요한 로직을 구현하면 된다.
  • 받은 HTTP 요청을 자바 객체로서 HttpServletRequest로 매개변수로 받아서 사용하면되고
  • 보내줄 HTTP 응답의 내용을 자바 객체(매개변수)로서 받아온 HttpServletResponse 이 객체에 정보를 담으면 된다.

Spring의 Dispatcher Servlet

[spring.io 설명(링크)]
HTTP 요청 컨트롤러(핸들러)를 위한 중앙 디스패처이다.
웹 요청을 처리하기 위해 등록된 핸들러를 디스패치하고,
편리하게 매핑하고, 예외 핸들링을 지원한다.

  • Dispatcher Servlet은 HttpServlet을 상속한다.
  • Dispatcher Servlet은 모든 요청을 받아서 요청에 맞는 컨트롤러(핸들러)를 연결해주는 역할을 한다.
  • Dispatcher Servlet의 요청 처리
    • 일단, 그 요청을 처리하는 컨트롤러(핸들러)를 찾는다.
    • 있으면, 해당 컨트롤러에서 처리 결과를 받아 응답
    • 없으면, 정적 자원(/resources 디렉토리의 하위) 영역을 살펴본 뒤 응답

그래서 그건 누가 결정할까?

  • Spring의 요청 응답의 과정은 다음과 같다

(사진 출처: https://mangkyu.tistory.com/49)

  • 이제 위 사진의 역할을 하나씩 살펴보자.

🗺️ Handler Mapping

Handler Mapping(spring.io)
요청컨트롤러(핸들러)를 매핑을 정의하는 객체에 대한 인터페이스다.
사용자가 정의 가능하다.
사용자가 Handler Mapping을 빈으로 등록하지 않는다면 기본적으로 BeanNameUrlHandlerMappingRequestMappingHandlerMapping이 기본 값이다.

  • Dispatcher Servlet이 초기화될 때, 스프링에 등록된 HandlerMapping 구현체들은 컨트롤러(핸들러)들과 요청 URL을 매핑한다.

  • 구현체의 종류와 순서
    1. RequestMappingHandlerMapping
    2. BeanNameUrlHandlerMapping
    3. SimpleUrlHandlerMapping

RequestMappingHandlerMapping

  • @RequestMapping이 붙어있는 정보와 URL 요청을 매핑한다.
    • @GetMapping@PostMapping@RequestMapping을 포함한다.
@Controller
public class WebController {

    private final RacingCarService racingCarService;

    public WebController(final RacingCarService racingCarService) {
        this.racingCarService = racingCarService;
    }

    @PostMapping("/plays")
    public ResponseEntity<ResultResponse> play(@RequestBody final NamesAndCountRequest namesAndCount) {
        ResultResponse resultResponse = racingCarService.playGame(namesAndCount);
        return ResponseEntity.status(HttpStatus.CREATED)
                .contentType(MediaType.APPLICATION_JSON)
                .body(resultResponse);
    }

}
  • 위와 같이 @GetMapping("/plays")라는 어노테이션을 읽어 /plays GET요청과 매핑한다.

BeanNameUrlHandlerMapping

  • /로 시작하는 이름을 가진 빈과 요청 URL을 매핑한다.
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Component("/plays")
public class PlayController implements Controller {
    @Override
    public ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
        return new ModelAndView();
    }
}
  • 빈 하나를 하나의 요청에 매핑하는 것
  • /plays라는 이름을 가진 빈을 /plays요청에 매핑하는 것이다.

🔌 Handler Adapter

  • 방금 Handler Mapping을 설명하는데에도 여러 유형의 컨트롤러(핸들러)를 예시로 들었다.
  • 특정 유형의 컨트롤러(핸들러)를 호출하는 역할을 DisaptcherServlet에는 포함하지 않는다.
  • 특정 유형의 컨트롤러(핸들러)의 메서드를 호출하는 역할을 한다.
  • Handler Adapter는 특정 컨트롤러(핸들러)의 메서드를 실행시키고, 결과를 ModelAndView 객체로 반환한다.
    • Model은 컨트롤러(핸들러)로부터 View에 전달하기 위한 데이터를 의미한다.
    • View는 클라이언트에게 보여줄 화면 정보를 의미한다.

  • 구현체 종류와 순서
    1. RequestMappingHandlerAdapter
    2. HttpRequestHandlerAdapter
    3. SimpleControllerHandlerAdapter

컨트롤러(핸들러) 메서드를 실행시키는건 핸들러 어뎁터가 알아서 하고,
DispatcerServlet이 Handler Adapter에게 바라는 형태는 ModelAndView다.

HttpRequestHandlerAdapter

  • HttpRequestHandler를 구현하여 등록한 컨트롤러(핸들러) 메서드를 실행시킨다.
import org.springframework.stereotype.Component;
import org.springframework.web.HttpRequestHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


@Component("/plays")
public class PlayController implements HttpRequestHandler {
    @Override
    public void handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException {
        // ...
    }
}

SimpleControllerHandlerAdapter

  • org.springframework.web.servlet.mvc.Controller을 구현한 핸들러 메서드를 실행시킨다.
    • org.springframework.stereotype.Controller아님 주의
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


@Component("/plays")
public class PlayController implements Controller {
    @Override
    public ModelAndView handleRequest(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
        return new ModelAndView();
    }
}

RequestMappingHandlerAdapter

  • @RequestMapping 어노테이션으로 등록된 핸들러 메서드를 실행시킨다.
@Controller
public class WebController {

    private final RacingCarService racingCarService;

    public WebController(final RacingCarService racingCarService) {
        this.racingCarService = racingCarService;
    }

    @PostMapping("/plays")
    public ResponseEntity<ResultResponse> play(@RequestBody final NamesAndCountRequest namesAndCount) {
        ResultResponse resultResponse = racingCarService.playGame(namesAndCount);
        return ResponseEntity.status(HttpStatus.CREATED)
                .contentType(MediaType.APPLICATION_JSON)
                .body(resultResponse);
    }

}

HttpRequestHanlder와 Controller를 구현하여 특정 메서드를 오버라이딩 해서 사용하는 핸들러와는 다르게,

어노테이션 기반의 핸들러는 매개변수와 리턴타입이 딱 하나로 정의되어 있지 않고, 개발자가 핸들러를 구현할 때, 여러 케이스를 고려할 수 있다.

📨 그렇다면 이 매개변수와 리턴타입은 누가 결정할 것인가?

HandlerMethodArgumentResolver

  • 어노테이션 기반의 핸들러 메서드에는 여러 매개변수가 올 수 있다.
    • HttpServletRequest
    • ModelAndView
    • HttpEntity
    • @ReqeusetParam
    • @ReqeustBody
    • ...
  • ArgumentResolver는 핸들러 어댑터에서 호출할 핸들러 메서드의 파라미터 정보를 가공하는 역할을 한다.

(사진 출처 : https://platanus.me/post/1678)

HandlerMethodReturnValueHandler

  • 어노테이션 기반의 핸들러 메서드는 리턴타입 또한 여러 타입으로 올 수 있다.
    • String
    • ModelAndView
    • ResponseEntity
    • @ResponseBody
    • ...
  • Return Value Handler는 핸들러의 반환 값을 지원하는 반환타입인지 확인 후 반환 값을 처리한다.

이 메서드를 처리하는 구현체들을 간략하게 살펴보자

    @PostMapping("/plays")
    public ResponseEntity<ResultResponse> play(@RequestBody final NamesAndCountRequest namesAndCount) {
        ResultResponse resultResponse = racingCarService.playGame(namesAndCount);
        return ResponseEntity.status(HttpStatus.CREATED)
                .contentType(MediaType.APPLICATION_JSON)
                .body(resultResponse);
    }

RequestResponseBodyMethodProcessor

  • HandlerMethodArgumentResolver와 HandlerMethodReturnValueHandler의 구현체이다.

[RequestResponseBodyMethodProcessor(공식문서 링크)]

@ReqeustBody나 @ResponseBody의 어노테이션을 보고 메서드의 매개변수나 반환값을 HttpMessageConverter로 처리한다.

HttpEntityMethodProcessor

  • HandlerMethodArgumentResolver와 HandlerMethodReturnValueHandler의 구현체이다.

[HttpEntityMethodProcessor(공식문서 링크)]

HttpEntity... 즉,
자식인 RequestEntity를 매개변수로 받을 때나
자식인 ResponseEntity를 리턴타입으로할 때 처리한다.

  • 여기서도 HttpMessageConverter를 사용한다.

HttpMessageConverter의 역할

  • 미디어타입을 체크하고 Http Message를 읽어 변환한다.
    • 미디어타입이 application/json일 때, MappingJackson2HttpMessageConverter로 변환한다.
      • HTTP 메세지의 JSON을 적절한 객체로 변환하거나
        - 객체를 HTTP 메세지의 JSON으로 변환하는 역할을 한다.
  • HttpMessageConverter는 미디어 타입이나 변환 대상에 따라 다른 구현체가 작동한다.
  • (매우 구체적인 내용이 있으니 필요하면 찾아보자..😭)

😳 정리

  • DispatcherSevlet : 모든 요청을 받아 그 요청을 핸들러 매핑에 의해 어떤 핸들러인지 파악을 하고 핸들러 어댑터에게 위임한고 응답한다.
  • Handler Mapping : 디스패처 서블릿이 초기화될 때 핸들러를 URL과 매핑한다. 요청이 오면 그에 맞는 핸들러를 알려준다.
  • Handler Adapter : 핸들러의 종류에 따라 핸들러를 호출하여 ModelAndView를 반환한다.
  • Argument Resolver, Return Value Handler : 어노테이션 기반의 핸들러의 아규먼트와 리턴값을 처리한다.
profile
1일 1산책 1커밋

0개의 댓글