본문 바로가기
개발자 일지/Spring

[인프런 김영한 로드맵4]스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(4)

by 네빌링 2022. 6. 4.
반응형

-이전 시간의 JSP, 서블릿 회원관리 개발을 하며 느낀 단점을 보완하기 위해 MVC프레임워크를 만들어본다.

-버전을 5가지로 만들어 본다. 프론트 컨트롤러 도입(v1), View분리(v2), Model 추가(v3), 단순하고 실용적인 컨트롤러(v4), 유연한 컨트롤러(v5)로 단계별로 만들어 본다.

-모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture4)


 

4.MVC 프레임워크 만들기

 

여기서는 기존 회원관리 애플리케이션의 단점을 한 단계씩 버전업하면서 단계별로 만들어본다. 버전별로 먼저 어떻게 변경되는지 간단하게 정리하면 다음과 같다.

 

  • v1 : 프론트 컨트롤러라는 것을 도입한다. 말 그대로 기존 컨트롤러에 앞서 동작하는 부분으로, 요청에 맞는 컨트롤러를 호출해주는 역할을 한다.
  • v2 : v1에서 반복되는 뷰 로직이 있는데 이를 분리해준다.
  • v3 : Model을 추가해서 컨트롤러에 반복되는 서블릿 종속성을 제거하고 뷰 이름 중복도 제거한다.
  • v4 : v3과 비슷하지만 컨트롤러 구현부분에서 ModelView를 직접 생성 후 반환하지 않고 viewName만 반환하도록 변경한다.
  • v5 : 프론트 컨트롤러가 유연하게 v3,v4 등을 사용할 수 있도록 어댑터패턴을 적용해본다.

 

1.프론트 컨트롤러 패턴 소개

 

이전 시간에서 JSP만 사용한 경우, 서블릿만 사용한 경우의 단점을 확인해봤다. 이 단점을 보완하기 위해 공통된 부분을 프론트 컨트롤러에서 처리하도록 리팩토링 해본다. 

 

2.프론트 컨트롤러 도입 - v1

 

우선 다 만들어진 v1의 패키지 구조는 다음과 같다.

 

web/frontcontroller/v1

 

 

다음 단계를 거쳐 개발한다.

 

  • 인터페이스 ControllerV1을 만든다.
  • 인터페이스를 구현한 컨트롤러(MemberFormController, MemberSaveController, MemberListController)를 만든다. 이전 서블릿의 로직은 그대로 갖다 쓴다.
  • 인터페이스를 구현한 FrontController를 만든다. HttpServlet을 상속하고 @WebServlet을 통해 URL 매핑을 한다. 이 때 모든 URL에서 접근가능하도록 마지막에 /*로 처리한다.
  • FrontController 내부적으로는 Map을 만들어 URL을 key, 컨트롤러 구현체를 value로 처리한 매핑정보 MAP을 하나 만든다.

 

코드로 얘기하는 게 좋을 것 같으니 다음과 같이 바로 만들어본다. 우선 ControllerV1 인터페이스를 다음과 같이 만든다.

 

[ControllerV1.java]

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

 

이 인터페이스를 구현한 각 Member 컨트롤러를 만든다.

 

[MemberFormControllerV1.java]

public class MemberFormControllerV1 implements ControllerV1 {
    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        //forward() : 다른 서블릿이나 JSP로 이동 가능. 서버 내부에서 다시 호출이 발생
        dispatcher.forward(request, response);
    }
}

 

[MemberSaveControllerV1.java]

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);

        //model에 데이터를 보관한다.
        request.setAttribute("member", member);

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

 

[MemberListControllerV1.java]

public class MemberListControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();

        request.setAttribute("members", members);

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

 

그리고 프론트 컨트롤러를 만든다.

 

[FrontControllerServletV1.java]

//urlPattern을 *로 주어 /front-controller/v1/ 하위의 모든 URL요청 매핑되어 이 서블릿이 호출됨
@WebServlet(name = "FrontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
    //URL을 key로 하여, ControllerV1 구현체들을 value로 가져올 수 있는 매핑정보MAP
    private Map<String, ControllerV1> controllerMap = new HashMap<>();

    //URL별 매핑정보 생성
    public FrontControllerServletV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV1.service");

        //요청 URI에 맞는 구현체가져옴
        String requestURI = request.getRequestURI();
        ControllerV1 controller = controllerMap.get(requestURI);

        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //가져온 구현체의 오버라이딩 메소드 실행
        controller.process(request, response);
    }
}

 

실행순서는 다음과 같다.

 

  • localhost:8080/front-controller/v1/new-form의 요청이 오면 위의 FrontContoller가 실행된다.
  • 생성자에서 매핑정보를 우선 생성한다. URL주소를 key, ControllerV1 인터페이스 구현체를 value로 Map에 넣어둔다.
  • service()가 실행된다.
    • request.getRequestURI()를 통해 요청한 URI를 꺼내온다.
    • 꺼내온 URI로 controllerMap에서 구현체를 가져온다.(다형성 적용)
    • 매핑된 구현체가 없으면 404 처리를 하고, 구현체가 있으면 오버라이딩 메소드(process())를 실행하여 이전에 만들었던 JSP forward로직을 실행한다.

 

3.View 분리 - v2

 

v1에서는 컨트롤러마다 JSP forward() 로직이 공통적으로 들어가 있었다. 이부분을 제거하기 위한 수정을 진행한다.

 

뷰 객체인 MyView를 다음과 같이 만든다.(frontcontroller 패키지 밑에 만들어 v1,2,3,4 모두 사용하도록 만든다)

 

[MyView.java]

public class MyView {
    private String viewPath;

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

	//각 컨트롤러마다 호출했던 forward 로직을 여기서 처리한다.
    public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
 }

 

컨트롤러 인터페이스는 다음과 같다.

 

[ControllerV2.java]

public interface ControllerV2 {
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 

그리고 v1과 공통적으로 회원폼, 회원저장, 회원목록 컨트롤러를 만든다.

 

[MemberFormControllerV2.java]

public class MemberFormControllerV2 implements ControllerV2 {
    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}

 

[MemberSaveControllerV2.java]

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);

        //model에 데이터를 보관한다.
        request.setAttribute("member", member);

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

 

[MemberListControllerV2.java]

public class MemberListControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findAll();
        request.setAttribute("members", members);

        return new MyView("/WEB-INF/views/members.jsp");
    }
}

 

프론트 컨트롤러는 다음과 같다. v1에서는 process()를 실행하면 각각의 컨트롤러에서 forward()가 실행되었다. v2는 MyView의 render()를 실행하는데, 셋팅한 viewPath를 통해 forward()가 실행된다.

 

[FrontControllerV2.java]

//urlPattern을 *로 주어 /front-controller/v2/ 하위의 모든 URL요청 매핑되어 이 서블릿이 호출됨
@WebServlet(name = "FrontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {
    //URL을 key로 하여, ControllerV2 구현체들을 value로 가져올 수 있는 매핑정보MAP
    private Map<String, ControllerV2> controllerMap = new HashMap<>();

    //URL별 매핑정보 생성
    public FrontControllerServletV2() {
        controllerMap.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
        controllerMap.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
        controllerMap.put("/front-controller/v2/members", new MemberListControllerV2());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV2.service");

        //요청 URI에 맞는 구현체가져옴
        String requestURI = request.getRequestURI();
        ControllerV2 controller = controllerMap.get(requestURI);

        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //가져온 구현체의 오버라이딩 메소드 실행
        //v2 : 기존과 다르게 viewName을 셋팅한 MyView 객체를 return받은 후, render()를 실행한다.
        MyView view = controller.process(request, response);
        view.render(request, response);
    }
}

 

4.Model 추가 - v3

 

변경하고 싶은 부분이 두 가지가 있다.

 

첫번째로 컨트롤러는 HttpServletRequest, HttpServletResponse에 종속적이다. 모델도 컨트롤러에서 request.setAttribute()를 통해 사용했다. 컨트롤러가 서블릿 기술을 사용하지 않도록 변경해보자.

 

두번째로 컨트롤러에서 리턴하는 뷰네임에 중복이 있다. "/WEB-INF/views/"나 ".jsp"가 그러하다. 이 부분을 제거하기 위해 컨트롤러는 논리이름만 반환한다. 예를 들어 다음과 같다.

 

//기존 중복 포함된 반환명
//return "/WEB-INF/views/new-form.jsp"

//중복 제거한 논리명
//return "new-form"

 

모델로 사용할 클래스를 다음처럼 만든다.

 

[ModelView.java]

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

    public ModelView(String viewName) {
        this.viewName = viewName;
    }

    public String getViewName() {
        return viewName;
    }

    public void setViewName(String viewName) {
        this.viewName = viewName;
    }

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

    public void setModel(Map<String, Object> model) {
        this.model = model;
    }
}

 

컨트롤러 인터페이스를 다음과 같이 만든다. 보다시피 서블릿 인자가 없다. 대신 Map을 인자로 사용한다.

public interface ControllerV3 {
    //서블릿 기술 안 씀
    ModelView process(Map<String, String> paramMap);
}

 

컨트롤러는 다음과 같이 변경된다.

 

[MemberFormControllerV3.java]

public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}

 

[MemberSaveControllerV3.java]

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

 

[MemberListControllerV3.java]

public class MemberListControllerV3 implements ControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public ModelView process(Map<String, String> paramMap) {
        List<Member> members = memberRepository.findAll();
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);

        return mv;
    }
}

 

컨트롤러들이 동일하게 변경된 부분은 ModelView 객체 생성 후 논리명을 셋팅하는 부분과 model처리가 필요한 부분은 setAttribute가 아니라 ModelView 객체에서 Model Map을 꺼내 put처리를 해준다는 점이다. 

 

프론트 컨트롤러는 다음과 같다.

 

[FrontControllerServletV3.java]

//urlPattern을 *로 주어 /front-controller/v3/ 하위의 모든 URL요청 매핑되어 이 서블릿이 호출됨
@WebServlet(name = "FrontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {
    //URL을 key로 하여, ControllerV3 구현체들을 value로 가져올 수 있는 매핑정보MAP
    private Map<String, ControllerV3> controllerMap = new HashMap<>();

    //URL별 매핑정보 생성
    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 {
        System.out.println("FrontControllerServletV3.service");

        //요청 URI에 맞는 구현체가져옴
        String requestURI = request.getRequestURI();
        ControllerV3 controller = controllerMap.get(requestURI);

        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        
        //v3 : request, response를 넘겨주지 말고 paramMap을 넘긴다.
        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);
    }

    //view의 논리명을 물리명으로 변경
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    //request 요청 파라미터를 Map에 셋팅해주는 역할
    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;
    }
}

 

위의 프론트 컨트롤러를 v2와 비교했을 때 달라진 점은 process(request, response)를 호출하는 것이 아니라 process(paramMap)을 호출한다는 점이다. 서블릿 기술 객체를 넘겨 컨트롤러에서 모델처리를 하는 것이 아니라, 모델처리를 프론트컨트롤러에서 처리 후(createPraramMap) 리턴 받은 map을 변수로 전달한다는 점이다. 

 

그리고 process()의 처리 결과는 MyView가 아니라 ModelView이다. 컨트롤러에서 ModelView에 논리명을 셋팅하고 model처리를 한 후 반환한다. 이후 반환 받은 ModelView 타입의 변수에서 논리명을 꺼내 물리명으로 viewResolver 처리를 한다.

 

마지막으로 view.render(mv.getModel(), request, response)에서는 컨트롤러에서 모델처리(map에 put처리)한 JSP에서 보여줄 객체들을 setAttribute 처리를 한다. 다음과 같다.

 

[MyView.java]

public class MyView {
    private String viewPath;

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

    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException  {
        modelToRequestAttribute(model, request);
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }

    //컨트롤러에서 model에 담은 객체들을 꺼내 setAttribute 처리하여 JSP에서 사용할 수 있도록 처리
    private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach((key, value) -> request.setAttribute(key, value));
    }
}

 

ModelView는 이후 스프링에서 ModelAndView이고 viewResolver는 viewResolver 역할을 한다. 실제 내부적 구현 방법에 대해 확인해볼 수 있었다.

 

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

 

앞의 v3에서는 서블릿 종속성 제거, 뷰 경로 중복 제거를 통해 설계를 개선했다. 더 개선할 부분 중 하나는 컨트롤러 구현에서 항상 ModelView 객체 생성과 반환 부분이다.

이 부분을 좀 더 편하게 사용할 수 있도록 개선해보자. 컨트롤러가 ModelView가 아니라 String 타입의 viewName만 반환하도록 다음과 같이 변경한다.

 

//ControllerV3 인터페이스의 process()
//ModelView process(Map<String, String> paramMap);

//ControllerV4 인터페이스의 process()
//String process(Map<String, String> paramMap, Map<String, Object> model);

 

모델이 파라미터로 전달되기 때문에 모델을 직접 생성 안한다. MemberSaveControllerV4 구현을 보면 다음과 같다.

 

[MemberSaveControllerV4.java]

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에 저장 후 viewName만 반환
        model.put("member", member);

        return "save-result";
    }
}

 

MemberListControllerV4와 MemberFormControllerV4도 동일한 방식으로 적용하면 된다.

 

FrontController는 이전 버전과 비교하여 바뀐 부분은 다음과 같다.

 

[FrontControllerServletV4.java]

@WebServlet(name = "FrontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {
    //...
    
    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV4.service");

        //요청 URI에 맞는 구현체가져옴
        String requestURI = request.getRequestURI();
        ControllerV4 controller = controllerMap.get(requestURI);

        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //가져온 구현체의 오버라이딩 메소드 실행
        //v4:paramMap뿐만 아니라 Model을 같이 넘긴다.
        Map<String, String> paramMap = createParamMap(request);
        Map<String, Object> model = new HashMap<>();
        String viewName = controller.process(paramMap, model);

        //viewResolver처리 : 논리이름(viewName)을 물리이름으로 변경
        MyView view = viewResolver(viewName);

        view.render(model, request, response);
    }
    
    //...
}

 

기존에는 process(paramMap)이었으나, v4에서는 model을 같이 넘겨서 model에 값을 처리 후 viewName만 반환한다. 

 

6,7.단순하고 실용적인 컨트롤러 - v5

 

마지막으로 v5에서는 어댑터패턴을 적용하여 상황에 따라 유연하게 컨트롤러를 사용하도록 변경한다. 어떤 개발자 또는 어떤 상황에서는 ControllerV3을 사용하고 또 다른 상황에서는 ControllerV4를 사용하고 싶을 때 어댑터패턴을 적용하면 유연한 설계가 가능하다. v110을 v220으로 바꿔주는 어댑터를 생각하면 된다.

 

로직이 꽤 바뀌는 부분이라 클라이언트 요청부터 동작 순서를 간단하게 설명하면 다음과 같다.

 

  1. 클라이언트가 요청을 보내면 FrontController 매핑이 되어 핸들러(컨트롤러) 매핑정보에서 해당 컨트롤러를 조회한다. 예를 들어 localhost:8080/front-controller/v5/v3/members/new-form URL 요청이 들어오면 MemberFormControllerV3을 갖고 온다.
  2. 가져온 핸들러(컨트롤러)에 맞는 핸들러어댑터를 갖고 온다. 
  3. 핸들러어댑터의 handler()를 호출해서 컨트롤러 로직 수행 후 ModelView를 반환한다.
  4. viewName을 꺼내서 viewResolver를 호출 후 MyView를 반환한다.
  5. MyView의 render(model)을 호출해서 HTML 응답을 받는다.

 

여기서 핸들러는 컨트롤러라고 생각하면 된다. 핸들러 어댑터는 중간에 어댑터 역할을 해줘서 다양한 컨트롤러 호출을 할 수 있다.

 

우선 MyHandlerAdapter 인터페이스를 만든다.

 

[MyHandlerAdapter.java]

public interface MyHandlerAdapter {

    //어댑터가 handler(controller)를 처리할 수 있는지 확인 반환
    boolean supports(Object handler);

    //어댑터가 실제 컨트롤러 호출하는 부분
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}

 

이제 어댑터를 구현하는데 ControllerV3, ControllerV4를 지원하는 어댑터를 구현해본다.

 

[ControllerV3HandlerAdapter.java]

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.java]

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

 

 

위의 어댑터에서 하나씩 살펴보면 다음과 같다.

  • supports() : 핸들러(컨트롤러)가 ControllerV3 구현체인지 확인한다.
  • handle() : 넘겨받은 핸들러(컨트롤러)를 ControllerV3 또는 ControllerV4로 캐스팅 후, 버전에 맞는 process처리를 한다. 

 

마지막으로 프론트 컨트롤러는 다음과 같다.

 

[FrontControllerV5Servlet.java]

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
    //ControllerV3,V4등 다 넣어야 하기 때문에 <String, Object>로 처리
    private final Map<String, Object> handlerMappingMap = new HashMap<>();
    private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

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

    //핸들러(컨트롤러) 매핑
    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());

        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 {
        //요청에 맞는 핸들러 가져옴 : MemberFormControllerV3, MemberFormControllerV4
        Object handler = getHandler(request);
        if (handler == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        //어댑터 반환 : ControllerV3HandlerAdapter, ControllerV4HandlerAdapter
        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 Object getHandler(HttpServletRequest request) {
        String requestURI = request.getRequestURI();
        return handlerMappingMap.get(requestURI);
    }

    private MyHandlerAdapter getHandlerAdapter(Object handler) {
        //iter 입력하면 for문 자동처리
        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");
    }
}

 

  • initHandlerMappingMap() : 이전 버전들의 controllerMap이다. 다른 점은 <Key, Value>가 <String, ControllverV>가 아니라 <String, Object>라는 점이다. 여기서는 V3,V4만 예를들지만 추후 V1,2,5를 다 넣을 수 있도록 처리한다.
  • initHandlerAdapter() : 만든 핸들러 어댑터를 담는다.
  • service() 
    • getHandler() : 요청에 맞는 컨트롤러를 가져온다.
    • getHandlerAdapter() : handlerAdapters에 지금은 ControllerHandlerAdapterV3, ControllerHandlerAdapterV4만 있지만 V4,V5등도 추가될 것이다. List<MyHandlerAdapter> 타입의 handlerAdapters를 반복하며 넘겨받은 handler가 어떤 버전의 컨트롤러인지 체크(support())한 후, 해당 어댑터를 반환한다.
    • 반환한 어댑터의 handle() 호출을 통해 컨트롤러 로직 수행 후 ModelView를 반환받는다.
    • 이후 똑같은 render() 처리가 진행된다.

 

여기서 가장 중요한 부분은 ControllerV3HandlerAdapter, ControllerV4HandlerAdapter의 handle() 부분이다. 이 부분을 보면 각 컨트롤러 process() 처리 후 반환타입이 다르다. 이런 차이를 어댑터를 통해 형식을 ModelView로 맞추어 반환해주기 때문에 어댑터의 필요한 이유가 된다.

 

버전별로 모두 버전업을 해봤는데 이 v5 코드가 스프링 MVC의 축약된 핵심 버전으로 이해하면 되고 구조도 비슷하다.

반응형