Spring spring-mvc 구현해보기

강정우·2023년 12월 1일
0

Spring-boot

목록 보기
27/73
post-thumbnail

front-controller

  • 사실 스프링 mvc의 핵심이라고 볼 수 있다.
    이 front-controller가 바로 "공통의 관심사"를 모아서 처리해주기 때문이다.

  • 그래서 이 front-controller가 바로 spring mvc의 dispatcherServlet이다.

  • 그럼 공통처리도 가능해지고 굉장히 많던 서블릿도 하나로 줄일 수 있다.

개선시 주의사항

  • 구조와 반복되는 코드를 한 큐에 고치고 싶을 땐 일단 한 번 참고
    같은 준위의 즉, 같은 레벨의 task를 처리해야한다.
    예를들어 우선 구조를 개선하고 싶다면 디테일한 로직이나 함수는 일단 제쳐두고 구조부터 손을 보고 추후에 디테일한 반복되는 부분을 개선한다든지 이런식으로 가야한다는 것이다.

  • ctrl + t : 리팩토링하면 된다.
  • cmd + opt + b : 해당 메서드로 가기

개선방법 1. 프론트 컨트롤러 작성

  • 원래 모든 req가 각각의 서블릿으로 가서 서블릿을 엄청 많이 생성해야했는데
    프론트 컨트롤러로 몰아서 하나의 공통 로직을 수행하도록 하였다.

Controller interface

public interface ControllerV1 {
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;

}

FrontControllerServlet

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
    private Map<String, ControllerV1> controllerV1Map = new HashMap<>();

    public FrontControllerServletV1() {
        controllerV1Map.put("/front-controller/v1/members/new-form", new MemberControllerV1());
        controllerV1Map.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerV1Map.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        ControllerV1 controller = controllerV1Map.get(requestURI);
        System.out.println("controller = " + controller);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        controller.process(request, response);
    }
}

MemberSaveController

public class MemberSaveControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        request.setAttribute("member", member);
        String viewPath = "/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

개선방법 2. 공통 로직 생성

  • MyView를 생성하여 원래는 각각의 컨트롤러에 들어갈 dispatcher 부분을 묶어줬다.

MyView

public class MyView {
    private String viewPath;

    public MyView(String viewPath) {
        this.viewPath = viewPath;
    }

    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • 컨트롤러는 똑같으니 적지 않겠다.

FrontControllerServlet

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
    private Map<String, ControllerV2> controllerV2Map = new HashMap<>();

    public FrontControllerServletV2() {
        controllerV2Map.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerV2Map.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerV2Map.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        ControllerV2 controller = controllerV2Map.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        MyView view = controller.process(request, response);
        view.render(request, response);
    }
}

MemberSaveController

public class MemberSaveControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

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

        request.setAttribute("member", member);
        return new MyView("/WEB-INF/views/save-result.jsp");
    }
}

개선방법 3. 서블릿 종속성 제거

  • 쓰지도 않는 각각의 컨트롤러에 HttpServletRequest,Response가 들어있는데 그러면 테스팅도 쉽지 않고 지금 MyView에 IDE가 관리해주지도 않는 string 타입으로 prefix와 suffix가 너무 귀찮다. 이를 정리해보자.

ModelView

@Getter
@Setter
public class ModelView {
    private String viewName;
    private Map<String, Object> model = new HashMap<>();
    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}
  • 우선 뷰 이름과 실제 jsp에서 사용할 파라미터들을 담아둘 객체가 필요하다.
    왜? 위의 변경사항을 다시 잘 살펴보면 저게 필요하다.

Contoller

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}
  • 그에 따라 컨트롤러 인터페이스에도 변경점이 생겼다.
    이제 모델 뷰가 생겨서 더이상 각 컨트롤러에게 req, resp를 안 넘겨줘도 되고 컨트롤러에서 파라미터를 받아서 비즈니스 로직을 처리하고 모델에 맞춰서 반환하면 되기 때문이다.

MemberSaveController

public class MemberSaveControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public ModelView process(Map<String, String> paramMap) {
        String username = paramMap.get("username");
        int age = Integer.parseInt(paramMap.get("age"));
        Member member = new Member(username, age);
        memberRepository.save(member);
        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member", member);
        return mv;
    }
}

FrontControllerServlet

@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    public FrontControllerServletV3() {
        controllerMap.put("/front-controller/v3/members/new-form", new
                MemberFormControllerV3());
        controllerMap.put("/front-controller/v3/members/save", new
                MemberSaveControllerV3());
        controllerMap.put("/front-controller/v3/members", new
                MemberListControllerV3());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse
            response)
            throws ServletException, IOException {
        String requestURI = request.getRequestURI();
        ControllerV3 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);
        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }

    private 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;
    }

    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
  • opt + cmd + M , ctrl + alt + m : 드래그 영역 메서드로 빼기
    왜? -> 준위를 맞춰줘야하기 때문에.

  • 논리 이름을 물리 이름으로 바꿔주는게 즉, 실제 view를 찾아주는 역할을 하는게 view Resolver다.

개선방법 4. 개발자 친화적으로

  • 사실 위 개선방법 3이 너무 많이 바뀌어서 헷갈릴 수 있다. 그리고 ModelView가 추가되어 복잡할 수 있다. 이를 빼보자.

  • 개념은 바로 빈모델을 파라미터로 넘겨서 컨트롤러에서 저장하도록 하고 컨트롤러는 해당 파라미터로 비즈니스 로직을 실행 후 뷰의 논리이름을 반환하도록 하는 것이다.

Controller

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}
  • 전에는 컨트롤러가 파라미터만 받아서 뷰모델을 생성해서 내보냈는데
    지금은 파라미터와 빈 뷰모델을 같이 받아서 뷰모델을 채워주고 pointing 해야할 논리이름만 반환한다.

MemberSaveController

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";
    }
}

FrontControllerServlet

@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;
        }
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>(); //추가
        String viewName = controller.process(paramMap, model);
        MyView view = viewResolver(viewName);
        view.render(model, request, response);
    }

    private 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;
    }

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

개선방법 5. 어댑터 추가

  • 개선방법 4의 FrontController를 보면 가장 중요한 역할을 하는객체인 controllerMap이 ControllerV4를 명시해서 받는다. 이렇게 되면 다른 인터페이스로 개발을 할 수 없다.
    이를 개선하기 위해 어댑터 개념을 도입하고

  • 이 어댑터를 통하여 컨트롤러를 호출한다. 다만, 여기서 컨트롤러에서 핸들러로 이름을 바꾼 이유는 해당 종류의 "어댑터"만 있다면 어떠한 데이터도 다 처리할 수 있기 때문에 "컨트롤러"에 국한되지 않아서 이름을 바꿨다.

  • 즉, 프론트 컨트롤러에 더 많은 기능을 추가하자는 것이다.
    유연한 컨트롤러를 만들어보자는 것이다.

MyHandlerAdapter

public interface MyHandlerAdapter {
    boolean supports(Object handler);

    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
  • support 메서드에서 handler는 컨트롤러를 뜻한다.
    어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드이다.

  • handle메서드는 실제 컨트롤러를 호출하고 해당 ModelView를 반환한다.
    만약 반환하지 못 하면 어댑터가 ModelView를 직접 생성해서라도 반환해야한다.
    이전에는 프론트 컨트롤러가 실제 컨트롤러를 호출했지만 이제는 이 어댑터를 통해서 실제 컨트롤러가 호출된다.

  • 이제 얘를 구현한 실제 구현제가 각각의 Controller의 버전에 맞춰서 파라미터를 생성한 ViewModel을 만들어서 논리이름과 함께 mv이라는 변수로 반환할 것이다.

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

    private 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;
    }
}

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);
        HashMap<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> paramMap = new HashMap<>();
        request.getParameterNames().asIterator()
                .forEachRemaining(paramName -> paramMap.put(paramName,
                        request.getParameter(paramName)));
        return paramMap;
    }
}

frontControllerServlet

@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();
        initHandlerAdapters();
    }

    public 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());
                // 추가 확장하고자 한다면 여기만 추가
        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());
    }
    @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 handlerAdapter = getHandlerAdapter(handler);
        ModelView mv = handlerAdapter.handle(request, response, handler);

        String viewName = mv.getViewName();
        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }



    private Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }
    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        for (MyHandlerAdapter adapter : handlerAdapters) {
            if (adapter.supports(handler)) {
                return adapter;
            }
        }
        throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
    }
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }
}
  • 이렇게 확장에있어서는 매우 열려있다. 그래서 OCP 원칙을 매우 잘 지켰다고 볼 수 있다.
profile
智(지)! 德(덕)! 體(체)!

0개의 댓글