BackEnd/Spring MVC

[Spring] MVC 프레임워크 만들기 - 2

샤아이인 2022. 2. 23.

내가 공부한것을 올리며, 중요한 단원은 저 자신도 곱씹어 볼겸 상세히 기록하고 얕은부분들은 가겹게 포스팅 하겠습니다.

 

4. 단순하고 실용적인 컨트롤러 - v4

지난 글에서 만들었던 v3 컨트롤러는 서블릿의 종속성을 제거하고, 뷰 경로의 중복을 제거하는 기능을 하여 잘 설계된 컨트롤러 이다.

하지만 실제 개발자가 컨트롤러 인터페이스를 구현할때마다 항상 ModelView객체를 생성하고 반환해야 하는 부분이 번거롭다.

 

좋은 프레임워크는 아키텍처도 중요하지만, 그와 더불어 실제 개발하는 개발자가 단순하고 편리하게 사용할 수 있어야 한다.

소위 실용성이 있어야 한다.

 

이번 V4는 좀더 개발자에게 실용적인 버전을 만들어 보자. 우선 그림을 살펴보자.

출처 - 인프런 김영한 스프링 강의

기본적인 구조는 v3와 거의 유사한데, 컨트롤러가 ModelView 대신 ViewName을 반환하고 있다.

 

● ControllerV4

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}
 

이번 버전은 인터페이스에 ModelView가 없다. model 객체는 파라미터로 전달되기 때문에 그냥 사용하면 되고, 결과로 뷰의 이름만 반환해주면 된다.

 

● MemberFormControllerV4

public class MemberFormControllerV4 implements ControllerV4 {

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        return "new-form";
    }
}
 

기존 3버전에서는 ModelView 객체를 반환했던거와는 달리, view의 이름만을 반환하면 된다.

개발자 입장에서 매우 편해졌다.

 

● MemberSaveControllerV4

public class MemberSaveControllerV4 implements ControllerV4 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        model.put("member", member);
        return "save-result";
    }
}
 

process의 인자로 model이 전달되기 때문에 model.put("member", member) 을 이용하여 데이터를 등록해주면 된다.

모델을 직접 생성하지 않아도 된다.

 

● MemberListControllerV4

public class MemberListControllerV4 implements ControllerV4 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        List<Member> members = memberRepository.findAll();
        model.put("members", members);
        return "members";
    }
}
 

● FrontControllerServletV4

 

@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {

    private Map<String, ControllerV4> controllerMap = new HashMap<>();

    public FrontControllerServletV4() {
        controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
        controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
        controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();

        ControllerV4 controller = controllerMap.get(requestURI);
        if(controller == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // paraMap
        Map<String, String> paraMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();

        String viewName = controller.process(paraMap, model);

        MyView view = viewResolver(viewName);
        view.render(model, request, response);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

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

FrontController는 이전과 거의 동일하다.

 

모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다. 다음 코드를 살펴보자.

Map<String, Object> model = new HashMap<>();
 

컨트롤러에서 모델 객체에 값을 담으면 여기에 담기게 되고, 이를 FrontController에서 접근할 수 있다.

 

또한 뷰의 논리 이름을 직접 반환한다.

String viewName = controller.process(paraMap, model); // 뷰 이름 반환
MyView view = viewResolver(viewName);
 

컨트롤러가 직접 뷰의 논리 이름을 반환하므로 이 값을 사용해서 실제 물리 뷰 경로를 찾아올 수 있다.

 

기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데, 컨트롤러를 구현하는 개발자 입장에서 보면 이제 군더더기 없는 코드를 작성할 수 있다.

 

5. 유연한 컨트롤러1 - v5

만약 어떤 개발자는 v3 컨트롤러를 사용하고 싶고, 다른 개발자는 v4 컨트롤러를 사용하고 싶다면 어떻게 해야할까?

 

어떤 컨트롤러를 호출하든 처리 가능하려면 중간에 어댑터가 필요하게 될 것 이다. 따라서 어댑터가 필요하다.

 

● 어댑터 패턴

지금까지 우리가 개발한 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.

ControllerV3 , ControllerV4 는 완전히 다른 인터페이스이다. 따라서 호환이 불가능하다.

마치 v3는 110v이고, v4는 220v 전기 콘센트 같은 것이다. 이럴 때 사용하는 것이 바로 어댑터이다.

어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자.

 

우선 v5의 구조부터 살펴보자.

출처 - 인프런 김영한 스프링 강의

● 핸들러 어댑터: 중간에 어댑터 역할을 하는 어댑터가 추가되었는데 이름이 핸들러 어댑터이다. 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.

 

● 핸들러: 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다. 그 이유는 이제 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있기 때문이다.

 

스프링 MVC의 구조와 거의 유사해졌다. 지금 시점에서 다음 글을 한번 살펴보길 권장한다.

 

[Spring] 스프링 MVC 프레임워크

공부하며 중요하다 싶은 내용들을 블로그에 공부겸 정리한 내용입니다! 지적 댓글 환영합니다~ 1. 스프링 MVC 핵심 구성 요소 " data-ke-type="html"> <>HTML 삽입 미리보기할 수 없는 소스 스프링 MVC의

blogshine.tistory.com

 

● MyHandlerAdapter

public interface MyHandlerAdapter {

    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
 

어뎁터들이 공통적으로 구현해야하는 interface이다.

v3를 위한 어뎁터를 만들때도, v4를 위한 어뎁터를 만들때도 위의 인터페이스를 구현해야 한다.

 

위 인터페이스에서는 2개의 메서드를 구현하도록 되어있다.

 

- supports()

인자로 넘어온 핸들러를 어댑터가 처리 가능한지 판단하는 메서드이다.

 

- handle()

반환 값을 보면 ModelView이다. 어댑터는 실제 컨트롤러 역할을 하는 핸들러를 호출하고, 그 결과로 ModelView 객체를 반환해야 한다.

만약 반환할 ModelView가 없다면 예외라도 발생시켜야 한다.

이전에는 프론트 컨트롤러가 직접 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 경유하여 컨트롤러가 호출된다.

 

이제 위 인터페이스를 실제로 구현한 어댑터를 만들어보자!

 

● ControllerV3HandlerAdapter

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);
        return controller.process(paramMap);
    }

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

우선 인자로 넘어온 handler가 처리할수 있는 타입인지를 확인해야 한다. 이를 위해 supports 함수를 구현해야 한다.

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

단순하게 instanceof를 통하여 확인한 bool값을 반환해주면 된다.

 

이후 처리 가능한 타입임을 알았으면 어댑터 에서 핸들러를 호출해줘야 한다.

public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
    ControllerV3 controller = (ControllerV3) handler;

    Map<String, String> paramMap = createParamMap(request);
    return controller.process(paramMap);
}
 

어뎁터의 handle 매서드가 인자로 handler를 받는다.

따라서 받은 handler를 우리가 원하는 ControllerV3로 형변환을 하고, 변환 완료된 컨트롤러의 process() 메서드를 호출하면 되는 것 이다.

이전에 supports() 메서드로 형변환 가능여부를 확인후 호출되기 때문에 casting을 해도 된다.

결과적으로 ModelView를 반환하게 된다.

 

● FrontControllerServletV5

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

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

    public FrontControllerServletV5() {
        initHandlerMappingMap();
        intiHandlerAdapters();
    }

    private void initHandlerMappingMap() {
        handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
        handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());
    }

    private void intiHandlerAdapters() {
        handlerAdapters.add(new ControllerV3HandlerAdapter());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        Object handler = getHandler(request);
        if(handler == null){
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyHandlerAdapter adapter = getHandlerAdapter(handler);

        ModelView mv = adapter.handle(request, response, handler);

        String viewName = mv.getViewName(); // 논리이름
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if(adapter.supports(handler)){
                return adapter;
            }
        }

        throw new IllegalArgumentException("Not found handler adapter for " + handler);
    }

    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
 

우선 생성자부터 살펴보자.

public FrontControllerServletV5() { 
    initHandlerMappingMap(); //핸들러 매핑 초기화 
    initHandlerAdapters(); //어댑터 초기화
}
 

생성자에서는 handlerMapping 과 handlerAdapter 를 초기화 한다.

 

● 매핑 정보

private final Map<String, Object> handlerMappingMap = new HashMap<>();
 

매핑 정보의 값이 ControllerV3 , ControllerV4 같은 인터페이스에서 아무 값이나 받을 수 있는 Object 로 변경되었다.

 

● 핸들러 매핑

private Object getHandler(HttpServletRequest request) {
    String requestURI = request.getRequestURI();
    return handlerMappingMap.get(requestURI);
}
 

getHandler() 메서드를 통하여 URL에 매핑된 핸들러 객체를 찾아서 반환한다.

 

● 핸들러를 처리가능한 어댑터 찾기

private MyHandlerAdapter getHandlerAdapter(Object handler) {
    for (MyHandlerAdapter adapter : handlerAdapters) {
        if(adapter.supports(handler)){
            return adapter;
        }
    }

    throw new IllegalArgumentException("Not found handler adapter for " + handler);
}
 

handler 를 처리할 수 있는 어댑터를 handlerAdapters 를 반복문을 통해 확인하면서 찾는다.

매 반복마다 adapter.supports(handler) 메서드를 호출하여 처리가능한 타입인지를 확인한다.

 

처리가 가능한 타입이라면, 즉 handler가 ControllerV3 인터페이스를 구현했다면, ControllerV3HandlerAdapter 객체가 반환된다.

 

● 어댑터 호출

위에서 원하는 handler를 찾았고, 어댑터 또한 찾았다면, 이제 어댑터를 통해 handler에 요청하면 된다.

ModelView mv = adapter.handle(request, response, handler);
 

어댑터는 handler(컨트롤러)를 호출하고 그 결과를 어댑터에 맞추어 반환한다.

 

6. 유연한 컨트롤러2 - v5

위 글에서 v3 추가에 이어서 v4 버전도 추가해야 원하는 걸 선택하여 사용할 수 있다.

 

우선 FrontController의 초기화 부분에 추가해줘야 할 부분이 있다.

 private void initHandlerMappingMap() {
    // .. v3 내용
 
    //V4 추가
      handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
      handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
      handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
  }

private void initHandlerAdapters() {
    handlerAdapters.add(new ControllerV3HandlerAdapter()); 
    handlerAdapters.add(new ControllerV4HandlerAdapter()); //V4 추가
}
 

handlerMappingMap에 v4를 사용하는 컨트롤러를 추가해야한다. 또한 해당 컨트롤러를 처리할 어댑터 또한 추가해야 한다.

 

이제 어뎁터를 추가해 보자

● ControllerV4HandlerAdapter

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 Map<String, String> createParamMap(HttpServletRequest request) {
        Map<String, String> paraMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paraName -> paraMap.put(paraName, request.getParameter(paraName)));
        return paraMap;
    }
}
 

위에서 봤던 v3와 마찬가지로 우선 supports()메서드로 처리가 가능한 타입인지 확인하는 부분을 구현한다.

 

실행 로직인 handle()은 다음과 같다.

ControllerV4 controller = (ControllerV4) handler;

Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();

String viewName = controller.process(paramMap, model);
 

handler를 ControllerV4로 형변환 한 후, controller에 파라미터를 담은 Map과 model 객체를 넘겨준다.

이를 통해 viewName을 받아올 수 있다.

 

하지만 문제가 하나있다!

 

어댑터가 호출하는 ControllerV4 컨트롤러는 뷰의 이름을 반환한다.

그런데 어댑터는 뷰의 이름이 아니라 ModelView 를 만들어서 반환해야 한다.

따라서 이를 변환해줄 어댑터가 필수적으로 필요하다.

ControllerV4 는 뷰의 이름을 반환했지만, 어댑터는 이것을 ModelView로 만들어서 형식을 맞추어 반환해줘야 한다.

String viewName = controller.process(paramMap, model);

ModelView mv = new ModelView(viewName);
mv.setModel(model);
 

우리의 FrontController는 전반적으로 인터페이스에 맞춰 구현을 했다.

변경에는 닫혀있고, 확장에는 열려있는 OCP의 원칙을 준수하고 있다.

 

스프링은 이와같이 다양한 확장성에 가능성을 열어둔 유용한 프레임 워크이다.

예를 들어 예전에는 스프링이 애노테이션 기반의 프로그래밍을 지원하지 않았지만, 애노테이션 기반 개발이 인기가 오르자

애노테이션 기반의 코드를 처리할수 있는 HandlerAdapter를 추가하게 되었다. FrontController는 인터페이스에 의존하고 있기 때문에 이러한 확장에 유연함을 보여주는 것 이다.

댓글