본문 바로가기
Spring/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

MVC 프레임워크 만들기 2 - 모델 추가, 실용적인 컨트롤러

by hk27 2022. 1. 23.
모델 객체를 따로 만들어볼까요?

 

안녕하세요!

오늘은 지난 게시글에 이어서 MVC 프레임워크를 발전 시켜 보겠습니다.

 

지난 게시글에는 프론트 컨트롤러 패턴을 도입해서 코드 중복을 제거하였습니다. 관심 있는 분들은 아래 게시글을 참고해주세요!

https://passionate.tistory.com/40

 

MVC 프레임워크 만들기 1 - 프론트 컨트롤러

안녕하세요! 오늘은 MVC 패턴을 따르는 프레임워크를 만들어보겠습니다. 지난 시간에 MVC 패턴을 적용해서 회원 관리 웹을 만들어보았습니다. 관심 있으신 분은 참고해주세요! https://passionate.tistor

passionate.tistory.com

 

지난 게시글에서 만든 MVC 프레임워크는 2가지 단점이 있습니다.

1. 컨트롤러가 서블릿에 종속합니다: 컨트롤러가 파라미터로 HttpServletRequest, HttpServletResponse 객체를 받습니다. HttpServletRequest 객체는 요청 파라미터를 받을 때와 데이터를 담는 모델로 사용하고, HttpServletResponse 객체는 사용하지 않습니다. 

따라서 요청 파라미터와 모델을 자바의 Map 객체로 만들면 컨트롤러가 서블릿 기술을 사용하지 않아도 됩니다. 

이렇게 하면 구현 코드도 매우 단순해지고, 테스트 코드 작성도 쉽습니다. 

 

2. 뷰 이름이 중복됩니다: 뷰 이름이 "/WEB-INF/views/new-form.jsp"로 사용되는데, new-form 외에는 모든 뷰에서 중복됩니다. 컨트롤러는 논리 이름인 new-form 부분만 반환하고, 실제 물리 위치는 프론트 컨트롤러에서 처리하면 중복이 줄어듭니다. 향후 뷰의 폴더 위치가 변경되어도 프론트 컨트롤러 코드만 수정하면 됩니다.

 

두 가지 단점을 고친 V3 버전의 MVC 프레임워크를 만들어봅시다.

 

V3 - 모델 추가

V3의 중요한 특징은 Controller에서 ModelView를 반환하는 것과 뷰의 논리 이름을 물리 주소로 변환하는 viewResolver가 추가된 것입니다. 

 

ModelView

앞에서는 Controller에서 MyVIew를 반환했는데 이제는 ModelView를 반환하니 ModelView 객체를 만들어봅시다.

ModelView는 모델(뷰로 보낼 데이터 저장) 역할을 하고, 뷰의 논리 이름을 저장합니다.

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

 

ControllerV3 인터페이스

V3의 컨트롤러는 ModelView를 반환합니다. 

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

컨트롤러에서 서블릿의 기능을 전혀 사용하지 않습니다. 따라서 구현이 단순하고, 테스트 코드 작성이 용이합니다.

 

회원 등록 폼 컨트롤러

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

컨트롤러에서는 View의 논리 이름인 "new-form"만 반환합니다. 

 

회원 저장 컨트롤러

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

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

파라미터로 넘어온 값을 HttpServletRequest에서 찾지 않고 paramMap에서 찾습니다.

프론트 컨트롤러에서 HttpServletRequest의 파라미터를 모두 paramMap에 저장하므로 컨트롤러는 편하게 Map을 사용할 수 있습니다.

또한, 뷰의 논리 이름을 저장한 ModelView 객체를 만들고 Model을 꺼내서 뷰로 전송할 데이터를 담습니다.

 

회원 목록 컨트롤러

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

 

 

프론트 컨트롤러

가장 중요한 프론트 컨트롤러입니다.

HttpServletRequest에서 파라미터를 받아서 paramMap을 만들고, ModelView를 ViewResolver에 넘겨서 물리 주소를 갖는 MyView를 만드는 역할이 추가되었습니다.

@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);
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        MyView view = viewResolver(mv.getViewName());
        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");
    }
}

createParamMap 메소드에서 paramMap을 만들고, viewResolver 메소드에서 물리 주소를 갖는 MyView객체를 만듭니다.

18번째 줄에서 view.render 메소드를 부를 때 모델 객체를 함께 넘깁니다.

MyView 클래스에서 모델을 request의 attribute로 담아서 뷰로 forward 합니다.

 

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);
    }
    public void render(Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        model.forEach((key, value)->request.setAttribute(key, value));
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}

가장 밑의 render 메소드가 추가되었습니다.

model 개체를 받아서 request에 저장합니다. 이렇게 하면 뷰 로직에서 request의 attribute를 이용할 수 있습니다. 

 

 

V4 - 단순하고 실용적인 컨트롤러

앞서 만든 V3도 잘 설계된 프레임워크이지만, 컨트롤러에서 ModelView 객체를 생성하고 반환해야 하는 번거로움이 있습니다.

따라서 컨트롤러에서 View의 논리 이름만 반환하는 V4를 만들어봅시다.

컨트롤러에서 모델 정보를 리턴하지 않기 없기 때문에, 모델을 프론트 컨트롤러에서 만들어서 컨트롤러를 부를 때 같이 보냅니다.

 

V4의 구조는 아래와 같습니다.

V3와 거의 같고, FrontControllr가 Controller를 호출할 때 model을 같이 보내고, Controller는 ModelView 대신 viewName을 반환하는 차이가 있습니다.

 

Controller 인터페이스

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

String으로 view의 논리 이름을 반환하고, model 객체를 받습니다.

 

Controller 구현 클래스

V3에서 수정된 부분을 위주로 살펴봅시다. 

/* 1. 회원 등록 폼
public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}
*/
public class MemberFormControllerV4 implements ControllerV4 {
    @Override
    public String process(Map<String, String> paramMap, Map<String, Object> model) {
        return "new-form";
    }
}

/* 2. 회원 저장
        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member", member);
        return mv;
 */
        model.put("member", member);
        return "save-result";

/* 3. 회원 목록
        ModelView mv = new ModelView("members");
        mv.getModel().put("members", members);
        return mv;
 */
        model.put("members", members);
        return "members";

뷰의 논리 이름만 리턴하고, 파라미터로 넘어온 model에 뷰로 전송할 값을 넣습니다.

 

프론트컨트롤러

수정된 부분을 위주로 살펴봅시다.

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

        MyView view = viewResolver(mv.getViewName());
        view.render(mv.getModel(),request, response);
   	*/
        Map<String, Object> model = new HashMap<>();
        String viewPath = controller.process(paramMap, model);
        
        MyView view = viewResolver(viewPath);
        view.render(model,request, response);
    }

}

모델을 만들어서 컨트롤러를 호출할 때 넘기고, view의 render 메소드를 호출할 때 model을 전달합니다.

 

코드가 정말 깔끔해졌습니다! 컨트롤러에서 모델을 생성하지 않고 전달된 모델을 사용하며, 뷰의 논리 이름만 반환합니다. 

 

만약 어떤 개발자는 V3 방식으로, 어떤 개발자는 V4 방식으로 개발하고 싶다면 어떻게 해야 할까요?

현재의 코드로는 둘을 동시에 지원할 수 없습니다.

다음 게시글에서는 둘을 동시에 지원하는 유연한 컨트롤러 V5를 만들어보겠습니다.

https://passionate.tistory.com/42

 

인프런  '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의를 듣고 공부하며 정리한 자료입니다. 

잘못된 부분은 피드백 주시면 감사하겠습니다. 

글 읽어주셔서 감사합니다 :-)

 

참고 자료

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술, 섹션 4. MVC 프레임워크 만들기 https://www.inflearn.com/course/스프링-mvc-1

 

댓글