[TIL] #4. MVC 프레임워크 만들기 ①

kiteB·2021년 9월 12일
0

TIL-Spring3

목록 보기
1/7
post-thumbnail
post-custom-banner

프론트 컨트롤러 패턴 소개

프론트 컨트롤러 도입 전

MVC 패턴만 적용했을 때는 공통 코드가 컨트롤러에 있어서, 클라이언트가 각각 컨트롤러를 호출해야 했다.

프론트 컨트롤러 도입 후

공통 코드가 프론트 컨트롤러에 있어서, 클라이언트의 요청은 모두 프론트 컨트롤러로 들어온다.


FrontController 패턴 특징

프론트 컨트롤러 서블릿 하나로 모든 클라이언트의 요청을 받은 뒤, 프론트 컨트롤러가 각 요청에 맞는 컨트롤러를 찾아서 호출해준다.

이로 인해 여러 컨트롤러에 있던 공통 코드를 프론트 컨트롤러 하나로 모을 수 있고, 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용할 필요도 없어진다!

스프링 웹 MVC의 DispatcherServletFrontController 패턴으로 구현되어 있다.


프론트 컨트롤러 도입 - v1

📌 V1

기존 코드를 최대한 유지하면서, 프론트 컨트롤러를 도입할 것이다!


ControllerV1

package hello.servlet.web.frontcontroller.v1;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV1 {

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

}

서블릿과 비슷한 컨트롤러 인터페이스를 도입함으로써
각 컨트롤러는 이 인터페이스를 호출하여 로직의 일관성을 가져갈 수 있게 되었다.


회원 등록/저장/조회 컨트롤러

ControllerV1 인터페이스를 구현한 컨트롤러를 만들어볼 것이다.
지금 단계에서는 기존 로직을 최대한 유지하는 것이 핵심이다!

코드는 아래 링크 참고!

🔗 전체 코드 확인하기


FrontControllerServletV1 - 프론트 컨트롤러

이제 프론트 컨트롤러를 만들어서 위에 만든 컨트롤러를 호출할 수 있도록 만들어보자!

(전체 코드는 위에 있는 링크 참고!)

urlPatterns

@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
  • /front-controller/v1를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
  • Ex) /front-controller/v1, /front-controller/v1/a, /front-controller/v1/a/b

controllerMap

private Map<String, ControllerV1> controllerMap = new HashMap<>();
  • key: 매핑
  • value: 호출될 컨트롤러

service()

    @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);
    }
  • 먼저 requestURI를 조회해서 실제 호출할 컨트롤러를 controllerMap에서 찾는다.
  • 만약 없다면 404(SC_NOT_FOUND) 상태 코드를 반환한다.
  • 컨트롤러를 먼저 찾고 controller.process(request, response);를 호출해서 해당 컨트롤러를 실행한다.

JSP

  • JSP는 기존에 만들어두었던 것을 그대로 사용한다.

실행 결과

http://localhost:8080/front-controller/v1/members/new-form에 접속해서 form 제출하면 아주 잘 된다!

기존 서블릿, JSP로 만든 MVC와 동일하게 실행되는 것을 확인할 수 있다!


View 분리 - v2

프론트 컨트롤러 패턴을 도입해서 모든 Request 요청에 대해서 프론트 컨트롤러를 거치게 함으로써 공통 처리를 모을 수 있었다.

하지만 여전히 모든 컨트롤러에서 뷰로 이동하는 부분에 중복되는 코드가 존재한다.😭

String viewPath = "경로";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);

📌 V2

컨트롤러에서 뷰로 이동하는 중복 코드를 제거하기 위해, 별도로 뷰를 처리하는 객체를 만들자!

기존에 컨트롤러에서 바로 JSP를 forward 해주는 과정을 제거하고 MyView를 프론트 컨트롤러에 반환한다.

그러면 프론트 컨트롤러는 MyView 인터페이스의 render()를 호출하여 해당 인터페이스에서 JSP를 forward 하도록 구조를 바꿨다!


MyView

package hello.servlet.web.frontcontroller;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.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);
    }
}

render() 메서드 호출 시 인자로 받은 request, response를 인자로 JSP로 forward 한다.


ControllerV2

package hello.servlet.web.frontcontroller.v2;

import hello.servlet.web.frontcontroller.MyView;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public interface ControllerV2 {

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

}

ControllerV1과 달리, 뷰를 반환한다!


회원 등록/저장/조회 컨트롤러

🔗 전체 코드 확인하기

  • 이제 각 컨트롤러는 복잡한 dispatcher.forward()를 직접 생성해서 호출하지 않아도 된다! MyView 객체를 생성하고 안에 뷰 이름만 넣고 반환해주면 된다.
  • V1과 비교하여 중복이 많이 제거되었다.😉

FrontControllerServletV2

(전체 코드는 위의 링크 참고)

MyView view = controller.process(request, response);
  • ControllerV2의 반환 타입이 MyView이므로 프론트 컨트롤러의 호출 결과로 MyView를 반환받는다.
  • view.render()를 호출하면 forward 로직을 수행해서 JSP가 실행된다.

실행 결과

이번에도 잘 동작한다 ㅎㅎ


Model 추가 - v3

📌 V3

이번에는 서블릿의 종속성 및 뷰 이름 중복을 제거해보자!

✔ 서블릿 종속성 제거

지금까지 모든 컨트롤러에는 Request, Response 객체를 인자값으로 전달해줬다. 하지만 모든 컨트롤러에서 이 인자들이 다 사용된 것은 아니었다.

🤔 이렇게 비효율적인 동작을 수정할 수는 없을까?

요청 파라미터 정보를 Map으로 대신 넘기고, request 객체를 Model로 사용하지 말고 별도의 Model 객체를 만들어서 반환하면 된다!

이제 컨트롤러가 서블릿을 전혀 사용하지 않도록 하여 구현 코드도 단순하게, 테스트 코드 작성도 쉽게 할 수 있도록 해보자!

✔ 뷰 이름 중복 제거

그리고 컨트롤러에서 viewPath를 지정할 때 다음과 같이 했다.

  • WEB-INF/views/new-form.jsp
  • WEB-INF/views/save-result.jsp
  • WEB-INF/views/members.jsp

계속 WEB-INF/views/가 중복되었는데 이제 이렇게 전체 경로를 다 적어주지 말고 new-form, save-result 등 단순화해보자!

이렇게 중복을 제거해주면 나중에 경로를 수정하게 될 때 한번에 수정할 수 있다.🤩


V3 구조

  • V2에서는 프론트 컨트롤러에서 바로 MyViewrender()를 호출해 JSP로 forward 해줬다. V3에서는 그전에 viewResolver를 호출해 MyView를 반환하도록 했다.
  • 컨트롤러MyView가 아니라 ModelView 객체를 반환해주도록 바뀌었다.
  • ModelView: 서블릿의 종속성을 제거하기 위해 별도의 Model을 만들어 View 이름까지 전달하는 ModelView 객체를 만들었다.

ModelView

package hello.servlet.web.frontcontroller;

import lombok.Getter;
import lombok.Setter;

import java.util.HashMap;
import java.util.Map;

@Getter @Setter
public class ModelView {

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

    public ModelView(String viewName) {
        this.viewName = viewName;
    }
}
  • 뷰의 이름과, 뷰를 렌더링할 때 필요한 model 객체를 가지고 있다.
  • model은 단순히 map으로 되어있으므로 컨트롤러에서 뷰에 필요한 데이터를 key, value로 넣어주면 된다.

ControllerV3

package hello.servlet.web.frontcontroller.v3;

import hello.servlet.web.frontcontroller.ModelView;

import java.util.Map;

public interface ControllerV3 {

    ModelView process(Map<String, String> paramMap);
}
  • 이제 서블릿을 전혀 사용하지 않게 되어, 구현이 매우 단순해지고 테스트 코드 작성도 쉬워졌다.
  • HttpServletRequest가 제공하는 파라미터는 paramMap에 담아서 호출해주면 된다.
  • 응답 결과로 뷰 이름과, 뷰에 전달할 Model 데이터를 포함하는 ModelView 객체를 반환하면 된다.

MemberFormControllerV3 - 회원 등록 폼

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

public class MemberFormControllerV3 implements ControllerV3 {

    @Override
    public ModelView process(Map<String, String> paramMap) {
        return new ModelView("new-form");
    }
}
  • 이제 ModelView를 생성할 때 뷰의 논리적인 이름을 new-form으로 지정한다.
  • 실제 물리적인 이름은 프론트 컨트롤러에서 viewResolver로 처리한다.

MemberSaveControllerV3 - 회원 저장

package hello.servlet.web.frontcontroller.v3.controller;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;

import java.util.Map;

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;
    }
} 
paramMap.get("username");
  • 파라미터 정보는 map에 담겨져 있다. map에서 필요한 요청 파라미터를 조회하면 된다.
mv.getModel().put("member", member);
  • 모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 담고 반환한다.

MemberListControllerV3 - 회원 목록 조회

🔗 전체 코드 확인하기


FrontControllerV3

(전체 코드는 위의 링크 참고)

  • MyView view = viewResolver(viewName): 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변환한다.

    • 논리 뷰 이름: members
    • 물리 뷰 이름: /WEB-INF/views/member.jsp
  • view.render(mv.getModel(), request, response): 뷰 객체를 통해 HTML 화면을 렌더링한다.

    • JSP는 request.getAttribute()로 데이터를 조회하기 때문에, 모델의 데이터를 꺼내 request.setAttribute()로 담아둔다.
    • JSP로 포워드해 JSP를 렌더링한다.

실행 결과

이번에도 역시 잘 실행된다~


하지만 아직도 수정해야할 부분이 남았다. 다음 시간에 마저 알아보자!

profile
🚧 https://coji.tistory.com/ 🏠
post-custom-banner

0개의 댓글