MVC 프레임워크 만들기 - View 분리(v2), Model 추가(v3)

2021. 7. 5. 21:53·교육 및 인강/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

인프런 김영한 님의 스프링 강의이며, 섹션 4 - MVC 프레임워크 만들기를 정리한다.

이전 시간의 프론트 컨트롤러를 개선하면서 진행한다. 

자세한 설명이 궁금하면 수강을 권장한다.

 

스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 - 인프런 | 강의

웹 애플리케이션을 개발할 때 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. 스프링 MVC의 핵심 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 원

www.inflearn.com


View 분리 - v2

프론트 컨트롤러로 여러 컨트롤러를 제어하지만 뷰로 이동되는 코드가 중복이 된다. 이를 개선해보자.

View를 사용하는 ControllerV2 만들어 보자.

public interface ControllerV2 {

    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

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

이전에는 ControllerV1의 process는 반환하는 게 없었는데 컨트롤러마다 뷰를 독립하기 위해 변경되었다.

@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

    private Map<String, ControllerV2> controllerMap = new HashMap<>();

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

        String requestURI = request.getRequestURI();

        ControllerV2 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        MyView view = controller.process(request, response);
        view.render(request, response);
    }
}

프론트컨트롤러V2도 로직 마지막 부분에 MyView로 forward를 실행하게 처리한다.

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

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

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

public class MemberSaveControllerV2 implements ControllerV2 {
    ...
}

public class MemberFormControllerV2 implements ControllerV2 {
    ...
}

MyView를 반환하게 되어 중복 코드 코드들이 많이 사라졌다.

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ...
        
	// 아래의 코드가 반환되는 MyView로 대체되었다.
        // String viewPath = "/WEB-INF/views/members.jsp";
        // RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        // dispatcher.forward(request, response);
        
        return new MyView("/WEB-INF/views/members.jsp");
    }

이어서 모델도 도입해보자.


Model 추가 - v3

서블릿 종속성 제거

프론트 컨트롤러가 생기면서 기존의 컨트롤러의 서블릿 의존이 없어도 될 정도가 되었다.

이제 컨트롤러의 서블릿 종속성을 제거해보자.

 

뷰 이름 중복 제거

컨트롤러에서 지정되는 뷰 이름이 매번 물리적인 경로를 다 넣다 보니 /WEB-INF/views/... 중복이 되고 있다.

프론트 컨트롤러에서 중복 경로를 처리하고, 컨트롤러는 뷰의 논리 이름을 반환하게 단순화를 진행해보자.

기존 컨트롤러의 서블릿을 대체하기 위한 ModelView를 만들어보자 (스프링의 ModelAndView를 떠올리면 된다.)

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

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

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

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

    public String getViewName() {
        return viewName;
    }
}

이 ModelView는 컨트롤러의 서블릿 객체를 대체하는 역할을 한다.

ModelView를 반환하는 ControllerV3 인터페이스를 생성하고 컨트롤러가 상속받게 구현한다

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

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

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

        return mv;
    }
}

public class MemberSaveControllerV3 implements ControllerV3 {
    ...
}

이전과 다르게 컨트롤러에 서블릿 객체가 없어지면서 파라미터로 Map 형식으로 받게 되었다.

현재 구조에 맞게 호출하는 프론트컨트롤러V3를 만들자

@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("FrontControllerServletV3.service");

        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }

        // Map에 담아 컨트롤러에 보내준다
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);

        String viewName = mv.getViewName(); // 논리이름 new-form, members 등 가져오기
        MyView view = viewResolver(viewName);

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

    // 객체 생성이라 메서드로 분리한다.
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    // 상세 로직이라 메서드로 분리한다.
    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;
    }
}

 

request의 모든 정보를 map 에 담는 로직이 추가되었고 이를 컨트롤러에 보내게 되었다.

기존의 MyView는 컨트롤러에서 반환하지 않고, 프론트컨트롤러에서 viewResolver로 반환하게 변경했다.

 

프론트 컨트롤러가 좀 더 복잡해졌지만, 기존의 컨트롤러는 불필요한 중복 코드가 없어져서 많이 깔끔해졌다.

마지막으로 MyView.rander()를 오버 로딩한다. 

 

public class MyView {
    ...

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

컨트롤러가 서블릿에서 벗어나 순수 자바 코드로만 구성되었다.

 

하지만 더 나아가, 여기서도 불편함이 존재한다. 컨트롤러가 매번 ModelView를 생성해서 반환해야 한다.

다음 시간에는 이를 개선한 v4를 알아보자.

 

 

 

'교육 및 인강 > 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 카테고리의 다른 글

스프링 MVC 이해 - 전체 구조, 핸들러 매핑과 핸들러 어댑터, 뷰 리졸버  (0) 2021.07.13
MVC 프레임워크 만들기 - 실용적인 컨트롤러 (v4), 유연한 컨트롤러 (v5)  (0) 2021.07.13
MVC 프레임워크 만들기 - 프론트 컨트롤러 패턴 소개, 도입  (0) 2021.07.05
MVC 패턴 - 개요, 적용, 한계  (0) 2021.06.29
서블릿, JSP - 회원 관리 요구사항, 서블릿으로 만들기, JSP로 만들기  (0) 2021.06.29
'교육 및 인강/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 카테고리의 다른 글
  • 스프링 MVC 이해 - 전체 구조, 핸들러 매핑과 핸들러 어댑터, 뷰 리졸버
  • MVC 프레임워크 만들기 - 실용적인 컨트롤러 (v4), 유연한 컨트롤러 (v5)
  • MVC 프레임워크 만들기 - 프론트 컨트롤러 패턴 소개, 도입
  • MVC 패턴 - 개요, 적용, 한계
loop-study
loop-study
오늘도 공부하자
  • loop-study
    개발 공부할래?
    loop-study
  • 전체
    오늘
    어제
    • 분류 전체보기 (187)
      • 목표 및 회고 (26)
      • 세미나 & 워크샵 (1)
      • 교육 및 인강 (67)
        • TDD, Clean Code with Java (5)
        • ATDD, 클린 코드 with Spring (6)
        • DDD 세레나데 (3)
        • 인프라 공방 (6)
        • 이규원의 현실 세상의 TDD (19)
        • 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 (18)
        • 스프링 MVC 2편 - 백엔드 웹 개발 활용 기술 (0)
        • 모든 개발자를 위한 HTTP 웹 기본 지식 - 김영한 (8)
        • 코딩으로 학습하는 GoF의 디자인 패턴 (1)
        • 스프링 시큐리티 완전정복 6.x (1)
      • 서적 (62)
        • 객체지향의 사실과 오해 (1)
        • 객체지향과 디자인패턴 (7)
        • 만들면서 배우는 클린 아키텍처 (3)
        • 테스트 주도 개발로 배우는 객체 지향 설계와 실천 (1)
        • 오브젝트: 코드로 이해하는 객체지향 설계 (17)
        • 리팩토링 : 코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기 (0)
        • 토비의 스프링 (3)
        • 엔터프라이즈 애플리케이션 아키텍처 패턴 (9)
        • 개발자의 글쓰기 (1)
        • 소프트웨어 장인 (17)
        • Real MySQL 8.0 (2)
        • JVM 밑바닥까지 파헤치기 (0)
      • 개발 & 방법론 (29)
        • Java (13)
        • TDD (5)
        • ATDD (3)
        • DDD (6)
        • 인프라 (2)
        • SQL (0)
      • 개인이야기 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

    • 백엔드 로드맵
  • 인기 글

  • 태그

    소프트웨어 장인
    마틴 파울러
    java
    객체지향
    추상화
    스터디
    넥스트스탭
    현실세상의 TDD
    모든 개발자를 위한 HTTP 웹 기본 지식
    테스트 주도 개발
    인프런
    DDD 세레나데
    JUnit
    조영호
    이규원
    스프링
    study
    Test Driven Development
    fastcampus
    백기선
    TDD
    Patterns of Enterprise Application Architecture
    김영한
    nextstep
    ATDD
    장인정신
    자바
    엔터프라이즈 애플리케이션 아키텍처 패턴
    인프라공방
    오브젝트
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
loop-study
MVC 프레임워크 만들기 - View 분리(v2), Model 추가(v3)
상단으로

티스토리툴바