김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
Front Controller가 모든 클라이언트의 요청을 받고, 공통 로직을 처리
Front Controller 서블릿 하나로 클라이언트의 요청을 받음
Front Controller가 요청에 맞는 컨트롤러를 찾아서 호출
Front Controller를 제외한 나머지 Controller는 서블릿을 사용하지 않아도 된다
스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있음
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
// 매핑 정보
private Map<String, ControllerV1> controllerMap = new HashMap<>();
// 요청 URL에 따른 Controller 매핑
public FrontControllerServletV1() {
controllerMap.put("/front-controller/v1/members/new-form", new MemberFormControllerV1());
...
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
// URI에 해당하는 Controller 찾기
ControllerV1 controller = controllerMap.get(requestURI);
...
// Controller 호출
controller.process(request, response);
}
}
HttpServlet
을 상속받음
@WebServlet
을 통해 모든 경로에 대한 요청을 받음
Map<String, ControllerV1>
을 이용해 경로에 따른 Controller 매핑
key = URL
value = Controller 생성
Controller의 process()
를 호출
Map에서 요청 URL에 맞는 Controller 구현체를 인터페이스로 받는다 ( 다형성 )
process()
호출 시 요청 파라미터 정보가 필요한 Controller가 있기 때문에 request, response 객체를 넘겨준다
public class MemberSaveControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// reqest의 요청 파라미터 꺼내기
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
memberRepository.save(member);
// request에 모델 데이터 저장
request.setAttribute("member", member);
// JSP 실행
String viewPath = "/WEB-INF/views/save-result.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
}
Controller 객체는 모두 동일한 Controller 인터페이스 ( ControllerV1 )를 구현
Controller 인터페이스의 process()
는 FrontController의 service()
와 유사한 형태로 작성
FrontController를 통해 전달받은 request 객체의 요청 파라미터 정보를 뽑아서 사용
request 객체의 임시 저장소를 Model 처럼 활용한다
forward()
를 통해 JSP를 실행
전달받은 request, response 객체를 함께 전달한다
request의 임시 저장소에 저장한 model 정보 조회를 위해 전달
response에는 클라이언트에게 제공될 view가 담기게 된다
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
Controller에서 View로 이동하기 위해 위의 코드를 사용
모든 Controller에 중복으로 작성됨
➡️ View를 처리하는 객체를 만들어서 해결
Controller ( ControllerV2 )
직접 forward()
하지 않고 MyView 객체를 만들어서 반환
MyView 객체를 생성하면서 viewPath를 넘겨준다
생성된 MyView 객체를 반환한다
Front Controller
Controller의 process()
실행 결과로 MyView 객체를 받는다
반환된 MyView 객체의 render()
를 호출
MyView
viewPath 필드를 가짐 ( JSP 파일의 위치 )
생성자를 통해 viewPath를 전달받음
render()
메서드에서 forward()
를 담당
Servlet 종속성 제거 ( Servlet 기술을 사용하지 않도록 )
요청 파라미터 정보가 필요한 Controller가 있어서 FrontController에서 Controller를 호출할 때 request, response 객체를 넘주었는데 모든 Controller가 요청 파라미터 정보가 필요한 것은 아님
➡️ FrontController가 request에서 요청 파라미터 정보를 뽑아 Map 객체를 만들고 Controller 호출 시 Map 객체를 전달
Controller에서 Model에 담을 정보를 request 객체의 임시 저장소에 저장했는데 request 객체를 전달받지 않음
➡️ 별도의 Model 객체를 만들어서 반환
View 이름 중복 제거
Controller에서 MyView 객체를 생성할 때, view의 경로를 전부 작성해주어야 했음
➡️ Controller에서 View의 논리 이름 ( 경로를 제외한 파일 이름 )을 반환하고
➡️ FrontController에서 실제 경로(위치)를 처리하도록 변경
ModelView
view 페이지의 논리 이름 + model ( Map 객체 )을 가짐
model을 만들고 view 이름까지 전달하는 객체
FrontController
request에 담긴 요청 파라미터 정보들을 Map 객체에 저장
Controller 호출 시 Map 객체를 전달
Controller ( ControllerV3 )
전달받은 Map 객체 ( 요청 파라미터 정보 )를 이용하여 동작 수행
ModelView 객체 생성 및 반환
view 페이지의 논리 이름 설정
model ( Map 객체 )에 정보를 삽입
FrontController
Controller에서 반환된 ModelView가 가진 논리 이름을 viewResolver()
로 전달
viewResolver()
논리이름을 물리이름으로 변환
변환된 물리 이름으로 MyView 객체 생성 및 반환
MyView의 render()
호출
MyView
전달된 model ( Map 객체 )에 담긴 데이터를 request의 데이터 저장소에 저장
request.getAttribute()
를 사용해 데이터를 조회하기 때문forward()
실행
FrontController
request에 담긴 정보들을 Map 객체로 복사
Controller가 model을 담을 수 있도록 Map 객체 생성 ( 빈 객체 )
Controller를 호출하면서 model을 담을 Map 객체를 함께 전달
Controller ( ControllerV4 )
ModelView를 반환하지 않고 viewName만 반환하도록 변경
전달받은 Map 객체에 put()
을 이용하여 model 정보 삽입
FrontController
Controller에서 반환된 view 페이지 논리 이름을 viewResolver()
를 통해 물리 이름으로 변환하면서 MyView 객체 생성
MyView의 render()
호출
public class FrontControllerServletV4 extends HttpServlet {
private Map<String, ControllerV4> controllerMap = new HashMap<>();
}
FrontController를 보면 URL에 따른 Controller를 매핑 해주기 위해 위의 ControllerV4 처럼 특정 Controller 인터페이스를 지정했음
즉, 한 가지 방식의 인터페이스만 사용 가능했음
한 가지 방식의 Controller 인터페이스만 사용 가능한 것이 아니라 다른 Controller 인터페이스도 사용 가능하도록 변경하기 위해
➡️ 어댑터 패턴을 이용
핸들러 매핑 정보를 보고 핸들러 조회
핸들러 = Controller 라고 생각
어떤 핸들러(Controller)인지 확인
핸들러 어댑터 목록에서 처리할 수 있는 어댑터 조회
조회된 핸들러(Controller)를 처리할 수 있는 어댑터를 가져온다
ex> 핸들러가 ControllerV4이면 ControllerV4를 처리할 수 있는 어댑터를 가져온다
FrontController는 핸들러 어댑터 호출
이전까지는 FrontController가 바로 핸들러(Controller)를 호출했지만 핸들러 어댑터를 통해 핸들러(Controller)호출
FrontController가 핸들러 어댑터를 호출하면서 핸들러(Controller)를 넘겨준다
핸들러 어댑터
중간에서 어댑터 역할을 함
핸들러 어댑터가 인터페이스로 존재하고 핸들러마다 핸들러 어댑터를 구현
핸들러마다 핸들러 어댑터를 구현할 수 있기 때문에 다양한 종류의 컨트롤러를 호출할 수 있다 ( 처리할 수 있다 )
핸들러
public interface MyHandlerAdapter {
// 1. 어댑터가 전달된 컨트롤러를 처리할 수 있는지 판단하는 메소드
boolean supports(Object handler);
// 2. Controller를 호출하고 결과를 받는 메서드
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
처리할 핸들러( Controller )마다 핸들러 어댑터 인터페이스를 구현
handle()
은 핸들러( Controller )를 호출하고, 결과를 ModelView로 받는다
반환형이 ModelView이기 때문에 위의 인터페이스를 구현한 핸들러 어댑터가 직접 ModelView 객체를 생성해서라도 반환형을 맞춰야함
public class ControllerV4HandlerAdapter implements MyHandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
ControllerV4 controller = (ControllerV4) handler;
// 요청 파라미터 꺼내서 Map 객체 생성
Map<String, String> paramMap = createParamMap(request);
// model 정보를 담을 Map 객체 생성 ( 빈 Map 객체 )
HashMap<String, Object> model = new HashMap<>();
// 핸들러 ( Controller ) 호출
String viewName = controller.process(paramMap, model);
// 반환형을 맞춰주기 위해 ModelView를 생성
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
}
기존 FrontController가 하던 일을 핸들러 어댑터가 처리
url에 맞는 Controller 호출 및 render()
호출 제외
제외된 기능은 FrontController에서 계속 수행
@WebServlet(name = "frontControllerServletV5", urlPatterns = "/front-controller/v5/*")
public class FrontControllerServletV5 extends HttpServlet {
// 핸들러 매핑 정보
// 모든 Controller를 받을 수 있어야 하기 때문에 특정 컨트롤러 인터페이스가 아닌 Object
private final Map<String, Object> handlerMappingMap = new HashMap<>();
// 핸들러 어댑터 목록
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();
public FrontControllerServletV5() {
initHandlerMappingMap();
initHandlerAdapters();
}
// 핸들러 매핑 정보 초기화
private void initHandlerMappingMap() {
handlerMappingMap.put("/front-controller/v5/v3/members/new-form", new MemberFormControllerV3());
handlerMappingMap.put("/front-controller/v5/v4/members/new-form", new MemberFormControllerV4());
...
}
// 핸들러 어댑터 목록 초기화
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerV3HandlerAdapter());
handlerAdapters.add(new ControllerV4HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 핸들러 찾기
Object handler = getHandler(request);
...
// 핸들러 어댑터 찾기 ( 찾아진 Controller가 처리할 수 있는 Controller인지 확인 )
MyHandlerAdapter adapter = getHandlerAdapter(handler);
// 어댑터의 handle을 호출
// 어댑터의 handle은 Controller를 호출해 process()를 수행하도록 함
// 수행 결과로 ModelView가 반환됨
ModelView mv = adapter.handle(request, response, handler);
// view 페이지 논리 이름 -> 물리 이름
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
// modelView의 model( Map 객체 )을 전달하면서 view 페이지 렌더링
view.render(mv.getModel(), request, response);
}
// 요청 url을 보고 어떤 Controller인지 조회 및 Object 형식으로 반환
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
// 핸들러 어댑터 목록을 반복하면서
// 처리할 수 있는 Controller이면 핸들러 어댑터 반환
private MyHandlerAdapter getHandlerAdapter(Object handler) {
for (MyHandlerAdapter adapter : handlerAdapters) {
if(adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
}
private MyView viewResolver(String viewName) {
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
url에 맞는 Controller 호출은 이제 하지 않음 ( 기존의 FrontController가 하던 일 )
url에 맞는 핸들러 찾기
찾아진 핸들러를 처리할 수 있는 핸들러 어댑터가 있는지 확인
핸들러 어댑터 호출
반환된 ModelView의 render()
호출
FrontController는 결과적으로 일치하는 핸들러 어댑터를 찾아서 호출하는 역할