🌱 Spring & Springboot

HandlerMapping 과 HandlerAdapter는 왜 나뉘었나요?

date
Jan 3, 2024
slug
why-handler-mapping-adapter-separate
author
status
Public
tags
Spring
DI
summary
HandlerMapping과 HandlerAdapter는 왜 나뉘어져 있는걸까요? 함께 알아봅시다.
type
Post
thumbnail
제목을-입력해주세요_-001 (29).png
category
🌱 Spring & Springboot
updatedAt
Jan 3, 2024 01:12 PM
오늘은 제가 참여하고 있는 부트캠프 과정인 Kernel360 에서 유명 자바 강사이신 박은종 디렉터님 과 함께 스터디를 진행하였습니다.
스터디의 주제는 Spring Web MVC Framework 이었고 그 중 해당 프레임워크의 구조에 대해서 이야기를 나누고 있었습니다.

Spring Web MVC 프레임워크의 처리 흐름

notion image
해당 이미지를 보고 처리 흐름을 알아보겠습니다. (요청의 흐름은 번호를 참고하면 됩니다)
  1. 클라이언트로 부터의 요청이 서버에 도착합니다.
  1. 서버의 요청은 Dispatcher Servlet 의 먼저 도달합니다. 이것은 Spring framework 이 프론트 컨트롤러 패턴으로 구성되어있기 때문인데요, 모든 요청을 적절한 처리기에 전달하는 역할을 합니다.
  1. 요청이 도착했으면 Dispathcer ServletHandlerMapping 을 사용해서 요청을 처리해야 할 적절한 컨트롤러를 찾게 됩니다.
  1. 이 때 적절한 컨트롤러를 찾았다면 Dispatcher Servlet 은 다시 HandlerAdapter 를 통해서 컨트롤러의 처리 메서드를 호출합니다.
  1. 이때 호출된 컨트롤러는 비즈니스 로직을 실행하고 데이터와 뷰의 이름을 반환합니다.
  1. 컨트롤러가 반환한 뷰 이름을 바탕으로 View Resolver 가 동작하여 해당하는 뷰 객체를 찾아줍니다.
  1. 그리고 뷰는 모델 데이터를 사용해 최종적으로 사용자에게 보여줘야 할 화면을 생성합니다.
  1. 마지막으로 생성된 뷰는 클라이언트에 응답으로 전송됩니다.
 
저는 이러한 구조를 보고 의문을 가지게 되었습니다.

왜 굳이 나뉘었을까?

notion image
해당 구조를 보니, Dispatcher ServletController 로 요청을 보낼 때, 두번에 거쳐서 요청을 보내게 됩니다.
먼저 HandlerMapping 을 통해서 적절한 컨트롤러를 찾아내고 HandlerAdapter 를 통해 실제 메서드를 호출하게 되죠.
이 때 HandlerMapping 에서 컨트롤러를 찾아서 바로 요청을 보내는게 더 간단하고 빠를 것 같다고 느꼈습니다.
왜 굳이 HandlerAdapter 를 통해서 요청을 보내게 될까요?
 

HandlerMapping과 HandlerAdapter를 알아보자

먼저 HandlerMappingHandlerAdapter 는 모두 인터페이스 구조로 되어있습니다.
 
HandlerMapping.java
public interface HandlerMapping { HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; }
HandlerMapping 인터페이스는 getHandler 라는 메서드를 정의하고 있는데요, 이 메서드는 HttpServletRequest 객체를 받아서 HandlerExecutionChain 객체를 반환합니다.
이 때, HandlerExecutionChain 객체는 요청을 처리할 핸들러 즉 Controller와 요청을 처리하기 전 후로 실행해야 하는 Interceptor 를 포함하고 있습니다.
 
HandlerAdapter.java
public interface HandlerAdapter { boolean supports(Object handler); ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; long getLastModified(HttpServletRequest request, Object handler); }
HandlerAdapter 인터페이스는 supports 메서드를 통해 해당 핸들러를 지원하는지 여부를 판단하고, handle 메서드를 통해 실제 요청을 처리합니다.
처리 결과는 ModelAndView 객체에 담겨 DispatcherServlet 으로 반환되며 이를 통해 뷰를 렌더링하고 클라이언트에게 응답을 보냅니다.
 
이렇게 두 인터페이스가 책임을 나누고 있는 이유는 다양한 종류의 컨트롤러를 지원하고 확장가능하게 만들도록 하기 위해서 입니다. 이 구조를 가짐으로써 Spring MVCPlug And Play 아키텍처를 가능하게 합니다.

Plug & Play

notion image
Plug and Play 아키텍처는 컴포넌트나 모듈을 시스템에 추가할 때 추가적인 설정 없이 쉽게 통합하고 사용할 수 있도록 설계하는 아키텍처를 말합니다.
따라서 소프트웨어가 독립적인 모듈로 구성되어 있어야하고, 컴포넌트를 쉽게 교체할 수 있어야 하고 이러한 작업이 일어났을 때 시스템의 재부팅이 필요 없이 자동으로 인식하고 필요한 구성을 처리할 수 있어야 합니다.
 
Spring 에서도 이미 저희는 플러그 앤 플레이 아키텍처를 경험했는데요,
SpringIOC container 는 애플리케이션의 객체를 빈 이라는 형태로 자동으로 생성하고 생명주기를 관리합니다. 개발자는 자유롭게 @Component , @Service , @Repository 등의 어노테이션으로 빈을 정의하고 Spring 은 이를 자동으로 인식하고 의존성 주입을 합니다.

컨트롤러가 다양한 종류가 있었다고?

HandlerMapping 은 요청의 URL을 보고 어떠한 컨트롤러가 처리할지를 결정하는 매핑 정보를 관리합니다.
그리고 HandlerAdapter 는 실제 DispatcherServlet 과 컨트롤러 사이의 호환성을 제공합니다.
이때 Controller 의 다양한 타입에 맞게 호환성을 가지고 동작하도록 책임을 나누게 된 것 입니다.
@Controller public class MyController { @ReqeustMapping("/myPath") public ModelAndView handleRequest() { // 컨트롤러 로직.. } } public class MyHttpRequestHandler implements HttpRequestHandler { public void handleRequest(HttpServletRequest request, HttpServletResponse response) { // 직접 서블릿 API를 사용하는 컨트롤러 로직.. } }
이건 가장 대표적인 두 종류의 컨트롤러 클래스 입니다.
MyController 는 어노테이션 기반의 컨트롤러이고, MyHttpRequestHandlerHttpRequestHandler 의 구현체로서, 서블릿 API를 직접 사용하는 형태의 컨트롤러 입니다.
이 두 컨트롤러는 요청을 처리하는 방식이 다릅니다.
 
HandlerMapping/myPath 와 같은 URL을 입력받았을 때, 어떠한 컨트롤러 객체를 사용할지 결정합니다. 하지만 여기서 바로 컨트롤러 객체를 호출하는 것은 불가능합니다. 왜냐하면 컨트롤러의 종류에 따라 호출 방법이 다르기 때문입니다. 이 때 HandlerAdapter 가 각 컨트롤러 타입에 맞는 HandlerAdapter 를 구현하여 DispatcherServlet 이 일관된 방식으로 요청을 처리하도록 합니다.
 
public class MyControllerHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return (handler instanceof MyController); } @Override public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) { return ((MyController)handler).handlerRequest(); } @Override public long getLastModified(HttpServletRequest request, Object handler) { // 마지막 수정 시간을 반환하는 로직 } }
위 코드는 간단한 HandlerAdapter 의 구현 예시입니다.
HandlerAdapterMyController 타입의 컨트롤러를 지원합니다.
public class HttpRequestHandlerAdapter implements HandlerAdapter { @Override public boolean supports(Object handler) { return (handler instanceof HttpRequestHandler); } @Override public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) { ((HttpRequestHandler) handler).handleRequest(request, response); return null; } @Override public long getLastModified(HttpServletRequest request, Object handler) { // 마지막 수정 시간을 반환하는 로직 } }
위 코드는 MyHttpRequestHandler 를 처리하는 예시입니다.
이처럼 각 타입마다 요청을 처리하는 방식이 다릅니다. 위는 ModelAndView 를 직접 반환이 가능하지만 아래 예제에서는 불가능합니다.
이는 간단한 예제이기 때문이고, 실제 구현은 더욱 복잡하게 구현되어 있으므로 모든 타입을 한번에 처리하기보다
HandlerMapping 을 통해 매핑 후 HandlerAdapter 를 통해 실제 호출 및 결과를 반환 받는 구조가 더 객체지향적인 구조가 됩니다.
 
좋은 인사이트를 나눠주신 박은종 디렉터님께 샤라웃을 날립니다. 🎉🎉🎉
 
참고하면 좋을 강의도 함께 남깁니다.