Spring MVC에서 Handler Adapter는 왜 등장했을까?

Libienz·2025년 1월 20일

개인적으로는 Adapter 패턴을 썩 좋아하진 않습니다. 어댑터 없이 두 시스템이 상호작용할 수 없는 것이 객체지향에서 굉장히 어색한 부분이라고 생각해요. a명세와 b명세가 달라서 adapter를 통해 일관적으로 다루어야 한다면 그 설계부터 잘못되었다는 것이 제 생각입니다. 어쩔 수 없는 상황에서 그 운용 의도를 이해하지 못하는 것은 아닙니다만 Adapter는 초반에 다형성 설계가 잘 되었다면 굳이 등장하지 않아도 되는 패턴이라고 생각해요.

그렇기에 Spring MVC 흐름을 살펴보면서 어색하다고 느꼈던 부분이 있습니다. 바로 DispatcherServlet이 Handler를 처리할 수 있는 Adapter를 조회한 후 핸들러를 처리하도록 요청하는 부분입니다. Handler를 하나의 타입으로 선언 후 어댑터를 찾을 필요 없이 다형성을 이용하여 핸들러를 실행하면 안되는 것일까요? 이와 같은 학습에서 궁금증이 남았었지만 다양한 형태의 핸들러가 존재할 수 있고, 이를 처리하기 위해 Adapter 패턴이 등장한 것으로 학습을 마무리 했었습니다.

그리고 시간이 지나 우아한테크코스 레벨 4에서 Spring MVC를 직접 코드로 구현해보는 미션을 진행해볼 수 있었습니다. 직접 구현해보니 HandlerAdapter 개념이 왜 필요한지 알 수 있더군요. 이번 포스팅에서 이 부분에 대해 학습한 내용을 정리해보려고 합니다.


핸들러 어댑터 없이 설계된 DispatcherServlet

package com.techcourse;

import com.interface21.webmvc.servlet.view.JspView;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.HashMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DispatcherServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;
    private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

    private ManualHandlerMapping manualHandlerMapping;

    public DispatcherServlet() {
    }

    @Override
    public void init() {
        manualHandlerMapping = new ManualHandlerMapping();
        manualHandlerMapping.initialize();
    }

    @Override
    protected void service(final HttpServletRequest request, final HttpServletResponse response)
            throws ServletException {
        final String requestURI = request.getRequestURI();
        log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI);

        try {
            final var controller = manualHandlerMapping.getHandler(requestURI);
            final var viewName = controller.execute(request, response);
            final var jspView = new JspView(viewName);

            jspView.render(new HashMap<>(), request, response);
        } catch (Throwable e) {
            log.error("Exception : {}", e.getMessage(), e);
            throw new ServletException(e.getMessage());
        }
    }
}

위의 코드는 어댑터를 사용하지 않고 핸들러를 다루는 DispatcherServlet의 코드입니다. 초반에 작성할 때에는 위의 방식이 훨씬 자연스럽다고 생각했어요. adapter없이 추상화된 타입으로써 handler를 다루니 위의 코드에서는 핸들러를 일관되게 다룰 수 있게되었고 어댑터를 찾는 과정 역시 필요없게 되었습니다.


어댑터 없이도 잘 동작하는데 왜 어댑터가 필요할까?

위의 버젼으로 작성된 제가 만든 스프링 MVC도 잘 동작합니다. 만약 다루어야 하는 핸들러 매핑이 많아지면 Handler 타입을 반환하는 핸들러 매핑을 기존 코드를 수정하지 않고 추가할 수도 있죠. 설계도 깔끔한 듯 보입니다. 어댑터 없이도 잘 동작하는데 어댑터는 왜 필요할까요? 리뷰어와 이야기 나누어보면서 답을 찾을 수 있었습니다. (코멘트 링크)

프레임 워크는 제어권을 가져갑니다. 이를 통해 개발자는 프레임 워크가 알아서 해주는 부분 보다 비즈니스 핵심 로직에 집중할 수 있게 되었죠. 하지만 프레임워크는 알아서 많은 일을 해주는 만큼 개발자에게 강제하는 것이 많습니다. "@RestController를 Rest Controller 클래스에 붙이세요"처럼요.

Spring MVC가 처음 등장했을 때는 다들 이전 버전의 핸들러를 사용하고 있었을 겁니다. 이러한 상황에서 만약 스프링진영이 Handler 타입의 표준을 새로 지정하고 이를 사용하도록 강제했다면 기존에 다른 핸들러를 운용하며 상당 부분을 구축했던 개발자들은 스프링 MVC를 사용하기 위해 전체적으로 코드를 뜯어내야합니다. 이미 상당 부분을 구축해둔 개발자에게 모든 부분을 고치라고 하면 개발자들은 쉽게 회유되지 않았을거에요.

학습을 같이한 크루와 저는 스프링 MVC 진영에서 레거시와의 호환을 위해 어댑터 패턴을 사용했다고 추측하게 되었습니다. 객체지향을 깨트리는 단점을 무릅쓰더라도 개발자의 회유를 중요한 가치로 둔 것이죠.


핸들러 어댑터를 참조하는 DispatcherServlet

    @Override
    protected void service(final HttpServletRequest request, final HttpServletResponse response)
            throws ServletException {
        log.debug("Method : {}, Request URI : {}", request.getMethod(), request.getRequestURI());
        try {
            Object handler = handlerMappingRegistry.getHandler(request);
            HandlerAdapter handlerAdapter = handlerAdapterRegistry.getHandlerAdapter(handler);
            ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
            if (modelAndView != null) {
                View view = modelAndView.getView();
                view.render(modelAndView.getModel(), request, response);
            }
        } catch (Exception e) {
            throw new ServletException("요청을 처리하는 것에 실패했습니다", e);
        }
    }
package com.interface21.webmvc.servlet;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

public interface HandlerAdapter {

    boolean supports(Object handler);

    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}
public class RequestMappingHandlerAdapter implements HandlerAdapter {

    @Override
    public boolean supports(Object handler) {
        return handler instanceof HandlerExecution;
    }

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception {
        HandlerExecution handlerExecution = (HandlerExecution) handler;
        return handlerExecution.handle(request, response);
    }
}

핸들러 어댑터를 사용하는 DispatcherServlet의 service 메서드는 위처럼 수정되었습니다. 덕분에 타입과 그 명세가 다른 핸들러들까지 adapter를 통해 일관적으로 다루게 될 수 있었죠. 이와 같이 작성된 Spring MVC는 이제 설득력이 높았을 겁니다. Spring MVC 프레임워크가 성공한 비결도 이러한 설득력에 있지 않았나 생각이 드는 지점입니다.

바퀴를 재발명하지 마라


프로그래밍 격언중에 바퀴를 재발명하지 말라는 격언이 있습니다. 기존에 제공되는 기능을 사용하는 것이 충분한 해결책인 경우가 많기에 나오는 격언이죠. 하지만 위의 사진처럼 새로 개발된 바퀴가 더 효율이 좋을 수도 있습니다. 개발자는 단순히 "있는 것을 쓰면 된다"는 사고방식에 갇히기보다, 왜 기존 기능이 적합한지 혹은 그렇지 않은지를 평가할 수 있는 비판적 사고를 가져야합니다.

저는 추상보다 상세에 집착하기 위해 노력하고 있습니다. 기술의 사용법 뿐인 추상적 지식을 넘어 기술의 상세를 이해하기 위해 노력하죠. 레벨 4에서 프레임워크와 라이브러리를 직접 구현하면서 기술의 상세를 배우며 새로운 관점에 눈뜨고 있음을 느낍니다.

앞으로의 공부에서도 추상보다는 상세에 집착할 것을 다짐하며 마무리합니다~

profile
추상보다 상세에 집착하는 개발자 리비(리비엔즈)입니다 🤗

0개의 댓글