본문 바로가기
BackEnd/Project

[MVC] Ch06. MVC 프레임워크(4)

by 개발 Blog 2024. 8. 1.

공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.

 

스프링 웹 MVC 프레임워크 - 2

지금까지 DispatcherServlet, HandlerMapping, ViewResolver, View를 구현했다. 

이제 핸들러와 이를 처리하는 방법을 구체화하기 위해 HandlerAdapter를 도입한다.

HandlerAdapter는 다양한 핸들러 타입을 지원할 수 있게 해 주며, 이번에 SimpleControllerHandlerAdapter와 ModelAndView 클래스를 구현하여 기본적인 컨트롤러 처리 방식을 정의한다.

 

HandlerAdapter 인터페이스

다양한 타입의 핸들러를 처리할 수 있도록 supports와 handle 메서드를 정의한다.

package org.example.mvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface HandlerAdapter {
    boolean supports(Object handler);

    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
}

- supports 메서드: 핸들러 타입을 확인하여 적합한 HandlerAdapter인지 여부를 반환한다.

- handle 메서드: 요청을 처리하고 ModelAndView 객체를 반환한다.

 

 

SimpleControllerHandlerAdapter 클래스

이 클래스는 Controller 인터페이스를 구현한 핸들러를 처리하는 HandlerAdapter이다.

package org.example.mvc;

import org.example.mvc.controller.Controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class SimpleControllerHandlerAdapter implements HandlerAdapter{

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

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String viewName = ((Controller) handler).handleRequest(request, response);
        return new ModelAndView(viewName);
    }
}

- supports 메서드: 주어진 핸들러가 Controller 인터페이스를 구현하는지 확인한다.

- handle 메서드: Controller 핸들러의 handleRequest 메서드를 호출하여 요청을 처리하고, 결과로 ModelAndView 객체를 반환한다.

 

ModelAndView 클래스

뷰 이름과 모델 데이터를 함께 담는 객체이다.

package org.example.mvc;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

public class ModelAndView {
    private Object view;
    private Map<String, Object> model = new HashMap<>();

    public ModelAndView(String viewName) {
        view = viewName;
    }

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

    public String getViewName() {
        return (this.view instanceof String ? (String) this.view : null);
    }
}

- viewName: 반환할 뷰의 이름을 저장한다.

- model: 뷰에 전달할 데이터를 저장한다.

 

DispatcherServlet에 HandlerAdapter 추가

@WebServlet("/")
public class DispatcherServlet extends HttpServlet {

 	...
    
    private List<HandlerAdapter> handlerAdapters;

    @Override
    public void init() throws ServletException {
        rmhm = new RequestMappingHandlerMapping();
        rmhm.init();
        handlerAdapters = List.of(new SimpleControllerHandlerAdapter());
        viewResolvers = Collections.singletonList(new JspViewResolver());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        log.info("[DispatcherServlet] service started");

        try {
            Controller handler = rmhm.findHandler(new HandlerKey(RequestMethod.valueOf(request.getMethod()), request.getRequestURI()));

            HandlerAdapter handlerAdapter = handlerAdapters.stream()
                    .filter(ha -> ha.supports(handler))
                    .findFirst()
                    .orElseThrow(() -> new ServletException("No adapter for handler [" + handler + "]"));

            ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);

            for (ViewResolver viewResolver : viewResolvers) {
                View view = viewResolver.resolveView(modelAndView.getViewName());
                view.render(modelAndView.getModel(), request, response);
            }

        } catch (Exception e) {
            log.error("exception occurred: [{}]", e.getMessage(), e);
            throw new ServletException(e);
        }
    }
}

handlerAdapters 리스트

  • 다양한 타입의 핸들러를 지원하기 위해 HandlerAdapter의 리스트를 초기화한다. 여기서는 SimpleControllerHandlerAdapter를 사용하여 Controller 인터페이스를 구현한 핸들러를 처리한다.

핸들러 선택 및 처리

  • RequestMappingHandlerMapping에서 핸들러를 찾고, 해당 핸들러를 지원하는 HandlerAdapter를 선택하여 요청을 처리한다. 처리 결과는 ModelAndView 객체로 반환된다.

뷰 렌더링

  • viewResolvers를 통해 뷰 이름을 해석하고, 해당 뷰를 렌더링하여 클라이언트에게 응답한다.
 

이전 단계에서 DispatcherServlet은 RequestMappingHandlerMapping 클래스와 직접적으로 결합되어 있었다.

이를 개선하기 위해, HandlerMapping 인터페이스를 도입하여 결합도를 낮출 것이다.

 

HandlerMapping 인터페이스

package org.example.mvc;

public interface HandlerMapping {
    Object findHandler(HandlerKey handlerKey);
}

- 요청을 처리할 핸들러를 찾는 메서드를 정의한다.

- 이 인터페이스를 통해 다양한 핸들러 매핑 전략을 유연하게 교체할 수 있다.

 

RequestMappingHandlerMapping 클래스

package org.example.mvc;

import org.example.mvc.controller.*;

import java.util.HashMap;
import java.util.Map;

public class RequestMappingHandlerMapping implements HandlerMapping{
    private Map<HandlerKey, Controller> mappings = new HashMap<>();

    void init() {
        mappings.put(new HandlerKey(RequestMethod.GET, "/"), new HomeController());
        mappings.put(new HandlerKey(RequestMethod.GET, "/users"), new UserListController());
        mappings.put(new HandlerKey(RequestMethod.POST, "/users"), new UserCreateController());
        mappings.put(new HandlerKey(RequestMethod.GET, "/user/form"), new ForwardController("/user/form"));
    }

    public Controller findHandler(HandlerKey handlerKey) {
        return mappings.get(handlerKey);
    }
}

- HandlerMapping 인터페이스를 구현하며, 요청 URL과 HTTP 메서드에 따라 적절한 핸들러를 찾아 반환한다.

 

DispatcherServlet 클래스

@WebServlet("/")
public class DispatcherServlet extends HttpServlet {

    private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);
	
    //변경된 부분
    private HandlerMapping hm;

    private List<HandlerAdapter> handlerAdapters;

    private List<ViewResolver> viewResolvers;

    @Override
    public void init() throws ServletException {
        RequestMappingHandlerMapping rmhm = new RequestMappingHandlerMapping();
        rmhm.init();
        
		//변경된 부분
        hm = rmhm;

        handlerAdapters = List.of(new SimpleControllerHandlerAdapter());
        viewResolvers = Collections.singletonList(new JspViewResolver());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        log.info("[DispatcherServlet] service started");

        try {
        	//변경된 부분
            Object handler = hm.findHandler(new HandlerKey(RequestMethod.valueOf(request.getMethod()), request.getRequestURI()));

            HandlerAdapter handlerAdapter = handlerAdapters.stream()
                    .filter(ha -> ha.supports(handler))
                    .findFirst()
                    .orElseThrow(() -> new ServletException("No adapter for handler [" + handler + "]"));

            ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);

            for (ViewResolver viewResolver : viewResolvers) {
                View view = viewResolver.resolveView(modelAndView.getViewName());
                view.render(modelAndView.getModel(), request, response);
            }

        } catch (Exception e) {
            log.error("exception occurred: [{}]", e.getMessage(), e);
            throw new ServletException(e);
        }
    }
}

- DispatcherServlet은 HandlerMapping 인터페이스를 통해 핸들러를 찾는다.

- 이는 DispatcherServlet이 특정 HandlerMapping 구현에 의존하지 않도록 하여, 향후 다른 매핑 전략으로 쉽게 변경할 수 있도록 한다.

 

DispatcherServlet과 RequestMappingHandlerMapping 간의 결합도를 낮추기 위해 HandlerMapping 인터페이스를 도입하였다. 이제, 더 나아가 스프링과 유사하게 애노테이션을 활용한 핸들러 매핑을 구현해보고자 한다. 이 방법은 컨트롤러가 반드시 인터페이스를 구현할 필요 없이 애노테이션을 사용하여 핸들러로 등록될 수 있게 한다.

 

가정해 보자. 어떤 담당자가 컨트롤러 인터페이스를 구현해야지만 핸들러에서 찾을 수 있었던 기존 방식이 번거로우니, 애노테이션 형태로 구현하여 더 간편하게 핸들러를 찾을 수 있게 해 달라는 요구를 하였다. 이러한 요구를 충족하기 위해 애노테이션 기반의 핸들러 매핑을 구현할 필요가 있다.

 

1. 애노테이션 정의

먼저, 컨트롤러 클래스와 메서드를 표시할 수 있는 애노테이션을 정의한다. 이는 클래스와 메서드에 애노테이션을 붙여 핸들러로 등록할 수 있도록 한다.

package org.example.mvc.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Controller {
}

- @Controller는 해당 클래스가 웹 요청을 처리하는 컨트롤러임을 나타내며, 이 클래스가 애플리케이션의 진입점이 될 수 있음을 명시한다.

package org.example.mvc.annotation;

import org.example.mvc.controller.RequestMethod;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestMapping {
    String value() default "";

    RequestMethod[] method() default {};
}

- @RequestMapping은 클래스나 메서드에 적용되어, 특정 URL 패턴과 HTTP 메서드에 대한 매핑 정보를 제공한다.

 

2. AnnotationHandler 및 AnnotationHandlerAdapter

애노테이션 기반 핸들러를 처리하기 위한 AnnotationHandler와 AnnotationHandlerAdapter를 구현한다.

AnnotationHandler는 컨트롤러 클래스와 메서드를 관리하고, AnnotationHandlerAdapter는 이를 처리하는 어댑터 역할을 한다.

package org.example.mvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class AnnotationHandler {
    private final Class<?> clazz;
    private final Method targetMethod;

    public AnnotationHandler(Class<?> clazz, Method targetMethod) {
        this.clazz = clazz;
        this.targetMethod = targetMethod;
    }

    public String handle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        Constructor<?> declaredConstructor = clazz.getDeclaredConstructor();
        Object handler = declaredConstructor.newInstance();

        return (String) targetMethod.invoke(handler, request, response);
    }
}

- 리플렉션을 사용하여 애노테이션이 부여된 컨트롤러 클래스와 메서드를 관리하며, 해당 메서드를 실행한다.

  • 클래스 정보 저장: clazz 필드는 핸들러의 클래스 타입을 저장한다.
  • 메서드 정보 저장: targetMethod 필드는 실행할 메서드의 정보를 저장한다.
  • 메서드 실행: handle 메서드는 지정된 클래스의 인스턴스를 생성하고, targetMethod를 실행하여 결과를 반환한다.
package org.example.mvc;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

    @Override
    public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String viewName = ((AnnotationHandler) handler).handle(request, response);
        return new ModelAndView(viewName);
    }
}

- AnnotationHandler를 지원하는 어댑터로, 해당 핸들러의 handle 메서드를 호출하여 요청을 처리한다.

  • 핸들러 지원 여부: supports 메서드는 주어진 핸들러가 AnnotationHandler인지 확인한다.
  • 핸들링 처리: handle 메서드는 AnnotationHandler의 handle 메서드를 호출하여 요청을 처리하고, 결과를 ModelAndView 형태로 반환한다.

3. AnnotationHandlerMapping

AnnotationHandlerMapping은 애노테이션을 기반으로 핸들러를 매핑하는 역할을 한다.

Reflections 라이브러리를 사용하여 클래스와 메서드에 붙은 애노테이션을 읽어, 이를 기반으로 핸들러를 매핑한다.

package org.example.mvc;

import org.example.mvc.annotation.Controller;
import org.example.mvc.annotation.RequestMapping;
import org.example.mvc.controller.RequestMethod;
import org.reflections.Reflections;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class AnnotationHandlerMapping implements HandlerMapping {
    private final Object[] basePackage;

    private Map<HandlerKey, AnnotationHandler> handlers = new HashMap<>();

    public AnnotationHandlerMapping(Object... basePackage) {
        this.basePackage = basePackage;
    }

    public void initialize(){
        Reflections reflections = new Reflections(basePackage);

        Set<Class<?>> clazzesWithControllerAnnotation = reflections.getTypesAnnotatedWith(Controller.class);

        clazzesWithControllerAnnotation.forEach(clazz ->
                Arrays.stream(clazz.getDeclaredMethods()).forEach(declaredMethod -> {
                    RequestMapping requestMapping = declaredMethod.getDeclaredAnnotation(RequestMapping.class);

                    Arrays.stream(getRequestMathods(requestMapping))
                            .forEach(requestMethod -> handlers.put(
                                    new HandlerKey(requestMethod, requestMapping.value()), new AnnotationHandler(clazz, declaredMethod)));
                })
        );
    }

    private RequestMethod[] getRequestMathods(RequestMapping requestMapping) {
        return requestMapping.method();
    }

    @Override
    public Object findHandler(HandlerKey handlerKey) {
        return handlers.get(handlerKey);
    }
}
  • 클래스 스캔: Reflections 라이브러리를 사용하여 지정된 패키지에서 @Controller 애노테이션이 붙은 모든 클래스를 스캔한다.
  • 핸들러 매핑 구성: 스캔된 클래스에서 @RequestMapping 애노테이션이 붙은 메서드를 찾아, HandlerKey와 AnnotationHandler를 매핑한다.
  • Reflections 사용: Reflections 객체를 통해 지정된 basePackage 내에서 @Controller 애노테이션이 붙은 모든 클래스를 찾는다.
  • 클래스 순회: 각 @Controller 애노테이션이 붙은 클래스를 순회하며, 클래스 내의 메서드를 검사한다.
  • 메서드 분석: 각 메서드에 대해 @RequestMapping 애노테이션이 적용된 경우, 해당 메서드의 요청 URL과 HTTP 메서드 정보를 읽어온다.
  • 핸들러 매핑 등록: HandlerKey와 AnnotationHandler 객체를 생성하여 handlers 맵에 저장한다. 여기서 HandlerKey는 요청 URL과 HTTP 메서드를 기준으로 매핑을 관리한다.

4. RequestMappingHandlerMapping 수정

기존의 RequestMappingHandlerMapping은 유지하면서, 애노테이션을 사용하는 경우와 아닌 경우를 나눌 수 있도록 수정하거나, 필요에 따라 분리할 수 있다.

package org.example.mvc;

import org.example.mvc.controller.*;

import java.util.HashMap;
import java.util.Map;

public class RequestMappingHandlerMapping implements HandlerMapping{
    private Map<HandlerKey, Controller> mappings = new HashMap<>();

    void init() {
//        mappings.put(new HandlerKey(RequestMethod.GET, "/"), new HomeController());
        mappings.put(new HandlerKey(RequestMethod.GET, "/users"), new UserListController());
        mappings.put(new HandlerKey(RequestMethod.POST, "/users"), new UserCreateController());
        mappings.put(new HandlerKey(RequestMethod.GET, "/user/form"), new ForwardController("/user/form"));
    }

    public Controller findHandler(HandlerKey handlerKey) {
        return mappings.get(handlerKey);
    }
}

 

5. DispatcherServlet 수정

기존의 DispatcherServlet은 핸들러 매핑과 어댑터를 각각 하나만 지원했지만, 이제는 리스트로 관리하여 다양한 매핑 방식과 어댑터를 지원하도록 수정되었다.

package org.example.mvc;

import org.example.mvc.controller.RequestMethod;
import org.example.mvc.view.JspViewResolver;
import org.example.mvc.view.View;
import org.example.mvc.view.ViewResolver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
import java.util.List;

@WebServlet("/")
public class DispatcherServlet extends HttpServlet {

    private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

    private List<HandlerMapping> handlerMappings;

    private List<HandlerAdapter> handlerAdapters;

    private List<ViewResolver> viewResolvers;

    @Override
    public void init() throws ServletException {
        RequestMappingHandlerMapping rmhm = new RequestMappingHandlerMapping();
        rmhm.init();

        AnnotationHandlerMapping ahm = new AnnotationHandlerMapping("org.example");
        ahm.initialize();

        handlerMappings = List.of(rmhm, ahm);
        handlerAdapters = List.of(new SimpleControllerHandlerAdapter(), new AnnotationHandlerAdapter());
        viewResolvers = Collections.singletonList(new JspViewResolver());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        log.info("[DispatcherServlet] service started");
        String requestURI = request.getRequestURI();
        RequestMethod requestMethod = RequestMethod.valueOf(request.getMethod());


        try {
        	// 1. 적절한 핸들러 찾기
            Object handler = handlerMappings.stream()
                    .filter(hm -> hm.findHandler(new HandlerKey(requestMethod, requestURI)) != null)
                    .map(hm -> hm.findHandler(new HandlerKey(requestMethod, requestURI)))
                    .findFirst()
                    .orElseThrow(() -> new ServletException("No handler for [" + requestMethod + ", " + requestURI + "]"));
                    
        	// 2. 핸들러 어댑터 찾기
            HandlerAdapter handlerAdapter = handlerAdapters.stream()
                    .filter(ha -> ha.supports(handler))
                    .findFirst()
                    .orElseThrow(() -> new ServletException("No adapter for handler [" + handler + "]"));

        	// 3. 핸들러 실행 및 결과 처리
            ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
			
        	// 4. ViewResolver를 통해 View 선택 및 렌더링
            for (ViewResolver viewResolver : viewResolvers) {
                View view = viewResolver.resolveView(modelAndView.getViewName());
                view.render(modelAndView.getModel(), request, response);
            }

        } catch (Exception e) {
            log.error("exception occurred: [{}]", e.getMessage(), e);
            throw new ServletException(e);
        }
    }
}

- 핸들러 매핑 초기화

  • RequestMappingHandlerMapping과 AnnotationHandlerMapping을 리스트로 관리하여, 인터페이스 기반과 애노테이션 기반 핸들러 매핑을 모두 지원한다.

- 핸들러 어댑터 초기화

  •  SimpleControllerHandlerAdapter와 AnnotationHandlerAdapter를 사용하여 다양한 핸들러 타입을 처리할 수 있다.

1. 적절한 핸들러 찾기

  • handlerMappings 리스트에서 findHandler 메서드를 호출하여 요청된 URI와 HTTP 메서드에 맞는 핸들러를 찾는다.
  • 핸들러가 존재하지 않으면 ServletException이 발생한다.
  • 이 부분에서 handlerMappings은 RequestMappingHandlerMapping과 AnnotationHandlerMapping을 포함한다.

2. 핸들러 어댑터 찾기

  • 핸들러를 처리할 수 있는 적절한 HandlerAdapter를 handlerAdapters 리스트에서 찾는다.
  • supports 메서드는 주어진 핸들러를 해당 어댑터가 지원하는지 검사한다.
  • 적절한 어댑터가 없으면 ServletException이 발생한다.

3. 핸들러 실행 및 결과 처리

  • 찾은 HandlerAdapter를 사용해 핸들러를 실행한다.
  • 실행 결과는 ModelAndView 객체로 반환된다. 이 객체는 뷰 이름과 뷰에 전달할 모델 데이터를 포함한다.

4. ViewResolver를 통해 View 선택 및 렌더링

  • viewResolvers 리스트에서 ViewResolver를 사용해 뷰 이름에 해당하는 View 객체를 찾는다.
  • View 객체의 render 메서드를 호출하여 모델 데이터를 포함한 응답을 클라이언트에게 보낸다.

실행해보면 정상적으로 애노테이션 기반으로 수정된 것을 확인할 수 있다.

 

이번에는 기존의 인터페이스 기반 MVC 프레임워크에서 애노테이션 기반으로 전환하는 과정을 다루었다.

 

1. 기존 구조 유지 및 애노테이션 도입

  • 기존 인터페이스 기반의 MVC 프레임워크에 애노테이션 기반 매핑을 도입하였다.
  • 이를 위해 AnnotationHandlerAdapter와 AnnotationHandlerMapping을 추가하여 애노테이션을 통해 핸들러를 매핑할 수 있도록 했다.

2. 핸들러 매핑 및 어댑터 역할

  • HandlerMapping은 URL에 맞는 핸들러를 찾고, HandlerAdapter는 해당 핸들러를 실행하여 ModelAndView 객체를 반환하는 역할을 한다.
  • 이 과정에서 AnnotationHandlerAdapter는 애노테이션 기반 핸들러를 처리한다.

3. 뷰 선택 및 렌더링

  • ViewResolver는 ModelAndView 객체에서 뷰 이름을 기반으로 실제 뷰를 선택하고, 이를 렌더링 하여 클라이언트에게 응답을 생성한다.

4. OCP 적용

  • 코드의 변경을 최소화하고, 애노테이션 기반의 설정을 추가하여 기존 시스템을 확장 가능하게 했다.
  • 이를 통해 시스템의 유지 보수성과 확장성을 강화했다.

'BackEnd > Project' 카테고리의 다른 글

[Board] Ch01. 프로젝트 기획  (0) 2024.08.04
[MVC] Ch07. DI 프레임워크  (0) 2024.08.02
[MVC] Ch06. MVC 프레임워크(3)  (0) 2024.08.01
[MVC] Ch06. MVC 프레임워크(2)  (0) 2024.07.31
[MVC] Ch06. MVC 프레임워크(1)  (0) 2024.07.30