스프링 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 |