Spring MVC 구조: 어댑터 패턴과 다형성은 왜 중요한가?

2025. 5. 20. 23:57·SpringBoot

스프링 MVC 구조를 공부하다 보면 DispatcherServlet, HandlerAdapter, ViewResolver 같은 구성 요소들이 등장하며 어댑터 패턴과 다형성이라는 개념이 반복적으로 등장한다.

해당 내용을 찾아보면서 평소 아무 생각 없이 사용하던 것들이 왜 이렇게 만들어졌는지 조금은 이해할 수 잇었다.


스프링 MVC 주요 구조

구성요소 설명
DispatcherServlet 모든 HTTP 요청의 진입점
HandlerMapping 요청 URL에 따라 실행할 컨트롤러 매핑
HandlerAdapter 다양한 타입의 컨트롤러를 실행할 수 있도록 중계
ViewResolver 논리적인 뷰 이름을 실제 뷰로 변환하는 역할

문제 상황

컨트롤러가 버전마다 인터페이스가 다르다고 가정하자.

// V1 컨트롤러 인터페이스
public interface ControllerV1 {
    void process(HttpServletRequest request, HttpServletResponse response);
}

// V2 컨트롤러 인터페이스
public interface ControllerV2 {
    Map<String, Object> execute(HttpServletRequest request);
}

각기 다른 형태의 컨트롤러를 DispatcherServlet이 직접 처리하려면 아래처럼 if문이 반복되게 된다.

if (handler instanceof ControllerV1) {
    ((ControllerV1) handler).process(...);
} else if (handler instanceof ControllerV2) {
    ((ControllerV2) handler).execute(...);
}

문제점

  • 새로운 컨트롤러가 생길 때마다 DispatcherServlet을 수정해야 함 → OCP 위반
  • DispatcherServlet이 컨트롤러 내부 로직까지 알아야 함 → 높은 결합도
  • 유지보수와 테스트가 어려워짐

해결책: 어댑터 패턴 + 다형성

각 컨트롤러 버전마다 해당 타입을 처리할 수 있는 어댑터를 따로 구현하면
DispatcherServlet은 어댑터 인터페이스만 보고도 모든 컨트롤러를 처리할 수 있다.


HandlerAdapter 인터페이스

public interface HandlerAdapter {
    boolean supports(Object handler); // 어떤 타입의 컨트롤러를 지원하는지 확인
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler);
}

V1 어댑터 구현

public class ControllerV1Adapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof ControllerV1; // V1 컨트롤러만 지원
    }

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        ControllerV1 controller = (ControllerV1) handler;
        controller.process(request, response);
        return new ModelAndView("viewV1");
    }
}

V2 어댑터 구현

public class ControllerV2Adapter implements HandlerAdapter {
    @Override
    public boolean supports(Object handler) {
        return handler instanceof ControllerV2;
    }

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        ControllerV2 controller = (ControllerV2) handler;
        Map<String, Object> model = controller.execute(request);
        return new ModelAndView("viewV2", model); // 모델 포함
    }
}

ModelAndView 클래스

public class ModelAndView {
    private final String viewName;
    private final Map<String, Object> model;

    public ModelAndView(String viewName) {
        this(viewName, new HashMap<>());
    }

    public ModelAndView(String viewName, Map<String, Object> model) {
        this.viewName = viewName;
        this.model = model;
    }

    public String getViewName() {
        return viewName;
    }

    public Map<String, Object> getModel() {
        return model;
    }
}

DispatcherServlet 구현 예시

public class DispatcherServlet {

    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<HandlerAdapter> handlerAdapters = new ArrayList<>();

    public DispatcherServlet() {
        initHandlerMapping();   // URI → 컨트롤러 매핑
        initHandlerAdapters();  // 어댑터 등록
    }

    private void initHandlerMapping() {
        handlerMappingMap.put("/v1/members", new MemberControllerV1());
        handlerMappingMap.put("/v2/orders", new OrderControllerV2());
    }

    private void initHandlerAdapters() {
        handlerAdapters.add(new ControllerV1Adapter());
        handlerAdapters.add(new ControllerV2Adapter());
    }

    public void service(HttpServletRequest request, HttpServletResponse response) {
        try {
            Object handler = handlerMappingMap.get(request.getRequestURI());
            if (handler == null) {
                response.getWriter().write("404 Not Found");
                return;
            }

            HandlerAdapter adapter = getHandlerAdapter(handler);
            ModelAndView mv = adapter.handle(request, response, handler);

            response.getWriter().write("View name: " + mv.getViewName());
            System.out.println("Model: " + mv.getModel());

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private HandlerAdapter getHandlerAdapter(Object handler) {
        for (HandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) return adapter;
        }
        throw new IllegalArgumentException("지원 어댑터 없음: " + handler);
    }
}

다형성의 역할

다형성을 이용하면 DispatcherServlet은 컨트롤러가 어떤 구체 클래스인지 몰라도 된다.

ControllerV1 controller = new MemberControllerV1();
controller.process(request, response); // 이 한 줄로 어떤 V1 구현체든 실행 가능

이는 코드 확장성과 유연성 측면에서 매우 큰 장점이다.


결론

스프링 MVC는 다양한 컨트롤러 구조를 유연하게 처리하기 위해 어댑터 패턴과 다형성을 적극 활용하며, OCP를 준수하도록 설계되어 있다.

이 구조를 살펴보면서 Spring의 핵심 설계 철학을 꿰뚫을 수 있었고,
 "왜 이렇게 구조가 나뉘어 있는가?"에 대한 의문이 어느정도는 해소되었다.

'SpringBoot' 카테고리의 다른 글

Spring MVC: HTTP Body 처리와 응답 제어  (1) 2025.06.13
Spring MVC: 요청 매핑과 파라미터 처리  (0) 2025.06.13
웹 애플리케이션의 이해  (0) 2025.05.13
RequestScope로 요청별 로그 분리하기  (0) 2025.05.07
스프링 DI: 생존자 주입을 권장하는 이유  (0) 2025.05.05
'SpringBoot' 카테고리의 다른 글
  • Spring MVC: HTTP Body 처리와 응답 제어
  • Spring MVC: 요청 매핑과 파라미터 처리
  • 웹 애플리케이션의 이해
  • RequestScope로 요청별 로그 분리하기
moodone
moodone
  • moodone
    무던하게
    moodone
  • 전체
    오늘
    어제
    • 분류 전체보기 (36)
      • Java (7)
      • SpringBoot (24)
      • JavaScript (0)
      • Database (1)
      • Python (0)
      • Git (1)
      • IDE (0)
      • 기타 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    티스토리챌린지
    Repository
    git bash
    git
    오블완
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
moodone
Spring MVC 구조: 어댑터 패턴과 다형성은 왜 중요한가?
상단으로

티스토리툴바