loop-study
개발 공부할래?
loop-study
전체 방문자
오늘
어제
  • 분류 전체보기 (186)
    • 목표 및 회고 (25)
    • 세미나 & 워크샵 (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)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

  • 백엔드 로드맵

인기 글

태그

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

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
loop-study

개발 공부할래?

MVC 프레임워크 만들기 - View 분리(v2), Model 추가(v3)
교육 및 인강/스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술

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

2021. 7. 5. 21:53

인프런 김영한 님의 스프링 강의이며, 섹션 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
    오늘도 공부하자

    티스토리툴바