개발은 아름다워

[ Spring ] 고대의 서블릿을 찾아서(7) - ModelView를 없애고 더욱 단순하게 본문

스프링

[ Spring ] 고대의 서블릿을 찾아서(7) - ModelView를 없애고 더욱 단순하게

do_it_zero 2024. 10. 14. 09:09

고대의 서블릿에서 현재 Spring MVC 프레임워크까지 여정을 살펴보고 있다.
서블릿 -> 템플릿 엔진 -> MVC 패턴 -> FrontController -> view -> ModelView 여정까지 오게 되었다.

ModelView 까지 적용한 과정을 간략하게 적어보면
1. ModelView 객체에 viewName과 controller로 부터 발생한 데이터를 model에 담는다.
2. controller는 ModelView 객체를 반환한다.
3. FrontController는 controller가 반환한 ModelView 객체에 있는
1) viewName을 viewResolver를 통해 viewPath로 바꿈
2) model에 담긴 데이터를 HttpServletRequest request객체로 데이터 전달
4. view.render는 model 데이터를 받은 후 jsp 서블릿에게 데이터를 전달

그렇다면 질문! 굳이 꼭 ModelView 객체를 생성하고 반환해야 하는 것일까? 실제 필자가 다니고 있는 회사는 올드한 회사라 아직 ModelAndView를 쓴다. 매번 컨트롤러에서 ModelAndView 객체를 생성 후 return 각 컨트롤러마다 써야하는 귀찮음이 있다.
그렇다면 ModelView를 반환하지 않고 실용적인 방법으로 바꿔보자

객체를 이용하기

이번에는 바로 FrontController 코드를 봐보자

@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 {
        System.out.println("FrontControllerServletV2.service");
        String requestURI = request.getRequestURI();
        ControllerV4 controller = controllerMap.get(requestURI);
        
        if(controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
        }

        // http요청에 있는 데이터를 paramMap에 넣어줘야함
        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");
    }
}

이전 ModelView를 이용했을 때의 차이는 Map<String,Object> model = new HashMap<>();를 넘겨준다는 것이다. 요청이 오면 model이라는 객체가 생성된다. 이 때 model 객체의 참조값은 x001이라고 가정하자. controller가 model로 받는 매개변수의 참조값도 x001이 된다. 왜냐하면 자바는 call by Value이기 때문

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

        // controller에 매개변수로 넘겨주는 model의 참조값도 x001 
        String viewName = controller.process(paramMap, model);

이제 감이 오지 않는가? FrontController에서 생성된 model의 참조값은 controller에서 사용되는 model의 참조값과 같다. 따라서 controller.process(paramMap, model); 이 진행된 후에는 model의 참조값 x001에 해당하는 데이터 값들이 변경되어 있다는 것이다.

이에 맞게 Controller 인터페이스와 구현체인 controller들의 코드를 변경해보자.

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

public class MemberFormControllerV4 implements ControllerV4 {
    // ModelView 사용 전
    // @Override
    // public MyView process(HttpServletRequest request, HttpServletResponse response)
    //         throws ServletException, IOException {
    //     return new MyView("/WEB-INF/views/new-form.jsp");
    // }

    // ModelView 사용 후
    // @Override
    // public ModelView process(Map<String, String> paramMap) {
    //     return new ModelView("new-form");
    // }

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

public class MemberSaveControllerV4 implements ControllerV4{

    private MemberRepository memberRepository = MemberRepository.getInstance();

    // ModelView 사용 전
    // @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);
    //     System.out.println("member = " + member);
    //     memberRepository.save(member);

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

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

    // ModelView 사용 
    // @Override
    // public ModelView process(Map<String, String> paramMap) {
        
    //     //paramMap은 Http 요청 데이터를 주고 받는 용도
    //     String username = paramMap.get("username");
    //     int age = Integer.parseInt(paramMap.get("age"));
        
    //     Member member = new Member(username, age);
    //     memberRepository.save(member);

    //     // ModelView에 데이터를 넣을 수 있음
    //     ModelView mv = new ModelView("save-result");
    //     mv.getModel().put("member", member);
    //     return mv;
    // }

    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        //paramMap은 Http 요청 데이터를 주고 받는 용도
         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";
    }    
}



public class MemberListControllerV4 implements ControllerV4{
    
    private MemberRepository memberRepository = MemberRepository.getInstance();

    // ModelView 사용 전
    // @Override
    // public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    //     System.out.println("MvcMemberListServlet.service");
    //     List<Member> members = memberRepository.findAll();
    //     request.setAttribute("members", members);
    //     return new MyView("/WEB-INF/views/members.jsp");
    // }

    // ModelView 사용
    // @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;
    // }

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

이전 코드를 비교 할 수 있게 주석처리를 해놨다. 딱봐도 코드가 훨~씬 말끔하게 변경되었다. ModelView와 동작 방식은 같다. 차이는 ModelView 객체를 반환하는 것이 아니라 컨트롤러는 논리 이름만 반환한다. controller에서 발생한 데이터는 FrontController에서 생성한 model 객체에 담기게 된다. 이번 버전의 컨트롤러는 매우 단순하고 실용적이다. 기존 구조에서 모델을 파라마터로 넘기고, 뷰의 논리 이름을 반환한다는 작은 아이디어를 적용했을 뿐인데, 코드가 훨씬 깔끔해졌다.
고대의 서블릿에서 지금 FrontController까지 한 번에 점프한 것이 아니라 점진적으로 불편한 점을 해결하면서 발전했다. 이로 보아 프레임워크나 공통 기능이 수고로워야 사용하는 개발자가 편리해진다는 것을 알 수 있다.

참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/