자바 백엔드 웹 기술의 진화과정(2) - 웹 프론트 컨트롤러 v1, v2, v3

2022. 6. 10. 16:33·Back-End/Spring MVC

자바 백엔드 웹 기술의 진화과정(2) - 웹 프론트 컨트롤러 v1, v2, v3

서블릿 → JSP → MVC패턴 → 프론트 컨트롤러 패턴 → 스프링MVC

프론트 컨트롤러 특징

  • 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  • 요청에 맞는 컨트롤러를 찾아서 호출
  • 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
  • 단계적으로 도입
    • v1 : 프론트 컨트롤러 패턴의 도입
    • v2 : view render를 처리해주는 MyView 도입
    • v3 : 서블릿 종속성 제거 및 뷰의 물리경로를 가지는 뷰 리졸버의 사용
    • v4 : v3코드에서 컨트롤러가 직접 뷰의 논리 이름을 반환
    • v5 : 어댑터(Adapter) 패턴의 도입 → 다양한 종류의 컨트롤러 처리
  • FrontController는 스프링 웹 MVC의 DispatcherServlet 으로 진화한다.

V1 - 프론트 컨트롤러 패턴 도입

ControllerV1 인터페이스

public interface ControllerV1 {
    //로직수행 추상 메서드
    void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
  • 비즈니스 로직을 수행 할 수 있는 process 추상 메서드를 가진다.

MemberFormControllerV1 - 회원 등록 컨트롤러

public class MemberFormControllerV1 implements ControllerV1 {

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        //view로 데이터 전달
        String viewPath = "/WEB-INF/views/new-form.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request,response);
    }
}
  • 회원가입 viewPath를 생성후에, 해당 viewPath로 dispatch

MemberSaveControllerV1 - 회원 저장 컨트롤러

public class MemberSaveControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @Override
    public void 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);
        memberRepository.save(member);

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

        //view로 데이터 전달
        String viewPath="/WEB-INF/views/save-result.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • request객체에 담긴 Parameter로 member객체를 memberRepository에 저장
  • viewPath를 생성후 dispatch

MemberListControllerV1 - 회원 목록 컨트롤러

public class MemberListControllerV1 implements ControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @Override
    public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findALl();
        request.setAttribute("members", members);

        //view로 데이터 전달
        String viewPath = "/WEB-INF/views/members.jsp";
        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
        dispatcher.forward(request, response);
    }
}
  • memberRepository의 member객체들을 setAttribute() 메서드를 이용해서 request에 저장
  • viewPath 생성 후 dispatch

FrontControllerServletV1 - 프론트 컨트롤러

@WebServlet(name = "FrontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

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

    public FrontControllerServletV1() {
        controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
        controllerMap.put("/front-controller/v1/members/save", new MemberSaveControllerV1());
        controllerMap.put("/front-controller/v1/members", new MemberListControllerV1());
    }

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("FrontControllerServletV1.service");

        String requestURI = request.getRequestURI();

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

        controller.process(request, response);
    }
}
  • @WebServlet의 urlPatterns : /front-controller/v1 으로 시작하는 모든 url path를 처리
  • ControllerMap : url에 해당되는 컨트롤러를 매핑
  • service() : requestURI 를 조회 → 호출할 컨트롤러를 controllerMap에서 찾고 없으면 404(SC_NOT_FOUND) 상태 코드를 반환

정리

  1. 프론트 컨트롤러에 url을 파싱
  2. url에 해당하는 controller객체 생성
  3. controller.process() 수행

V2 : view render를 처리해주는 MyView 도입

  • 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있어서 별도로 뷰를 처리하는 객체 생성
  • 중복되는 코드
String viewPath = "/WEB-INF/views/???.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

ControllerV2 인터페이스

public interface ControllerV2 {
    //V1과 유사하나 process의 리턴 타입이 MyView로 변경
    MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
  • 모든 컨트롤러가 MyView를 리턴하도록 변경됨

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);
    }
}
  • HttpServletRequest / HttpServletResponse 객체를 받아서 dispatch를 수행하는 객체

MemberFormControllerV2 - 회원 등록 폼

public class MemberFormControllerV2 implements ControllerV2 {

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        return new MyView("/WEB-INF/views/new-form.jsp");
    }
}

MemberSaveControllerV2 - 회원 저장

public class MemberSaveControllerV2 implements ControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @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);
        memberRepository.save(member);

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

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

MemberListControllerV2 - 회원 목록

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

    @Override
    public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        List<Member> members = memberRepository.findALl();
        request.setAttribute("members", members);
        return new MyView("/WEB-INF/views/members.jsp");
    }
}

프론트 컨트롤러 V2

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

정리

  1. 매핑된 Controller를 호출하고 process를 수행하면 MyView를 반환 받는다
  2. 반환받은 MyView의 render() 메서드를 호출해서 forward로직을 수행하게 한다.

V3 - Model 추가

  • 컨트롤러에서 HttpServletRequest / HttpServletResponse를 사용하지 않아도 매개변수로 받음
    → 불필요한 객체 / Servlet에 종속적
  • HttpServletRequest를 Model로 사용 → Model을 별도로 생성하도록 변경

V3에서 개선할 사항

  1. Servlet종속성 제거 → Model추가
  2. View path 중복 제거 → View Resolver 추가

ControllerV3 인터페이스

public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}
  • process() 메서드는 Map을 매개변수로 받으며, `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;
    }
}
  • Servlet의 종속성을 제거하기 위해 별도추가한 Model객체
  • view에 가져갈 데이터가 있는 HashMap과 실제 물리주소인 viewName을 가진다.

MemberFormControllerV3 - 회원 등록 폼

public class MemberFormControllerV3 implements ControllerV3 {
    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}
  • ModelView를 생성할 때 new-form 이라는 논리 이름을 지정.

MemberSaveControllerV3 - 회원 저장

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

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

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

        ModelView mv = new ModelView("save-result");
        mv.getModel().put("member", member);
        return mv;
    }
}
  • 저장 로직 수행후 마찬가지로 ModelView에 “save-result” 라는 논리이름을 지정
  • 또한 ModelView의 model멤버변수에 저장한 멤버객체를 담아서 반환

MemberListControllerV3 - 회원 목록

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;
    }
}
  • members라는 논리 이름을 지정한 ModelView객체 생성
  • 해당 ModelView객체의 model멤버 변수에 members 데이터를 추가한뒤 반환

프론트 컨트롤러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 {
        String requestURI = request.getRequestURI();

        ControllerV3 controller = controllerMap.get(requestURI);
        if (controller == null) {
            response.setStatus(HttpServletResponse.SC_NOT_FOUND);
            return;
        }
        //paramMap에 request정보들을 담은 createParamMap()메서드를 수행한후 컨트롤러에 넘김
        Map<String, String> paramMap = createParamMap(request);
        ModelView mv = controller.process(paramMap);
        String viewName = mv.getViewName();

        MyView view = viewResolver(viewName);
        view.render(mv.getModel(), request, response);
    }

    //논리 주소를 받은뒤 실제 물리주소로 만들어서 반환해주는 메서드
    private MyView viewResolver(String viewName) {
        return new MyView("/WEB-INF/views/" + viewName + ".jsp");
    }

    //request에 있는 Parameter들을 paramMap에 담아서 반환해줌
    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;
    }
}
  • Map<String, String> createParamMap() : request에 있는 Parameter들을 사용할 수 있게 paramMap에 담아주는 메서드
  • MyView viewResolver() : 넘어온 viewName(논리주소)를 받아서 실제 물리주소로 만들어서 반환 해주는 메서드
  • service()
    1. requestURI를 컨트롤러에 넘겨서 어떤 컨트롤러를 사용할지 선택
    2. 선택된 컨트롤러의 process()메서드에 paramMap을 넘겨서 해당 컨트롤러에서 파라미터를 사용해서 로직수행후 ModelView를 받음
    3. ModelView에 있는 논리주소를 viewResolver() 메서드에 넘겨서 실제 물리주소로 변환
    4. MyView의 render()메서드에 실제 주소를 넘겨서 forward시킴

정리

  • process() 메서드의 코드 양 변화

반응형
'Back-End/Spring MVC' 카테고리의 다른 글
  • 스프링 MVC의 구조 이해(1) - DispatcherServlet, 핸들러 매핑과 핸들러 어댑터, 뷰 리졸버
  • 자바 백엔드 웹 기술의 진화과정(3) - 웹 프론트 컨트롤러 v4, v5
  • 자바 백엔드 웹 기술의 진화과정(1), 서블릿, jsp, MVC 패턴
  • 서블릿의 사용, HttpServletRequest, HttpServletResponse
LightSource
LightSource
어제보단 발전한 오늘의 나를 위한 블로그
    반응형
  • LightSource
    LightSourceCoder
    LightSource
  • 전체
    오늘
    어제
    • 분류 전체보기 (152)
      • Git (4)
      • Language (6)
        • Java (6)
      • Back-End (63)
        • Spring Boot (4)
        • MyBatis (1)
        • Oracle (1)
        • PL SQL (3)
        • JPA (26)
        • Spring Data JPA (5)
        • Spring MVC (8)
        • Spring (12)
        • Spring Security (2)
        • Redis (1)
      • Front-End (38)
        • 아이오닉 (2)
        • JSP (7)
        • JavaScript (4)
        • React (16)
        • TypeScript (3)
        • Angular (6)
      • AWS (1)
      • CI & CD (1)
      • 개발지식 (13)
        • 네트워크 (9)
        • CS 지식 (4)
      • 데이터모델링 (2)
      • Tool (1)
      • 프로젝트 (5)
      • 독후감 (2)
      • 잡생각 (0)
      • 면접 준비 (1)
      • 알고리즘 (14)
  • 블로그 메뉴

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

  • 공지사항

  • 인기 글

  • 태그

    배열요소수정
    배열요소삭제
    리액트
    react
    배요소열추가
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
LightSource
자바 백엔드 웹 기술의 진화과정(2) - 웹 프론트 컨트롤러 v1, v2, v3
상단으로

티스토리툴바