반응형

프론트 컨트롤러 패턴 소개

1. FrontController 패턴 특징

 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음

 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출

공통 처리 가능

 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 됨

 

※ 스프링 웹 MVC와 프론트 컨트롤러

스프링 웹 MVC의 핵심: FrontController패턴, DispatcherServlet이 FrontController 패턴으로 구현되어 있음

 

 

프론트 컨트롤러 도입 - version 1

1. version 1의 구조

 

 

2. 코드

2.1 ControllerV1 인터페이스

package hello.servlet.web.frontcontroller.v1;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface ControllerV1 {
	void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

 서블릿과 유사한 ControllerV1 인터페이스를 컨트롤러가 각각 구현함.

  프론트 컨트롤러에서는 해당 인터페이스를 호출해서 다형성으로 각각의 구현 컨트롤러와의 의존관계를 끊을 수 있음.

 

2.2 ControllerV1 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV1 - 회원 등록 컨트롤러

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

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.v1.ControllerV1;

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

MemberSaveControllerV1 - 회원 저장 컨트롤러

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

import java.io.IOException;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;


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);
		
		String viewPath = "/WEB-INF/views/save-result.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);
	}
}

MemberListControllerV1 - 회원 목록 컨트롤러

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

import java.io.IOException;
import java.util.List;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v1.ControllerV1;

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);
		
		String viewPath = "/WEB-INF/views/members.jsp";
		RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
		dispatcher.forward(request, response);		
	}
}

 

2.3 FrontControllerServletV1 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v1;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.v1.controller.MemberFormControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberListControllerV1;
import hello.servlet.web.frontcontroller.v1.controller.MemberSaveControllerV1;

// urlPatterns 끝의 *은 상위 경로(/front-controller/v1/ )를 포함한 하위 경로 모두 이 서블릿에서 받아들인다는 의미
// /front-controller/v1, /front-controller/v1/depth1, /front-controller/v1/depth1/depth2
// 모두 해당 서블릿에서 받아들인다.
@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");
		
		// getRequestURI() 메서드로 요청 URI를 얻음
		String requestURI= request.getRequestURI();
		
		// controllerMap에서 적절한 컨트롤러를 찾아서 인터페이스형으로 담음
		ControllerV1 controller = controllerMap.get(requestURI);
		
		// 만약 찾는 컨트롤러가 없으면
		if(controller == null) {
			// 404(SC_NOT_FOUND) 상태 코드를 반환후 리턴
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 컨트롤러가 있으면 ControllerV1 인터페이스를 구현한 컨트롤러에 해당하는
		// process(reqeust, response)메서드를 호출해서 해당 컨트롤러 로직을 실행.
		controller.process(request, response);
		
	}
}

 

 

view 분리 - version 2

1. version2의 구조

 기존에 컨트롤러에서 바로 JSPforward해주는 과정이 사라지고 컨트롤러는 MyView 라는 뷰 인터페이스를 반환하면

컨트롤러에서는 MyView 인터페이스의 render() 메서드를 호출함으로써 해당 인터페이스에서 JSPforward하도록 구조 변경

 

 

2. 코드

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

 컨트롤러에서 JSP forward하는 코드를 모듈화 하기 위해 만든 객체로 클래스 생성시 전달한 jsp 경로를 viewPath로 받아 생성되며 render() 메서드 호출시 인자로 받은 request, response를 인자로 jsp forward를 한다.

 

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

 V1과 유사하지만 반환 타입이 MyView.

 각 컨트롤러는 dispatcher.forward()를 직접 생성해서 호출하지 않아도 된다. 단순히 MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환하면 됨.

 

2.3 ControllerV2 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV2 - 회원 등록 컨트롤러

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

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;

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

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 - 회원 저장 컨트롤러

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

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;

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

public class MemberSaveControllerV2 implements ControllerV2 {
    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);
        Member savedMember = memberRepository.save(member);

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

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

MemberListControllerV2 - 회원 목록 컨트롤러

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

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.ControllerV2;

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

public class MemberListControllerV2 implements ControllerV2 {
    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");
    }
}

 모든 컨트롤러에서 공통적으로 구현되있던 dispatcher.forward()가 사라지고 반환타입인 MyView 객체를 생성하여 반환함.

 

2.4 FrontControllerServletV2 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v2;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v2.controller.MemberFormControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberListControllerV2;
import hello.servlet.web.frontcontroller.v2.controller.MemberSaveControllerV2;


// 1. /front-controller/v2/members/new-form가 urlPattern에 의해서 호출하게됨
@WebServlet(name="frontControllerServletV2", urlPatterns = "/front-controller/v2/*")
public class FrontControllerServletV2 extends HttpServlet{
	private Map<String, ControllerV2> controllerMap = new HashMap<>();
	
	public FrontControllerServletV2() {
		// 3. /front-controller/v2/members/new-form을 controllerMap에서 찾음
		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();
		
		// 2. /front-controller/v2/members/new-form을 controllerMap에서 찾음
		ControllerV2 controller = controllerMap.get(requestURI);
		if(controller == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 4. new MemberFormControllerV2()의 process 호출해서 new MyView("/WEB-INF/views/new-form.jsp")를 리턴받음
		// V1과는 다르게 MyView객체를 반환타입으로 반환받는다. 
		MyView view = controller.process(request, response);
		
		// 5. 생성된 myView 인스턴스의 render 실행
		// 반환받은 MyView 객체의 render(request, response) 메서드를 호출하면 forward로직이 수행되어 JSP가 실행된다
		view.render(request, response);
	}
}

 

model 분리 - version 3

1. version3의 구조

 Version2에서 프론트 컨트롤러에서 바로 MyView객체의 render()메서드를 호출해 JSP로 forward 해줬다면

Version3에선 그전에 viewResolver를 호출해 MyView를 반환하도록 함.

 컨트롤러는 MyView가 아니라 ModelView 객체를 반환해주도록 바뀜

※ ModelView

 - 지금까진 컨트롤러에서 모델로 HttpServletRequest의 setAttribute를 사용했는데,

서블릿의 종속성을 제거하기위해 별도의 Model을 만들어 View 이름까지 전달하는 ModelView 객체를 만든다.

 

 

2. 코드

2.1 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 객체를 가지고 있는 ModelView 객체.

view의 논리 이름을 저장하는 viewName과 뷰에 필요한 데이터를 담는 Model을 Map으로 구현.

 

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

Request, Response 객체를 인자값으로 받지 않기에 컨트롤러는 서블릿 기술로부터 종속성이 사라짐. 테스트 코드를 구현하기 쉬워졌다는 의미도 됨.

 HttpServletRequest에서 필요한 정보는 프론트 컨트롤러에서 paramMap으로 담아 호출하면 됨.

 ControllerV3에서는 반환타입이 ModelView로 forward될 view의 논리명과 데이터가 담긴 Model이 반환.

 

2.3 ControllerV3 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV3 - 회원 등록 컨트롤러

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

import java.util.Map;

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

public class MemberFormControllerV3  implements ControllerV3{
	@Override
	public ModelView process(Map<String, String> paramMap) {
		// 파라미터가 없기때문에 paramMap에서 꺼낼게 없음
		return new ModelView("new-form");
	}
}

MemberSaveControllerV3 - 회원 저장 컨트롤러

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

import java.util.Map;

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;

public class MemberSaveControllerV3 implements ControllerV3 {
	MemberRepository memberRepository = MemberRepository.getInstance();
	
	@Override
	public ModelView process(Map<String, String> paramMap) {
		// 파라미터를 원래 request.getParameter함수로 꺼내줬던 것을 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;
	}
}

MemberListControllerV3 - 회원 목록 컨트롤러

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

import java.util.List;
import java.util.Map;

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;

public class MemberListControllerV3 implements ControllerV3{
	private MemberRepository memberRepository = MemberRepository.getInstance();
	
	@Override
	public ModelView process(Map<String, String> paramMap) {
		// 파라미터가 없기때문에 paramMap에서 꺼낼게 없음
		List<Member> members = memberRepository.findAll();
		ModelView mv = new ModelView("members");
		mv.getModel().put("members", members);
		return mv;
	}
}

 

2.4 FrontControllerServletV3 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v3;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;


// 1. /front-controller/v3/members/new-form가 urlPattern에 의해서 호출하게됨 
@WebServlet(name="frontControllerServletV3", urlPatterns = "/front-controller/v3/*")
public class FrontControllerServletV3 extends HttpServlet{
	private Map<String, ControllerV3> controllerMap = new HashMap<>();
	
	public FrontControllerServletV3() {
		// 3. /front-controller/v3/members/new-form을 controllerMap에서 찾음
		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();
		
		// 2. /front-controller/v3/members/new-form을 controllerMap에서 찾음
		ControllerV3 controller = controllerMap.get(requestURI);
		if(controller == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 4. new MemberFormControllerV3() 컨트롤러에 넘겨줄 파라미터를 생성
		// paramMap을 만들어줌(컨트롤러에서 쓰기위해)
		Map<String, String> paramMap = createParamMap(request);
		
		// 5. new MemberFormControllerV3() 컨트롤러에 파라미터와 함께 process호출
		// paramMap으로 보낸 파라미터로 처리된 결과 인 model과 view로 보낼 String을 받아옴.
		ModelView mv = controller.process(paramMap);
		
		
		// 6. view 페이지로 넘길 수 있도록 논리 이름인 new-form를 가져옴
		// 논리 이름만 가져옴 new-form
		// view resolver가 다 합쳐줘야함(실제 물리 뷰 경로로 변경)
		String viewName= mv.getViewName();
		
		// 7. viewResolver로 전체 URI를 가져와서 MyView를 인스턴스화
		MyView view = viewResolver(viewName);
		
		// 8. model과 request로 setAttribute를 만들고 view 페이지로 포워딩함
		// 이전에 사용하던 render() 메서드에서 model 파라미터를 추가하는 메서드를 만들어줘야함(오버로딩)
		view.render(mv.getModel(), request, response);
	}
	
	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;
	}

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

 

2.5 MyView 객체 수정(render 메서드 오버로딩)

package hello.servlet.web.frontcontroller;

import java.io.IOException;
import java.util.Map;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

	// FrontControllerv3에서 model 객체를 처리하기 위한 메서드 오버로딩
	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);
	}
	
	// model 객체를 request의 Attribute에 담기위한 메서드
	private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request) {
		model.forEach((key, value) -> request.setAttribute(key, value));
	}
}

 

 

 

단순하고 실용적인 컨트롤러 - version 4

1. version4의 구조

리팩토링의 방향은 개발자 편의에 맞춰져있기에 큰 변화가 있지는 않고 컨트롤러에서 ModelView를 반환하는 대신 ViewName만 반환함.

 

2. 코드

2.1 ControllerV4

package hello.servlet.web.frontcontroller.v4;

import java.util.Map;

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

 반환 타입을 String으로 viewName만 그대로 반환하면 되도록 변경.

 ControllerV4 구현 컨트롤러는 따로 ModelView객체를 생성할 필요 없이 viewName만 반환해주면 됨.

 

2.2 ControllerV4 인터페이스를 구현한 컨트롤러(회원 등록, 저장, 목록)

MemberFormControllerV4 - 회원 등록 컨트롤러

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

import java.util.Map;

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

public class MemberFormControllerV4  implements ControllerV4{
	@Override
	public String process(Map<String, String> paramMap, Map<String, Object> model) {
		return ("new-form");
	}
}

MemberSaveControllerV4 - 회원 저장 컨트롤러

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

import java.util.Map;

import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.v4.ControllerV4;

public class MemberSaveControllerV4 implements ControllerV4 {
	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";
	}
}

MemberListControllerV4 - 회원 목록 컨트롤러

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

import java.util.List;
import java.util.Map;

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 hello.servlet.web.frontcontroller.v4.ControllerV4;

public class MemberListControllerV4 implements ControllerV4{
	private 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";
	}
}

 

2.3 FrontControllerServletV4 - 프론트 컨트롤러

package hello.servlet.web.frontcontroller.v4;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v4.controller.MemberFormControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberListControllerV4;
import hello.servlet.web.frontcontroller.v4.controller.MemberSaveControllerV4;


// 1. /front-controller/v4/members/new-form가 urlPattern에 의해서 호출하게됨 
@WebServlet(name="frontControllerServletV4", urlPatterns = "/front-controller/v4/*")
public class FrontControllerServletV4 extends HttpServlet{
	private Map<String, ControllerV4> controllerMap = new HashMap<>();
	
	public FrontControllerServletV4() {
		// 3. /front-controller/v4/members/new-form을 controllerMap에서 찾음
		controllerMap.put("/front-controller/v4/members/new-form", new MemberFormControllerV4());
		
		controllerMap.put("/front-controller/v4/members/save", new MemberSaveControllerV4());
		controllerMap.put("/front-controller/v4/members", new MemberListControllerV4());
	}

	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String requestURI= request.getRequestURI();
		
		// 2. /front-controller/v4/members/new-form을 controllerMap에서 찾음
		ControllerV4 controller = controllerMap.get(requestURI);
		if(controller == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 4. new MemberFormControllerV4() 컨트롤러에 넘겨줄 파라미터를 생성
		// paramMap을 만들어줌(컨트롤러에서 쓰기위해)
		Map<String, String> paramMap = createParamMap(request);
		// 컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 됨.
		Map<String, Object> model = new HashMap<>();
		
		// 5. new MemberFormControllerV4() 컨트롤러에 파라미터와 전달받을 model 참조변수 주소 값과 함께 process호출
		// paramMap으로 보내 처리된 결과인 view로 보낼 String을 받아옴.
		String viewName=controller.process(paramMap ,model);
		
		// 6. viewResolver로 전체 URI를 가져와서 MyView를 인스턴스화
		MyView view = viewResolver(viewName);
		
		// 7. model과 request로 setAttribute를 만들고 view 페이지로 포워딩함
		view.render(model, request, response);
	}
	
	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;
	}

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

 

유연한 컨트롤러 - version 5

1. 유연한 컨트롤러의 개요

ControllerV3 방식과 ControllerV4 방식을 모두 사용할 수 있는 컨트롤러를 만들려고 함.

ControllerV3 

public interface ControllerV3 {
    ModelView process(Map<String, String> paramMap);
}

ControllerV4 

public interface ControllerV4 {
    String process(Map<String, String> paramMap, Map<String, Object> model);
}

두 컨트롤러의 process메서드의 반환타입과 인자 값의 갯수와 타입이 다름.

 다른 타입의 컨트롤러를 혼용해서 쓸 수 있도록 해주는 어댑터 패턴을 활용.

 

2. version5의 구조

 이전과 비교해 달라진 것은 핸들러 어댑터와 핸들러.

핸들러 어댑터

 - 프론트 컨트롤러와 핸들러(컨트롤러) 사이에 핸들러 어댑터가 추가

 - 이 핸들러(컨트롤러)의 어댑터 역할을 하기 때문에 핸들러 어댑터.

 - 여기서 어댑터 역할을 해주는 덕분에 다양한 종류의 컨트롤러(ControllerV3, ContollerV4)를 호출할 수 있음.

※ 핸들러

 - 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경. (컨트롤러를 더 포괄적으로 부르는 명칭)

 - 어댑터를 이용해서 컨트롤러의 개념 뿐 아니라 해당하는 종류의 어댑터만 있으면 처리가 가능

 

3. 코드

3.1 MyHanderAdapter 인터페이스

package hello.servlet.web.frontcontroller.v5;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;

public interface MyHandlerAdapter {
	// 이 어댑터가 해당 컨트롤러를 처리할 수 있는지 확인 후 반환하는 메서드
	// 파라미터의 handler는 컨트롤러를 의미
	boolean supports(Object handler);
	
	// 어댑터는 실제 컨트롤러를 호출한 뒤 ModelView를 반환해야 함.
	// 실제 컨트롤러에서 ModelView를 반환하지 않는다면 어댑터에서 생성해서라도 반환해야 함.
	// 컨트롤러가 호출되는 위치가 프론트컨트롤러에서 어댑터로 변경되었다. 
	ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException; 
}

 

3.2 ControllerV3HandlerAdapter

 - MyHanderAdapter 인터페이스를 구현하는 구현체 1

 - ControllerV3를 지원하는 어댑터를 구현

package hello.servlet.web.frontcontroller.v5.adapter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

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

public class ControllerV3HandlerAdapter implements MyHandlerAdapter{
	@Override
	public boolean supports(Object handler) {
		// 핸들러(컨트롤러)가 ControllerV3의 자식이거나 구현체인지 판단해 지원여부를 반환
		// 반환 값이 true일경우 이 어댑터는 해당 핸들러를 지원 지원할 수 있다는 의미
		return (handler instanceof ControllerV3);
	}

	@Override
	public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
		// 인자값으로 받은 핸들러는 Object타입이기에 ControllerV3 타입으로 형변환을 해줘야 함
		// (위의 support 메서드를 통해 ControllerV3의 구현체인것을 확인했으니 형변환시 문제없이 동작)
		ControllerV3 controller =(ControllerV3) handler;
		
		Map<String, String> paramMap = createParamMap(request);
		
		// V3버전은 반환타입이 ModelView이기에 바로 로직 수행 후 반환하면 됨.
		ModelView mv = controller.process(paramMap);
		return mv;
	}
	
	// 파라미터를 받아오는 
	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;
	}
}

 

3.3 FrontControllerServletV5 - 프론트 컨트롤러 (ControllerV3HandlerAdapter와 V3Handler들(컨트롤러들)먼저 적용)

package hello.servlet.web.frontcontroller.v5;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.v3.controller.MemberFormControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberListControllerV3;
import hello.servlet.web.frontcontroller.v3.controller.MemberSaveControllerV3;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;

@WebServlet(name="frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
	// 기존 frontControllerServlet
	//private Map<String, ControllerV3> controllerMap = new HashMap<>();
	
	// 핸들러를 담는 맵의 value type이 Object인 이유는 담길 핸들러가 V3, V4중 어느 핸들러 인터페이스일지 모르기 때문
	private final Map<String, Object> handlerMappingMap = new HashMap<>();
	
	// 핸들러 어댑터(ControllerV3HandlerAdapter, ControllerV4HandlerAdapter)들을 담아둘 리스트 컬랙션 객체
	private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
	
	public FrontControllerServletV5() {
		initHandlerMappingMap();
		initHandlerAdapters();
	}

	// urlPattern에 매핑되는 컨트롤러를 초기화해주는 메서드
	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());
	}

	// 사용할 어댑터를 리스트 콜렉션에 추가하는 작업을 하는 메서드
	private void initHandlerAdapters() {
		handlerAdapters.add(new ControllerV3HandlerAdapter());
	}
	
	@Override
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// 초기화한 핸들러 매핑 정보에서 URL을 key로 매핑된 핸들러(컨트롤러)를 찾아 반환
		Object handler = getHandler(request);
		
		if(handler == null) {
			response.setStatus(HttpServletResponse.SC_NOT_FOUND);
			return;
		}
		
		// 핸들러를 처리 할 수 있는 어댑터 조회
		MyHandlerAdapter adapter = getHandlerAdapter(handler);		
		
		// 어댑터의 handle 메서드를 통해 실제 어댑터가 구현한 로직이 수행
		// 어댑터는 handler(컨트롤러)를 호출하고 handler는 로직에서 자신이 지원하는 핸들러(ControllerV3, ControllerV4)로 형변환해서 로직 수행 후 ModelView로 리턴
		ModelView mv = adapter.handle(request, response, handler);
		
		String viewName= mv.getViewName();
		MyView view = viewResolver(viewName);
		
		view.render(mv.getModel(), request, response);
	}

	private Object getHandler(HttpServletRequest request) {
		String requestURI= request.getRequestURI();
		return handlerMappingMap.get(requestURI);
	}

	private MyHandlerAdapter getHandlerAdapter(Object handler) {
		// handlerAdapters 리스트 콜렉션에 등록된 어댑터를 순회하며 해당 핸들러가 지원되는지 확인하기위한 루프
		for(MyHandlerAdapter adapter : handlerAdapters) {
			// 지원하는 어댑터가 있다면 그 어댑터 반환
			if(adapter.supports(handler)) {
				return adapter;
			}
		}
		// 지원하는 어댑터가 없으면 Exception
		throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler="+ handler);
	}

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

3.4 ControllerV4HandlerAdapter

 - MyHanderAdapter 인터페이스를 구현하는 구현체 2

 - ControllerV4를 지원하는 어댑터를 구현

package hello.servlet.web.frontcontroller.v5.adapter;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.v3.ControllerV3;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;

public class ControllerV4HandlerAdapter implements MyHandlerAdapter{

	@Override
	public boolean supports(Object handler) {
		// handler가 ControllerV4인지 확인
		return (handler instanceof ControllerV4);
	}

	@Override
	public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
		ControllerV4 controller = (ControllerV4) handler;
		
		Map<String, String> paramMap = createParamMap(request);
		HashMap<String, Object> model = new HashMap<>();
		
		// ControllerV4는 반환타입이 String으로 ModelView가 아니라 viewName만 반환
		// 그래서 이 어댑터에서 이를 ModelView로 만들어줘서 어댑터 반환타입을 일치시켜줘야함
		String viewName = controller.process(paramMap,model);
		
		ModelView mv = new ModelView(viewName);
		mv.setModel(model);
		
		return mv;
	}

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

 

3.5 FrontControllerServletV5 - 프론트 컨트롤러 (ControllerV4HandlerAdapter와 V4Handler들(컨트롤러들)추가 적용)

	// urlPattern에 매핑되는 컨트롤러를 초기화해주는 메서드
	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());
		
		// V4Handler들(컨트롤러들)추가
		// ControllerV4용 경로(key)와 컨트롤러를 handlerMappingMap에 추가
		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 void initHandlerAdapters() {
		handlerAdapters.add(new ControllerV3HandlerAdapter());
		// ControllerV4도 호환이 되도록 해주는 어댑터를 handlerAdapter 콜렉션에 추가
		handlerAdapters.add(new ControllerV4HandlerAdapter()); // ControllerV4HandlerAdapter 추가
	}

 

컨트롤러 version 정리

 version1: 프론트 컨트롤러를 도입

 version2: View 분리, 단순 반복 되는 뷰 로직 분리

 version3: Model 추가, 컨트롤러의 서블릿 종속성 제거, 뷰 이름 중복 제거

 version4: 단순하고 실용적인 컨트롤러 v3와 거의 비슷, 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 View 논리 이름을 String으로 반환

 version5: 유연한 컨트롤러 어댑터 도입, 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계(controllerV3, controllerV4 어댑터 구현)

반응형

+ Recent posts