[스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 04. MVC 프레임워크 만들기

Turtle·2024년 6월 27일
0
post-thumbnail

🙄프론트 컨트롤러

✔️프론트 컨트롤러(Front Controller) 패턴 특징

  1. 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
  2. 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
  3. 공통 처리 가능
  4. 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

🙄프론트 컨트롤러 도입 V1

✔️프론트 컨트롤러(Front Controller) V1 특징

  1. 클라이언트로부터 HTTP 요청이 들어온다.
  2. URL 매핑 정보에서 컨트롤러를 조회한다.
  3. 해당 컨트롤러를 호출한다.
  4. 컨트롤러에서 JSP를 포워드한다.

✔️프론트 컨트롤러(Front Controller) V1 분석

  1. urlPatterns = "/front-controller/v1/*" : /front-controller/v1를 포함한 하위 모든 요청을 이 서블릿에서 받아들인다.
  2. controllerMap
    • key : 매핑 URL
    • value : 호출될 컨트롤러
  3. service() : requestURI() 메서드로 조회해서 실제 호출할 컨트롤러를 controllerMap에서 찾는다. 없으면 404를 반환하도록 하고 있다면 process() 메서드를 호출해서 해당 컨트롤러를 실행한다.
public interface ControllerV1 {
	void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {

	// 다형성 활용
	/* String : URL */
	/* ControllerV1 : Form/Save/List  */
	private Map<String, ControllerV1> controllerV1Map = new HashMap<>();

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

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("controllerV1Map = " + controllerV1Map);

		// Ex. /front-controller/v1/members/new-form
		String requestURI = req.getRequestURI();

		// 컨트롤러 호출
		ControllerV1 controller = controllerV1Map.get(requestURI);
		if (controller == null) {
			resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		controller.process(req, resp);
	}
}
public class MemberFormControllerV1 implements ControllerV1 {
	@Override
	public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String viewPath = "/WEB-INF/views/new-form.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
}
public class MemberListControllerV1 implements ControllerV1 {

	private final MemberRepository memberRepository = MemberRepository.getInstance();

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

		request.setAttribute("members", members);

		String viewPath = "/WEB-INF/views/members.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
}
public class MemberSaveControllerV1 implements ControllerV1 {

	private final 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);

		String viewPath = "/WEB-INF/views/save-result.jsp";
		request.setAttribute("member", member);
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
}

🙄View 분리 V2

✔️프론트 컨트롤러(Front Controller) V2 특징

  1. 클라이언트로부터 HTTP 요청이 들어온다.
  2. URL 매핑 정보에서 컨트롤러를 조회한다.
  3. 해당 컨트롤러를 호출한다.
  4. V1에서 컨트롤러가 JSP를 직접 포워드했으나 이번 V2에서는 뷰 역할을 하는 MyView 객체를 반환한다.
  5. FrontController에서 render()를 호출한다.
  6. MyView에서 JSP로 포워드한다.
public interface ControllerV2 {
	MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
@WebServlet(name = "frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet {

	// 다형성 활용
	/* String : URL */
	/* ControllerV1 : Form/Save/List  */
	private Map<String, ControllerV2> controllerV2Map = new HashMap<>();

	public FrontControllerServletV2() {
		controllerV2Map.put("/front-controller/v2/members/new-form", new MemberFormControllerV2());
		controllerV2Map.put("/front-controller/v2/members/save", new MemberSaveControllerV2());
		controllerV2Map.put("/front-controller/v2/members", new MemberListControllerV2());
	}

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("controllerV2Map = " + controllerV2Map);
		// Ex. /front-controller/v1/members/new-form
		String requestURI = req.getRequestURI();

		// 컨트롤러 호출
		ControllerV2 controller = controllerV2Map.get(requestURI);
		if (controller == null) {
			resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		MyView myView = controller.process(req, resp);
		myView.render(req, resp);
	}
}
public class MemberSaveControllerV2 implements ControllerV2 {

	private final 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);
		request.setAttribute("member", member);
		return new MyView("/WEB-INF/views/save-result.jsp");
	}
}
public class MemberListControllerV2 implements ControllerV2 {

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

🙄Model 추가 V3

✔️개선할 부분

  1. 서블릿 종속성 제거 : HttpServletRequest, HttpServletResponse 등이 꼭 필요하지 않는 경우도 있으므로
  2. 뷰 이름의 중복 제거 : 물리적 경로 대신 논리적인 이름으로 대체 → 실제 물리적 경로는 프론트 컨트롤러에서 처리하도록 유도

✔️프론트 컨트롤러(Front Controller) V3 특징

  1. 클라이언트로부터 HTTP 요청이 들어온다.
  2. URL 매핑 정보에서 컨트롤러를 조회한다.
  3. 해당 컨트롤러를 호출한다.
  4. 모델이랑 뷰(논리 이름)가 같이 들어있는 ModelView를 반환한다.
  5. 뷰 리졸버를 호출한다.
  6. 뷰 리졸버에서 ModelView에 있는 뷰(논리 이름)를 실제 물리적 경로로 변환하여 MyView로 반환한다.
  7. 프론트 컨트롤러에서 render를 호출한다.
public class ModelView {
	// 뷰 논리 이름
	private String viewName;
	// String : 키
	// Object : 데이터
	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;
	}
}
public interface ControllerV3 {
	ModelView process(Map<String, String> paramMap);
}
@WebServlet(name = "frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet {

	// 다형성 활용
	/* String : URL */
	/* ControllerV1 : Form/Save/List */
	private Map<String, ControllerV3> controllerV3Map = new HashMap<>();

	public FrontControllerServletV3() {
		controllerV3Map.put("/front-controller/v3/members/new-form", new MemberFormControllerV3());
		controllerV3Map.put("/front-controller/v3/members/save", new MemberSaveControllerV3());
		controllerV3Map.put("/front-controller/v3/members", new MemberListControllerV3());
	}

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("controllerV3Map = " + controllerV3Map);
		/* Ex. /front-controller/v1/members/new-form */
		String requestURI = req.getRequestURI();

		// 컨트롤러 호출
		ControllerV3 controller = controllerV3Map.get(requestURI);
		if (controller == null) {
			resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		Map<String, String> paramMap = createParamMap(req);
		ModelView mv = controller.process(paramMap);

		String viewName = mv.getViewName();// Ex. 논리 이름 : save-result

		// Ex. 물리적 경로 : "/WEB-INF/views/save-result.jsp"
		MyView myView = viewResolver(viewName);
		myView.render(mv.getModel(), req, resp);
	}

	private static MyView viewResolver(String viewName) {
		return new MyView("/WEB-INF/views/" + viewName + ".jsp");
	}

	// createParamMap() : HttpServletRequest에서 파라미터 정보를 꺼내 Map으로 변환한다. 해당 Map을 컨트롤러에 전달하면서 호출
	private static Map<String, String> createParamMap(HttpServletRequest req) {
		Map<String, String> paramMap = new HashMap<>();
		req.getParameterNames().asIterator().forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
		return paramMap;
	}
}
public class MemberSaveControllerV3 implements ControllerV3 {

	private final 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 modelView = new ModelView("save-result");
		modelView.getModel().put("member", member);
		return modelView;
	}
}
public class MemberListControllerV3 implements ControllerV3 {

	private final MemberRepository memberRepository = MemberRepository.getInstance();

	@Override
	public ModelView process(Map<String, String> paramMap) {
		List<Member> members = memberRepository.findAll();

		ModelView modelView = new ModelView("members");
		modelView.getModel().put("members", members);
		return modelView;
	}
}
public class MemberFormControllerV3 implements ControllerV3 {

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

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

✔️프론트 컨트롤러(Front Controller) V4 특징

V3와 동일하나 컨트롤러에서 뷰의 논리 이름인 viewName을 반환한다.

public interface ControllerV4 {
	String process(Map<String, String> paramMap, Map<String, Object> model);
}
@WebServlet(name = "frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet {

	// 다형성 활용
	/* String : URL */
	/* ControllerV1 : Form/Save/List */
	private Map<String, ControllerV4> controllerV4Map = new HashMap<>();

	public FrontControllerServletV4() {
		controllerV4Map.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
		controllerV4Map.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
		controllerV4Map.put("/front-controller/v4/members", new MemberListControllerV4());
	}

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		System.out.println("controllerV4Map = " + controllerV4Map);
		// Ex. /front-controller/v1/members/new-form
		String requestURI = req.getRequestURI();

		// 컨트롤러 호출
		ControllerV4 controller = controllerV4Map.get(requestURI);
		if (controller == null) {
			resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		Map<String, Object> model = new HashMap<>();
		Map<String, String> paramMap = createParamMap(req);
		String viewName = controller.process(paramMap, model);

		// Ex. 물리적 경로 : "/WEB-INF/views/save-result.jsp"
		MyView myView = viewResolver(viewName);
		myView.render(model, req, resp);
	}

	private static MyView viewResolver(String viewName) {
		return new MyView("/WEB-INF/views/" + viewName + ".jsp");
	}

	private static Map<String, String> createParamMap(HttpServletRequest req) {
		Map<String, String> paramMap = new HashMap<>();
		req.getParameterNames().asIterator().forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
		return paramMap;
	}
}
public class MemberSaveControllerV4 implements ControllerV4 {

	private final MemberRepository memberRepository = MemberRepository.getInstance();

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

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

		model.put("member", member);
		return "save-result";
	}
}
public class MemberListControllerV4 implements ControllerV4 {

	private final MemberRepository memberRepository = MemberRepository.getInstance();

	@Override
	public String process(Map<String, String> paramMap, Map<String, Object> model) {
		List<Member> members = memberRepository.findAll();
		model.put("members", members);
		return "members";
	}
}
public class MemberFormControllerV3 implements ControllerV3 {

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

🙄유연한 컨트롤러 V5

✔️어댑터 패턴(Adapter Pattern)

여태까지 개발한 컨트롤러는 완전히 다른 인터페이스로 호환이 불가능하다. 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경해보자.

✔️핸들러 어댑터 : 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러를 호출할 수 있다.
✔️핸들러 : 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.

  1. 클라이언트로부터 HTTP 요청이 들어온다.
  2. 프론트 컨트롤러가 핸들러를 조회한다.
  3. 조회한 핸들러를 통해 핸들러를 처리할 수 있는 핸들러 어댑터를 조회한다.
  4. supports() 메서드를 통해 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단한다.
  5. 어댑터가 실제 컨트롤러를 handle() 메서드를 통해 호출하고 그 결과로 ModelView를 반환한다.

✔️핸들러 어댑터

public interface MyHandlerAdapter {
	boolean supports(Object handler);
	ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException;
}

✔️컨트롤러 V3를 지원하는 핸들러 어댑터

public class ControllerV3HandlerAdapter implements MyHandlerAdapter {

	// 해당 핸들러를 지원할 수 있는지?
	@Override
	public boolean supports(Object handler) {
		return (handler instanceof ControllerV3);
	}


	// [3]. 각 버전의 컨트롤러에서 handle() 메서드를 수행
	@Override
	public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
		ControllerV3 controllerV3 = (ControllerV3) handler;

		Map<String, String> paramMap = createParamMap(req);
        // [4]. 핸들러 호출
		ModelView mv = controllerV3.process(paramMap);
        // [5]. 핸들러 어댑터에서 모델 뷰 반환
		return mv;
	}

	private Map<String, String> createParamMap(HttpServletRequest req) {
		Map<String, String> paramMap = new HashMap<>();
		req.getParameterNames().asIterator().forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
		return paramMap;
	}
}

✔️컨트롤러 V4를 지원하는 핸들러 어댑터

public class ControllerV4HandlerAdapter implements MyHandlerAdapter {

	// 해당 핸들러를 지원할 수 있는지?
	@Override
	public boolean supports(Object handler) {
		return (handler instanceof ControllerV4);
	}

	// [3]. 각 버전의 컨트롤러에서 handle() 메서드를 수행
	@Override
	public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
		ControllerV4 controllerV4 = (ControllerV4) handler;

		Map<String, Object> model = new HashMap<>();
		Map<String, String> paramMap = createParamMap(req);
       	// [4]. 핸들러 호출
		String viewName = controllerV4.process(paramMap, model);
        // [5]. 핸들러 어댑터에서 모델 뷰 반환
		ModelView modelView = new ModelView(viewName);
		modelView.setModel(model);
		return modelView;
	}

	private Map<String, String> createParamMap(HttpServletRequest req) {
		Map<String, String> paramMap = new HashMap<>();
		req.getParameterNames().asIterator().forEachRemaining(paramName -> paramMap.put(paramName, req.getParameter(paramName)));
		return paramMap;
	}
}

✔️어댑터 패턴을 적용한 프론트 컨트롤러★

@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {

	private final Map<String, Object> handlerMappingMap = new HashMap<>();
	private final List<MyHandlerAdapter> handlerAdapterList = new ArrayList<>();

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Object handler = getHandler(req);
		if (handler == null) {
			resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}

		MyHandlerAdapter a = getHandlerAdapter(handler);

		ModelView modelView = a.handle(req, resp, handler);
        
		String viewName = modelView.getViewName();

		// [6]. viewResolver 호출 + [7]. MyView 반환
		MyView myView = viewResolver(viewName);
		// [8]. render() 호출
		myView.render(modelView.getModel(), req, resp);
	}

	/* [2]. 핸들러 어댑터 목록에서 해당 핸들러를 처리할 수 있는 핸들러 어댑터를 조회 */
	private MyHandlerAdapter getHandlerAdapter(Object handler) {
		for (MyHandlerAdapter adapter : handlerAdapterList) {
			if (adapter.supports(handler)) {
				return adapter;
			}
		}
		throw new IllegalArgumentException("핸들러 어댑터를 찾을 수 없습니다.");
	}

	/* [1]. 클라이언트로부터 HTTP 요청을 받아 핸들러 매핑 정보에서 핸들러를 조회 */
	private Object getHandler(HttpServletRequest req) {
		String requestURI = req.getRequestURI();
		Object handler = handlerMappingMap.get(requestURI);
		return handler;
	}

	public FrontControllerServletV5() {
		initHandlerMappingMap();
		initHandlerAdapters();
	}

	private void initHandlerAdapters() {
		handlerAdapterList.add(new ControllerV3HandlerAdapter());
		handlerAdapterList.add(new ControllerV4HandlerAdapter());
	}

	private void initHandlerMappingMap() {
		handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
		handlerMappingMap.put("/front-controller/v5/v3/members/save", new MemberSaveControllerV3());
		handlerMappingMap.put("/front-controller/v5/v3/members", new MemberListControllerV3());

		handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
		handlerMappingMap.put("/front-controller/v5/v4/members/save", new MemberSaveControllerV4());
		handlerMappingMap.put("/front-controller/v5/v4/members", new MemberListControllerV4());
	}

	private static MyView viewResolver(String viewName) {
		return new MyView("/WEB-INF/views/" + viewName + ".jsp");
	}
}

0개의 댓글