🌱 Spring & Springboot
HandlerMapping 과 HandlerAdapter는 왜 나뉘었나요?
오늘은 제가 참여하고 있는 부트캠프 과정인
Kernel360
에서 유명 자바 강사이신 박은종 디렉터님
과 함께 스터디를 진행하였습니다.스터디의 주제는 Spring Web MVC Framework 이었고 그 중 해당 프레임워크의 구조에 대해서 이야기를 나누고 있었습니다.
Spring Web MVC 프레임워크의 처리 흐름
해당 이미지를 보고 처리 흐름을 알아보겠습니다. (요청의 흐름은 번호를 참고하면 됩니다)
- 클라이언트로 부터의 요청이 서버에 도착합니다.
- 서버의 요청은
Dispatcher Servlet
의 먼저 도달합니다. 이것은 Spring framework 이 프론트 컨트롤러 패턴으로 구성되어있기 때문인데요, 모든 요청을 적절한 처리기에 전달하는 역할을 합니다.
- 요청이 도착했으면
Dispathcer Servlet
은HandlerMapping
을 사용해서 요청을 처리해야 할 적절한 컨트롤러를 찾게 됩니다.
- 이 때 적절한 컨트롤러를 찾았다면
Dispatcher Servlet
은 다시HandlerAdapter
를 통해서 컨트롤러의 처리 메서드를 호출합니다.
- 이때 호출된 컨트롤러는 비즈니스 로직을 실행하고 데이터와 뷰의 이름을 반환합니다.
- 컨트롤러가 반환한 뷰 이름을 바탕으로
View Resolver
가 동작하여 해당하는 뷰 객체를 찾아줍니다.
- 그리고 뷰는 모델 데이터를 사용해 최종적으로 사용자에게 보여줘야 할 화면을 생성합니다.
- 마지막으로 생성된 뷰는 클라이언트에 응답으로 전송됩니다.
저는 이러한 구조를 보고 의문을 가지게 되었습니다.
왜 굳이 나뉘었을까?
해당 구조를 보니,
Dispatcher Servlet
이 Controller
로 요청을 보낼 때, 두번에 거쳐서 요청을 보내게 됩니다.먼저
HandlerMapping
을 통해서 적절한 컨트롤러를 찾아내고 HandlerAdapter
를 통해 실제 메서드를 호출하게 되죠.이 때
HandlerMapping
에서 컨트롤러를 찾아서 바로 요청을 보내는게 더 간단하고 빠를 것 같다고 느꼈습니다.왜 굳이
HandlerAdapter
를 통해서 요청을 보내게 될까요?HandlerMapping과 HandlerAdapter를 알아보자
먼저
HandlerMapping
과 HandlerAdapter
는 모두 인터페이스 구조로 되어있습니다.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 MVC
의 Plug And Play
아키텍처를 가능하게 합니다.Plug & Play
Plug and Play
아키텍처는 컴포넌트나 모듈을 시스템에 추가할 때 추가적인 설정 없이 쉽게 통합하고 사용할 수 있도록 설계하는 아키텍처를 말합니다.따라서 소프트웨어가 독립적인 모듈로 구성되어 있어야하고, 컴포넌트를 쉽게 교체할 수 있어야 하고 이러한 작업이 일어났을 때 시스템의 재부팅이 필요 없이 자동으로 인식하고 필요한 구성을 처리할 수 있어야 합니다.
Spring
에서도 이미 저희는 플러그 앤 플레이 아키텍처를 경험했는데요,Spring
의 IOC 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
는 어노테이션 기반의 컨트롤러이고, MyHttpRequestHandler
는 HttpRequestHandler
의 구현체로서, 서블릿 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
의 구현 예시입니다.이
HandlerAdapter
는 MyController
타입의 컨트롤러를 지원합니다.
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
를 통해 실제 호출 및 결과를 반환 받는 구조가 더 객체지향적인 구조가 됩니다.좋은 인사이트를 나눠주신 박은종 디렉터님께 샤라웃을 날립니다. 🎉🎉🎉
참고하면 좋을 강의도 함께 남깁니다.