FireDrago

[Spring] Spring MVC 기본구조 본문

프로그래밍/Spring

[Spring] Spring MVC 기본구조

화이용 2024. 1. 26. 15:30

저번 포스팅에서 서블릿을 이용하여 MVC 패턴의 발전과정에 대하여 알아보았다.

컨트롤러는 ModelView 를 반환했다가, 나중에는 String 논리주소만 반환하도록 발전시켰다.

그런데 Spring MVC를 사용하면 ModelAndView를 반환하는 컨트롤러와 

논리주소만 반환하는 컨트롤러를 모두 지원한다. 즉 개발자가 원하는 반환타입을 컨트롤러에서 선택하여 사용할 수 있다.

어떻게 이게 가능한 것일까?

 

1. 어댑터 패턴 (Adapter Pattern)

위 질문에 대답하기 위해서는 우선 '어댑터 패턴'에 대하여 알아야한다.

어댑터 패턴은 디자인 패턴중 하나인데, 여러 형식의 인터페이스를 함께 사용하고 싶을때,

어댑터를 사용하여 여러 인터페이스를 공통의 형식으로 변환해준다. 

두개의 인터페이스가 서로다른 메서드나 인자를 가지고 있더라도, 어댑터를 사용하여 연결 해줄 수 있다.

110V 전기를 220V로 바꿔주는 것을 떠올리면 편하다 코드로 살펴보자

 

Adapter 클래스가 Target 인터페이스와 Adaptee 클래스를 연결한다.

 

Target 인터페이스는 클라이언트가 사용하는 인터페이스이다.
Adaptee 클래스는 기존에 존재하는 클래스이며, Target 인터페이스와 호환되지 않는다.
Adapter 클래스는 Target 인터페이스를 구현하고 있으며, Adaptee 클래스의 인스턴스를 가지고 있다.
Adapter 클래스는 doSomething() 메서드에서 Adaptee 클래스의 doSomethingElse() 메서드를 호출하여 

두 인터페이스를 연결한다. 이것을 '어댑터 패턴' 이라고 한다.

 

 

2. 어댑터 패턴 적용

어댑터 패턴의 개념을 살펴보았으니, 저번 포스팅에서 만들었던 두 가지 반환타입의 컨트롤러를 어댑터 패턴을 사용하여

FrontController 에서 둘 다 사용할 수 있도록 만들어보자

 

두가지 반환타입의 컨트롤러를 Adapter 패턴을 사용하여 함께 사용할 수 있도록 해보자

 

먼저 어댑터 클래스를 만들기 위해 어댑터 인터페이스를 만들자 여기서 Handler 는 Controller를 의미한다. 

Http 요청을 '처리' 하고 Http 응답을 만들어 내는 '처리자'의 의미인 Handler 이름을 사용한다. 

 

 

다음으로, MyHandlerAdapter를 구현하는 각각의 Adapter 구현체를 생성해야 한다. 

인터페이스를 구현하는 이유는, 클래스 간 결합도를 낮추기 위함이다.

MyHandlerAdapter 인터페이스를 구현하는 Adapter 구현체들은 모두 MyHandlerAdapter 타입으로 사용할 수 있다.

따라서 FrontController는 MyHandlerAdapter 타입을 사용하여 두 구현체를 모두 사용할 수 있게 된다.

 

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {

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

    @Override
    public ModelView handle(HttpServletRequest request,
                            HttpServletResponse response,
                            Object handler) throws ServletException, IOException {
        //형변환
        ControllerV3 controller = (ControllerV3)handler;

        //실행
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);
        return mv;
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
	
    @Override
    public boolean supports(Object handler) {
    	// 파라미터의 핸들러가 어댑터가 지원하는 핸들러인지 확인한다.
        return handler instanceof ControllerV4;
    }

    @Override
    public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
        //형변환
        ControllerV4 controller = (ControllerV4)handler;

        //실행로직
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();
        String viewName = controller.process(paramMap, model);

        //어댑터 변환
        ModelView mv = new ModelView(viewName);
        mv.setModel(model);

        return mv;
    }

    private static Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
        return paramMap;
    }
}

 

위 코드를 살펴보면, 각각의 어댑터는 항상 ModelView 를 반환하도록 변환해준다.

특히 ControllerV4HandlerAdapter를 주의깊게 보자. ControllerV4HandlerAdapter가 호출하는 
컨트롤러는 String 을 반환하지만 Adapter가 ModelView로 반환하도록 변환해준다. 

이렇게 되면 FrontController는 Adapter를 사용하여 컨트롤러를 호출할때 항상 ModelView를 받을 수 있게된다.

 

 

프론트 컨트롤러의 생성자부터 살펴보자 먼저 Map<String, Object> handlerMappingMap 에 url별로 호출되는 컨트롤러를

생성한다. 이때 ModelView를 반환하는 V3 컨트롤러와 String을 반환하는 V4 컨트롤러가 함께 들어가는 것에 주목하자

 

handlerMappingMap을 초기화 하면, initHandlerAdapters() 메서드를 호출하여

List<MyHandlerAdapter>handlerAdapters 에 V3용 어댑터와 V4용 어댑터를 초기화한다.

여기서 <MyHandlerAdapter> 를 사용할 수 있는 이유도 인터페이스를 이용한 다형성 덕분이다.

 

 

이제 프론트 컨트롤러의 메서드이다. 

우선 request 객체를 통해 URI 정보를 가져온다. URI 정보를 통해 handlerMappingMap에서 handler를 찾아온다.

찾아온 handler는 handlerAdapter를 순차적으로 돌면서 support 메서드를 통해 지원하는 Adapter를 찾는다.

찾아온 Adapter를 통해 handler를 실행하고 항상 ModelView 객체를 받아온다.

이렇게 어댑터 패턴을 사용하여 V3 컨트롤러와 V4 컨트롤러를 함께 사용할 수 있게 되었다. 

만약 추가적인 어댑터를 만들고 이를 추가해준다면, 더 많은 컨트롤러를 지원할 수 있을 것이다. 

위 코드를 그림으로 살펴보자

 

 

3. SpringMVC 구조

우리가 구현한 구조를 그림으로 표현하면 이렇게 된다. 그렇다면 Spring MVC는 어떨까?

 

 

우리가 직접 구현한 구조와 완전히 같은 구조임을 알 수 있다. Spring MVC 역시 Adapter 패턴을 사용하고 있는것이다.

FrontController ---->         DispatcherServlet
handlerMappingMap ----> HandlerMapping (인터페이스)
MyHandlerAdapter ---->    HandlerAdapter  (인터페이스)
ModelView ---->     ModelAndView
viewResolver ----> ViewResolver (인터페이스)
MyView ---->          View (인터페이스)

 

다만 한가지 다른 점이 있다면

Spring MVC는 HandlerMapping, HandlerAdapter, ViewResolver, View를 인터페이스로 정의한다.

더 많은 형식과 기능을 지원하는 여러 맵핑과 어댑터, 뷰 형식을 지원할 수 있게된다.

 

 

이 포스팅은 김영한님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술을 수강하고 작성되었습니다

'프로그래밍 > Spring' 카테고리의 다른 글

[Spring] HttpMessageConverter  (0) 2024.02.01
[Spring] Spring 기본기능  (0) 2024.01.30
[Spring] 빈 생명주기 콜백  (0) 2024.01.11
[Spring] 의존성 자동주입  (0) 2024.01.09
[Spring] 스프링 컨테이너와 싱글턴  (0) 2024.01.06