https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
위 강의를 들으며 정리한 글입니다.
프론트 컨트롤러 패턴
모든 요청을 받는 컨트롤러를 하나 둬서 이 컨트롤러가 각 컨트롤러에게 요청을 전달하도록하는 패턴이다.
기존 패턴은 서블릿 매핑으로 각 컨트롤러가 직접 호출됐지만 프론트 컨트롤러를 사용하면 모든 사용자의 요청들은 프론트 컨트롤러를 거쳐가게 된다.
프론트 컨트롤러 패턴의 특징
- 프론트 컨트롤러 서블릿 하나로 모든 요청을 받음
- 프론트 컨트롤러가 알아서 적절한 컨트롤러를 찾아 호출함
- 공통 처리가 쉬워진다.
- 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
✍ 스프링의 Dispatcher Servlet이 프론트 컨트롤러 패턴이다.
MVC 프레임워크 만들기
MVC 프레임 워크를 만들어보며 스프링 MVC를 이해해본다.
1) version 1
버전1에서는 프론트 컨트롤러를 도입해본다.
@WebServlet(name="findControllerV1", urlPatterns = "/v1/*")
public class FrontControllerServ1 extends HttpServlet {
private Map<String , ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServ1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberForm1());
controllerMap.put("/front-controller/v1/members/save", new MemberSave1());
controllerMap.put("/front-controller/v1/members", new MemberList1());
}
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
이렇게 하기 위해서 우선 다형성을 위해 ControllerV1이라는 인터페이스를 만들고 MemberForm1, MemberSave1, MemberList1 컨트롤러들은 모두 이 인터페이스의 구현체로 둔다. 그러면 위와같이 다형성을 활용해서 컨트롤러들을 Map 자료형으로 관리할 수 있게된다.
컨트롤러 중 하나의 코드는 다음과 같다(셋이 비슷하다)
public class MemberForm1 implements ControllerV1 {
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dis = request.getRequestDispatcher(viewPath);
dis.forward(request, response);
}
}
버전 1의 구조는 위와같다. FrontControllerServ1 서블릿의 url 패턴을 v1/* 로 했으므로 v1으로 시작하는 모든 요청은 이 FrontControllerServ1에 오게되어서 프론트 컨트롤러의 역할을 하게 된다. 요청이 오면 요청에 맞는 컨트롤러를 매핑정보로 찾아내서 해당 컨트롤러의 로직을 실행하도록 한다.
2) version 2
버전 1에서의 흐름
프론트 컨트롤러 ▶ 요청에 맞는 컨트롤러 찾아서 호출 ▶ 컨트롤러가 뷰(JSP) forward
버전 2에서의 흐름
프론트 컨트롤러 ▶ 컨트롤러 호출, 컨트롤러는 MyView 반환 ▶ 프론트 컨트롤러로 다시 돌아와서 ▶ 전달받은 MyView로직 호출 ▶ MyView 가 JSP로 forward
public class MyView {
private String viewPath;
public MyView(String viewPath) {
this.viewPath = viewPath;
}
public void render(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
RequestDispatcher dis = req.getRequestDispatcher(viewPath);
dis.forward(req, resp);
}
}
MyView의 코드이다. 각 컨트롤러에서 MyView를 생성하고 각자의 뷰 경로를 넣어주고 반환하면 프론트 컨트롤러가 이 MyView를 받아서 render 함수를 호출해주는 식이다.
public class MemberForm1 implements ControllerV1 {
@Override
public MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//String viewPath = "/WEB-INF/views/new-form.jsp";
//RequestDispatcher dis = request.getRequestDispatcher(viewPath);
//dis.forward(request, response);
//이랬던 코드가 아래같이 단순해진다.
return new MyView("/WEB-INF/views/new-form.jsp");
}
}
v1에서는 3줄씩 있던 코드가 이제는 한줄이다. 엄청 간결해진 것을 볼 수 있다. 프론트 컨트롤러의 코드는 2줄이 추가됐을 것이다. 컨트롤러로부터 MyView를 반환받아 render() 함수를 호출하는 코드.
fv그렇다면 왜 각 컨트롤러에서 MyView의 render를 호출하지 않는가?
👉 MyView의 render를 호출하는 것은 공통 로직으로 프론트 컨트롤러가 이를 일관되게 처리하도록 한다. 그러므로 인터페이스에 따라 각 컨트롤러는 MyView를 생성해서 반환하면 된다는 것을 지키기만 하면 된다.
3) version 3
버전3에서는 각 컨트롤러가 서블릿과 관련된 기술을 몰라도 동작할 수 있도록 바꿀 것이다. HttpServletRequest와 HttpServletResponse를 인자로 주는 것이 아닌 Map을 넘겨줄 것이다. 그리고 뷰의 이름 중복을 제거해본다.
public interface ControllerV3 {
ModelView process(Map<String, String> param);
}
각 컨트롤러의 인터페이스는 이렇게 바뀌었다. 더이상 서블릿 기술에 종속적이지 않다.
@Getter @Setter
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
public ModelView(String v) {
viewName = v;
}
}
ModelView는 데이터를 담아줄 모델을 가지고 있으면서 view 이름도 가지고 있는다.
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
ControllerV3 controller = controllerMap.get(requestURI);
if (controller == null){
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String ,String> param = createParam(req);
ModelView mv = controller.process(param);
// view Resolver
String viewName = mv.getViewName();
MyView myView = viewResolver(viewName);
myView.render(mv.getModel(), req, resp);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
모델뷰의 역할은 결국 request객체에서 파라미터들을 Map으로 싸들고 컨트롤러에 들어가서 논리이름을 들고 나오는 역할이다.
위처럼 viewResolver 함수에서 뷰의 이름을 설정해줌으로써 변경지점을 하나로 둘 수 있다. 만약 폴더 위치가 바뀌면 기존에는 일일이 컨트롤러들 찾아다니며 뷰 이름 수정해줬어야하는데 이제는 이 viewResolver 함수만 수정하면 된다.
httpServletRequest에 담긴 파라미터들은 프론트 컨트롤러에서 풀어서 Map에 담고 각 컨트롤러를 거쳐나온 뒤에 MyView의 render함수에서 다시 request 객체에 담기게 된다. MyView에 추가된 코드는 아래와 같다.
public void render(Map<String, Object> model, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
model.forEach((key, value) -> req.setAttribute(key, value));
RequestDispatcher dis = req.getRequestDispatcher(viewPath);
dis.forward(req, resp);
}
4) version 4
버전3은 각 요청마다 ModelView를 생성하고 반환하는 것이 좀 번거롭다. 이를 더 단순화 시켜보자.
버전4는 구조는 버전3과 똑같으나 ModelView를 사용하지 않는다는 점이 다르다.
public class MemberList4 implements ControllerV4 {
private MemberRepository mr = MemberRepository.getInstance();
@Override
public String process(Map<String, String> param, Map<String, Object> model) {
List<Member> all = mr.findAll();
model.put("members", all);
return "members";
}
}
컨트롤러의 코드는 이렇게 바뀐다. ModelView를 사용하지 않으니 코드가 많이 단순해졌다. model은 프론트 컨트롤러에서 전달받은 것으로 사용한다. 이렇게 되면 프론트 컨트롤러의 코드는 다음과 같이 바뀐다.
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
ControllerV4 controller = controllerMap.get(requestURI);
if (controller == null){
resp.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
Map<String ,String> param = createParam(req);
Map<String ,Object> model = new HashMap<>();
String viewName = controller.process(param, model);
MyView myView = viewResolver(viewName);
myView.render(model, req, resp);
}
ModelView의 Map 대신에 프론트 컨트롤러가 Map을 만들어서 컨트롤러에게 전달한다. 결국 param과 model 변수를 사용하는 이유는 각 컨트롤러가 서블릿 기술을 몰라도 동작하도록, 단순하게 Map 컬렉션 두개 받아서 동작하도록 하기 위한 것이다.
5) version5
버전5에서는 버전3과 버전4에서 만든 다양한 형식의 컨트롤러들을 사용할 수 있게 바꾼다. 이를 위해 어댑터 패턴을 사용할 것이다. 구조는 다음과 같이 변한다.
핸들러는 컨트롤러를 포함하는 개념이다. 어댑터를 사용하면 컨트롤러 외에 다른 것들도 처리할 수 있기 때문이다.
1. 핸들러(컨트롤러) 매핑정보에서 어떤 컨트롤러인지 찾아낸다.
2, 3. 찾은 컨트롤러를 처리해줄 수 있는 어댑터를 어댑터 목록에서 찾는다.
4. 해당 어댑터가 핸들러(컨트롤러)의 process 함수를 호출하여 받은 다양한 형식의 반환값들을 통일화해서 프론트 컨트롤러에게 전달해준다.
@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 adapter = getAdapter(handler);
ModelView mv = adapter.handle(req, resp, handler);
String viewName = mv.getViewName();
MyView myView = viewResolver(viewName);
myView.render(mv.getModel(), req, resp);
}
private MyHandlerAdapter getAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter 찾을 수 없음");
}
private Object getHandler(HttpServletRequest req) {
String requestURI = req.getRequestURI();
return handlerMapping.get(requestURI);
}
프론트 컨트롤러의 코드이다. getHandler 함수는 Map 객체에서 컨트롤러를 꺼내오는 것이다. v3의 MemberList인지 v4의 MemberSave인지 구체적인 컨트롤러를 찾아오는 것이다. 그리고 어댑터 목록에서 이 컨트롤러를 처리해줄 수 있는 어댑터를 찾아온다. 각 어댑터는 supports 함수를 가지고 있는데 코드는 다음과 같다.
public class V3adapter implements MyHandlerAdapter {
@Override
public boolean supports(Object hand) {
return (hand instanceof ControllerV3);
}
@Override
public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
ControllerV3 controller = (ControllerV3)handler;
Map<String, String> param = createParam(req);
return controller.process(param);
}
public Map<String ,String> createParam(HttpServletRequest req) {
Map<String ,String> param = new HashMap<>();
req.getParameterNames().asIterator().forEachRemaining(paramName -> param.put(paramName, req.getParameter(paramName)));
return param;
}
}
supports 함수는 그냥 해당 객체의 인스턴스인지만 확인해주는 함수이다. 어댑터를 찾게되면 프론트 컨트롤러에서 어댑터의 handle 함수를 호출한다. 어댑터의 handle 함수는 MyHandlerAdapter의 구현체이니 똑같은 반환값을 가진다.
여기까지는 V3Adapter였고 버전 4의 컨트롤러들을 처리해주기 위한 V4Adapter는 어떻게 생겼을까. 버전4에서는 ModelView를 없앴었다. 그래서 컨트롤러의 반환값은 String이므로 ModelView를 반환해야하는 V5에는 맞지않는다. 이때 V4Adapter가 활약하는 순간이다.
@Override
public ModelView handle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4)handler;
Map<String, String> param = createParam(req);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(param, model);
ModelView modelView = new ModelView(viewName);
modelView.setModel(model);
return modelView;
}
String 타입인 viewName를 받아와서 ModelView를 만들어서 반환해준다. 어댑터의 역할을 하는 것이다.
스프링 MVC가 이런 식으로 되어있다고 한다. 인터페이스가 다 정의되어있다. 만약 지금까지 만든 MVC에서 애노테이션을 사용한 컨트롤러를 넣고 싶으면 그냥 애노테이션을 지원하는 어댑터를 추가하기만하면 된다. 확장성에 열려있는 것이다.
Tip.
구조를 바꿀때에는 큰 구조적인 부분을 개선하고 그 다음에 세부적인 부분을 변경해라!
'백엔드 > 스프링' 카테고리의 다른 글
스프링MVC 요청 매핑 (0) | 2021.08.18 |
---|---|
스프링MVC의 Handler, View Resolver (0) | 2021.08.18 |
Http Request 데이터 형식 정리 (0) | 2021.08.17 |
스프링 빈 스코프 (0) | 2021.08.13 |
스프링 빈 등록 총정리 (0) | 2021.08.13 |