spring

Front Controller (3)

Beencle 2023. 3. 18. 00:08
public void render(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewPath);
        requestDispatcher.forward(request,response);
    }

서블릿 종속성 제거 컨트롤러 입장에서 HttpServletRequest, HttpServletResponse를 받아서 처리를 하는데 위의 Request와 Response가 필요하지 않다. 우리에게 필요한것은 파라미터 정보이기 때문에 Map에서 받은 정보를 대신 넘기게 되면 컨트롤러에서는 서블릿의 기술을 몰라도 동작이 가능하다. 그리고 전달하는 값을 Reqeust가 아니라 Model 객체를 만들어 반환이 가능하다. 이것말고도 우리는 중복되는 부분이 있는데 바로 반환하는 파일의 경로이름이다.(viewPath)

여태 우리는 모든 경로의 이름인 WEB-INF/view/*.jsp 이런식으로 *부분을 제외하면 모두 반복하는 모습이 보이는데 이부분역시 따로 빼서 우리는 앞으로 파일의 경로를 앞에 WEB-INF/ivews와 뒤에 .jsp부분을 제외한 *만 사용할 것이다.

출처 : 김영한 MVC 1편

먼저 뷰와 반환 할 정보를 받을 객체를 만들어보자.

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 Map<String, Object> getModel() {
        return model;
    }

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

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

안에 내용을 간단하게 보면 이동할 뷰페이지의 이름과 정보들을 담을 객체를 만드는데 생성자로 뷰페이지의 이름을 받았다. 이어 각 컨트롤러에서 사용될 인터페이스를 보자.

public interface ControllerV3 {
    ModelView process(Map<String,String> paraMap);
}

이전에 V2모델에서는 request와 response를 받았지만 이번에는 Map타입으로 정보를 받아왔다. 이것이 어떻게 받아오는지는 각각의 컨트롤러를 확인해보자.

- New-form

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

- Save

public class MemberSaveControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

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

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

        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member",member);
        return mv;
    }
}

- List

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

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

각각의 컨트롤러를 보게 되면 이전에 V2에 비해 각각의 컨트롤러에서 하는일이 줄어든 모습을 볼 수 있다.

이제 이 컨트롤러를 조종할 FrontController를 보자

@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 {
        System.out.println("frontControllerServletVdd2.service");

        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI); // 해당 키 값의 value 추출

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

        Map<String, String> paramMap = createParaaMap(request);

        ModelView mv = controller.process(paramMap);
        String viewName = mv.getViewName();

        MyView myView = viewResolver(viewName);

        myView.render(mv.getModel(),request,response);
    }

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

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

여기서부터 이제 하나씩 코드를 천천히 설명하면서 이해해보자.

하나의 예시로 New-form에서 받은 정보를 save컨트롤러로 이동하는 예시를 들어보자.

먼저 FrontController에서 매핑주소가 들어오게 되면 앞서 V2와 같이 각 URI에 해당하는 controller가 생성이 되고

new-form에서 username과 age를 받아 FrontController로 오게 되는데 Save컨트롤러로 가기전에 FrontController에서 Save컨트롤러로 username과 age를 process메서드에 넣어서 가져가야하므로

Map<String, String> paramMap = createParaaMap(request);

이 부분이 실행이 된다. 이 기능은 아래에 정의된 그대로 request로 받은 username과 age를 getParameterName로 받아 하나씩 이름에 맞는 값이 저장이 된다.

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

그리고 Save컨트롤러에서는 위에서 반환된 객체를 가지고 process가 실행이 되는데 

public class MemberSaveControllerV3 implements ControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

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

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

        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member",member);
        return mv;
    }
}

paramname이라는 이름에 저장된 username과 age를 get메서드를 통해 변수에 저장이 되고 이를 Member객체에 담은 뒤에 MemberRepository에 save메서드를 통해 저장을 한다. 그리고 ModelView를 생성한뒤 이동할 viewName를 저장하고 같은 ModelView객체에 member라는 이름으로 username과 age를 저장했던 Member객체를 담아서 리턴한다. 

그러면 다시 FrontController에서는 해당 ModelView객체를 mv란 이름으로 저장을 하고 그 안에 viewName을 꺼낸뒤 꺼낸값으로 앞 뒤에 WEB-INF/view와 .jsp를 붙여서 다시 반환을 하게 된다. 이때 반환 된 값은 실제로 디스패처 포워딩이 실행되는 MyView에 담기게 되는데 그렇게 되면 MyView에는 실제 포워딩 되는 파일의 주소가 담기게 되고 아까 Save컨트롤러에서 받환 된 ModelView의 객체를 매개변수로 가져가게 되면서 최종 적으로 MyView에는 저장될 저장된 객체와 포워딩할 위치까지 알 수 있게 된다. 이제 이정보들을 갖고잇는 MyView를 보자

public class MyView {
    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);
    }

    private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
        model.forEach((key, value)-> request.setAttribute(key,value));
    }
}

여기서 FrontController에서 실행되는 render부분을 보게되면 Map형태인 model에 객체가 담겨지게 되고 그 값을 modelToRequestAttribute라는 메서드를 만들어 forEach문으로 하나씩 request.setAttribute를 통해 값이 담기게 된다. 그리고 맨 처음에 저장된 viewPath의 경로를 디스패처로 받아서 포워딩을 한 모습이다.


이번 V3강의는 보면서도 이게 어디서 이동하고 어디서 담기고 값이 어디서 어디로 가는지도 많이 헷갈렷는데 보다보니 알게 될 정도로 난이도가 있어보였다. 간단 간략하게 줄여보자면

1. FrontController 호출과 동시에 request로 전달된 값이 있다면 paramMap에 담기게 되고 해당 값이 없다면 빈 상태로 해당 컨트롤러로 이동을 한 뒤

2. 가져온 값이 있다면 그 값들을 꺼내어 사용되고(예시에서는 저장) 만약 리스트를 가져오는 컨트롤러라면 정보들을 List로 담아서 ModelView에 담고 동시에 각 컨트롤러에서 viewName을 생성해 담긴 정보들(username,age,viewName)가지고 다시  FrontController로 이동

3. 각 컨트롤러에서 가져온 viewName은 실제 주소로 사용하기에 짧으므로 viewResolver라는 메서드를 만들어 앞 뒤로 WEB-INF/views와 .jsp를 붙여서 다시 반환

4. 이제 모든 정보가 완전해 졋으니 모든 정보들을 가지고 실제로 서비스 되는 MyView로 이동

5. MyView에서는 가져온 객체가 한세트가 아니라 List처럼 많을 수 있으므로 forEach문을 통해 request.setAttribute에 담아 클라이언트로 보낼준비

6. viewName으로 만든 viewPath를 이용해 디스패처 생성 후 포워딩