본문 바로가기
Springboot

[Spring] Spring MVC: HandlerAdapter

by 대복2 2025. 6. 1.

서론

지난 글에서 HandlerMapping이 어떠한 원리로 컨트롤러를 찾아내는지에 대해 살펴보았다. 이번 글에서는 찾은 컨트롤러를 어떻게 연결하게 되는지에 대해 기술한다.


HandlerAdapter (요리 보조)

HandlerAdapter는 HandlerMapping이 찾아준 핸들러를 실행할 수 있도록 해준다.

  • 역할: HandlerMapping이 찾아준 "특정 요리사(핸들러)가 실제로 요리(요청 처리)를 할 수 있도록" 옆에서 돕고, 요리사의 결과를 받아서 다음 단계로 넘겨준다.
  • 설명: 주문 접수원(HandlerMapping)이 "스테이크" 주문을 '스테이크 전문 요리사'에게 연결하고 요리사는 요리를 담당한다. 요리 보조(HandlerAdapter)는 요리사에게 필요한 재료를 준비해 주고, 요리사가 요리를 마치면 그 결과를 받아서 서빙 직원에게 전달한다. 요리사가 어떤 방식으로 요리를 하든(프라이팬 사용, 오븐 사용 등), 요리 보조는 그 방식에 맞춰 요리사를 어시스트한다.

==> 스프링에서 HandlerAdapter는

  • HandlerMapping이 찾아준 컨트롤러(Handler)를 실제로 실행(invoke)해 주는 역할
  • 메서드를 호출하고, 메서드에 필요한 인자(요청 파라미터 등)를 전달하고, 메서드의 반환 값(뷰 이름 등)을 받아서 처리
  • 다양한 형태의 컨트롤러들을 일관된 방식으로 처리할 수 있도록 추상화된 역할을 수행(@Controller 어노테이션을 사용하는 컨트롤러, 과거의 Controller 인터페이스를 구현하는 컨트롤러)
// 대부분의 스프링 MVC 애플리케이션에서 @Controller 어노테이션이 붙은 컨트롤러를 실행하는 데 사용된다

// OrderController의 orderSteak() 메서드가 호출될 때
// 1. HTTP 요청에서 필요한 파라미터(@RequestParam, @PathVariable 등)를 추출하여 orderSteak() 메서드의 인자로 전달
// 2. orderSteak() 메서드를 호출
// 3. orderSteak() 메서드가 반환한 값("steakOrderPage")을 받아서 ViewResolver에게 전달하여 적절한 뷰를 찾는다.

HandlerAdapter의 동작 원리

HandlerMapping으로부터 HandlerExecutionChain(핸들러와 인터셉터 목록)을 반환받은 후, HandlerAdapter가 동작

 

1. 핸들러 객체 획득

DispatcherServlet은 HandlerExecutionChain에서 실제 요청을 처리할 핸들러 객체(예: 특정 컨트롤러의 인스턴스)를 추출한다.

 

=> HandlerExecutionChain는 "A 요리사 + 소금 치기 + 플레이팅"이라는 정보의 묶음

 

2. HandlerAdapter 조회 및 선택

DispatcherServlet은 설정된 HandlerAdapter 빈들을 순회하며, 현재 핸들러를 처리할 수 있는 HandlerAdapter를 찾는다.

각 HandlerAdapter 구현체는 supports(Object handler) 메서드를 가지고 있어서 자신이 특정 타입의 핸들러를 지원하는지 여부를 DispatcherServlet에게 전달한다.

// HandlerAdapter 인터페이스의 일부
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
long getLastModified(HttpServletRequest request, Object handler); // Optional

// DispatcherServlet은 supports(handler) 메서드가 true를 반환하는 첫 번째 HandlerAdapter를 선택

 

=> 찾은 요리보조(HandlerAdapter)는 요리사가 어떤 방식으로 요리를 하든(예: @RequestParam으로 재료를 받든, @RequestBody로 반죽을 받든), 그 방식에 맞춰 필요한 재료를 준비해 주고, 요리가 끝나면 결과를 알아서 받아줄 수 있는 전문 보조이다.

여기서 DispatcherServlet은 요리 보조에게 "A요리사시켜서 요리를 완성해"라는 명령을 한다. 

 

3. 핸들러 실행 위임

선택된 HandlerAdapter는 이제 handle 메서드를 호출하여 실제 핸들러(컨트롤러 메서드)를 실행하도록 위임

handle 메서드 내부에서 각 HandlerAdapter는 자신이 지원하는 핸들러 타입에 맞춰 작업을 실행

  • 메세지 파라미터 처리: HTTP 요청에서 오는 param을 분석해 핸들러 메서드의 타입에 맞게 변환하고 주입
    (String -> int, Json을 특정 객체로 역직렬화 등)
  • 메서드 호출: 실제 핸들러 메서드를 호출
  • 메서드 반환 값 처리: 핸들러 메서드가 반환하는 값을 적절히 처리해 ModelAndView 혹은 응답 스트림 생성
  • 예외 처리: 핸들러 메서드 실행 중 발생하는 예외를 처리

=> 요리 보조(HandlerAdapter)는 이 지시를 받고, 요리사에게 필요한 재료(요청 파라미터)를 알아서 준비해서 주고, A 요리사가 스테이크를 요리하는 행위(컨트롤러 메서드 호출)를 진행시킵니다. 요리사가 요리를 마치면(메서드 실행 완료) 그 결과물(완성된 스테이크)을 받는다.

 

4. ModelAndView 반환

HandlerAdapter는 핸들러의 실행 결과로 ModelAndView 객체를 DispatcherServlet에게 반환


주요 HandlerAdapter 구현체

스프링은 다양한 HandlerAdapter 구현체를 제공하여 여러 종류의 핸들러를 지원한다.

스프링 부트를 사용하면 대부분 자동으로 등록된다.

 

- 리플렉션(Reflection)런타임 시점에 클래스나 메서드, 필드 등의 정보를 동적으로 접근하고 조작할 수 있도록 하는 기능(어노테이션이 이거 기반)

 

RequestMappingHandlerAdapter(Default)

  • @Controller 및 @RestController 어노테이션이 붙은 POJO 기반의 컨트롤러 메서드(즉, @RequestMapping 계열의 어노테이션이 붙은 메서드)를 처리
  • 리플렉션을 사용해 컨트롤러 메서드를 호출, HandlerMethodArgumentResolver를 사용하여 메서드 파라미터를 해결, HandlerMethodReturnValueHandler를 사용하여 메서드 반환 값을 처리
  • 지원 핸들러: HadnlerMethod (내부적으로 @RequestMapping이 적용된 메서드를 나타내는 객체)

HttpRequestHandlerAdapter

  • org.springframework.web.HttpRequestHandler 인터페이스를 구현하는 핸들러를 처리
  • 단일 handleRequest(HttpServletRequest request, HttpServletResponse response) 메서드를 가지고 있다.
  • 간단한 정적 리소스나 직접 HttpServeltResponse를 조작할 때 사용
  • 스프링의 리소스 핸들러(예: /resources/** 매핑)도 내부적으로 HttpRequestHandler를 사용

RouterFunctionAdapter(Spring Webflux)

  • 함수형 방식의 라우팅(RouterFunction)과 핸들러(HandlerFunction)를 처리

RequestMappingHandlerAdapter 확장

 

  • HandlerMethodArgumentResolver
    • 컨트롤러 메서드의 파라미터(인자)들을 해석하고 값을 주입하는 역할
    • 예를 들어, @RequestParam("name") String name이라는 파라미터가 있다면, RequestParamMethodArgumentResolver가 HTTP 요청에서 name이라는 파라미터를 찾아 String 타입으로 변환하여 name 변수에 주입
    • 커스텀 ArgumentResolver를 구현하여 원하는 방식으로 파라미터를 처리할 수 있다.
  • HandlerMethodReturnValueHandler
    • 컨트롤러 메서드의 반환 값을 해석하고 처리
    • 예를 들어, @ResponseBody 어노테이션이 붙은 메서드가 객체를 반환하면, RequestResponseBodyMethodProcessor가 해당 객체를 JSON 등으로 변환하여 HTTP 응답 바디에 작성
    • ModelAndView를 반환하는 경우, ModelAndViewMethodReturnValueHandler가 이를 처리
    • 마찬가지로, 커스텀 ReturnValueHandler를 구현하여 원하는 방식으로 반환 값을 처리할 수 있다.

 


HandlerAdapter이 왜 필요할까

스프링 MVC의 초기에는 컨트롤러를 만들 때 org.springframework.web.servlet.mvc.Controller 인터페이스를 구현해야 했다.

// 스프링 초창기 Controller 인터페이스 방식
public class MyOldController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // ... 요청 처리 로직 ...
        return new ModelAndView("viewName");
    }
}

 

하지만 이러한 방식은

  • 결합도가 높고(모든 컨트롤러가 인터페이스에 종속)
  • 유연성이 부족했다.(handlerRequest가 고정되어 다양한 param이나 반환 타입을 처리하기 힘듦)

그렇기에 스프링에서는 해당 문제에 대한 해결로 어노테이션 기반 컨트롤러를 도입해 Controller 인터페이스의 구현 없이 일반 자바 클래스에 @Controller, @RequestMapping 어노테이션을 통해 구현할 수 있게 되었다.

// POJO 기반의 스프링 MVC 컨트롤러 (현재 표준)
@Controller
public class MyNewController {
    @GetMapping("/hello")
    public String hello(@RequestParam String name, Model model) {
        model.addAttribute("message", "Hello, " + name + "!");
        return "helloView";
    }

    @PostMapping("/users")
    @ResponseBody
    public User createUser(@RequestBody User user) {
        // ... 사용자 생성 로직 ...
        return user;
    }
}

 

=> 이러한 방식으로 스프링은 POJO 형식의 컨트롤러를 구현할 수 있게 되었다.

 

// DispatcherServlet 내부
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
	// ...
                HttpServletRequest processedRequest = request;
                HandlerExecutionChain mappedHandler = null; // HandlerMapping이 반환할 핸들러 체인
                HandlerAdapter ha = null; // 선택된 HandlerAdapter
                ModelAndView mv = null; // 핸들러 실행 결과로 얻을 ModelAndView
    	// ...
        		
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = HttpMethod.GET.matches(method);
				if (isGet || HttpMethod.HEAD.matches(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}
                
				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
		// ...
        
// HandlerAdapter 인터페이스
	@Nullable
	ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

 

DispatcherServlet은 mv에게 Object 타입의 handler를 주게 된다. 여기서 알 수 있는 점은 DispatcherSerlvet은 handler가 어떤 클래스인지 모르고 메서드의 param이나 return 타입을 직접 알 수 없다.

 

이때 DispatcherServlet은 HandlerAdapter에게 컨트롤러 호출을 위임하게 되면서(ha.handle(processedRequest, response, mappedHandler.getHandler())) 리플렉션으로 파악한 컨트롤러 메서드(런타임 시점에 완성)를 찾아올 수 있게 된다.