본 게시글은 김영한 님의 '스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술' 강의를 구매 후 정리하기 위한 포스팅입니다.
프론트 컨트롤러
- 그림과 같이 공통인 부분을 모아서 그것을 통해 접근하는 방식이 프론트 컨트롤러
- 프론트 컨트롤러도 Servlet이다.
- 프론트 컨트롤러와 서블릿 요청을 다 받아서 프론트 컨트롤러에서 공통으로 처리해야 하는 부분을 처리하고 요청이 어느 컨트롤러에 필요한지 확인하고 해당 컨트롤러를 호출한다.
- 공통 처리가 가능하다.
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
=> 프론트 컨트롤러가 요청에 대한 처리를 대신 해주기 때문.
스프링 웹 MVC의 핵심도 바로 FrontController이다.
스프링 웹 MVC의 DispatcherServlet이 FrontController 패턴으로 구현되어 있다.
Controller v1
- 클라이언트가 HTTP요청을 하면 프론트 컨트롤러(서블릿)가 요청을 받는다.
- 프론트 컨트롤러가 매핑 정보에서 url 요청에 맞는 컨트롤러를 조회하여 해당 컨트롤러를 호출한다.
(매핑 정보 : url 정보와 컨트롤러 정보를 매핑한 정보. 즉, 요청에 해당하는 컨트롤러 정보를 가짐.) - 컨트롤러는 자기 로직을 수행하고 forward 로직으로 JSP를 호출해서 응답을 한다.
구현 방식
서블릿과 비슷한 모양의 컨트롤러 인터페이스를 도입한다. 각 컨트롤러들은 이 인터페이스를 구현하면 된다. 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
- 실제 url 요청을 받아 동작하는 컨트롤러(Member~Controller)들은 Controller(인터페이스)를 상속받아서 구현되어 있다.
프론트 컨트롤러 예시
/**
* /front-controller/v1/* :: v1하위의 어떤 url이 들어와도 이 프론트 컨트롤러(서블릿)에 무조건 호출된다.
* 프론트 컨트롤러
*/
@WebServlet(name = "FrontControllerServletV1", urlPatterns = "/front-controller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
//매핑 정보 생성 Map<url, 컨트롤러>
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
//컨트롤러들과 url 경로를 맵에 담음.
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");
//requestURI로 클라이언트로부터 url을 얻어 맵에 있는 컨트롤러를 호출
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
//컨트롤러가 존재하지 않을때 처리
if (controller == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND); // 응답코드 404를 띄움.
return;
}
//조회 성공
controller.process(request,response); //오버라이드 된 process 메서드가 실행된다.
}
- 프론트 컨트롤러는 각 url과 해당 컨트롤러들을 매핑한 매핑 정보를 가지고 있다.
- 따라서, 매핑 정보를 바탕으로 프론트 컨트롤러는 url 요청을 받으면 해당 컨트롤러에게 연결해주는 역할을 한다.
View 분리 - v2
v2 구조
- 컨트롤러가 더 이상 jsp 포워드에 대해서 생각하지 않아도 된다.
- 단순히 컨트롤러는 myView를 생성해서 호출하기만 하고 실제 뷰를 생성하는건 myView에서 이루어진다.
(컨트롤러에서 뷰를 분리.)
v2 구조를 사용하게 되면 뷰로 이동하는 부분에 발생한 중복을 제거할 수 있다.
//아래 코드를 분리
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request,response);
=> 해당 코드들은 공통으로 MyView의 render()메서드로 묶음.
//MyView
private String viewPath; //뷰 경로 주소 생성
//뷰로 이동시키는 코드 (getRequestDistpatcher/ forward(requsest, reponse) => redering(렌더링)
public void render(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException{
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
- V2 구조를 적용하였을 때 중복을 제거하여 훨씬 코드가 간결해진 것을 볼 수 있다.
- 브라우저에서 url요청이 들어오면 프론트 컨트롤러에서 요청 url을 매핑 정보에서 찾아서 해당 컨트롤러를 호출한다.
- 컨트롤러에서는 url 요청에 따른 해당 화면을 출력해주는 뷰 경로 (ViewPath)를 반환하여 프론트 컨트롤러에 넘긴다.
- 프론트 컨트롤러에서 받은 뷰 경로를 가지고 뷰를 렌더링해주는 MyView의 render()메서드를 호출한다.
- 뷰 경로를 통해 jsp를 응답하여 화면을 출력한다.
Model 추가 - v3
서블릿 종속성 제거
컨트롤러에 있는 요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 컨트롤러가 서블릿 기술을 몰라도 동작할 수있다.
그리고 request 객체를 Model로 사용하는 대신에 별도의 Model 객체를 만들어서 반환하면 된다.
서블릿 종속성을 제거하면 request가 존재하지 않는다. (request.setAtrribute()를 사용할 수 없다.)
=> 따라서 Model을 직접 생성해서 데이터를 전달해야 한다.
뷰 경로 이름의 중복 제거
컨트롤러에 있는 뷰 경로 주소들을 보면 공통적으로 작성되는 부분이 있다.
중복 되는 부분 : "/WEB-INF/views/파일명.jsp"
따라서, 이러한 중복되는 부분을 제거하기 위해 공통적인 물리 위치는 프론트컨트롤러에서 처리하고, 컨트롤러에서는 뷰의 파일명만 반환하게끔 작성하면 된다.
예) 컨트롤러는 전체 경로가 아닌 파일명만 반환하면 된다.
- /WEB-INF/views/new-form.jsp => new-form
- /WEB-INF/views/save-result.jsp => save-result
- /WEB-INF/views/members.jsp => members
/*프론트 컨트롤러*/
ModelView mv = controller.process(paramMap); // 요청 컨트롤러 호출 후 처리 진행.
String viewName = mv.getViewName(); // 반환된 논리 뷰 이름
// ViewResolver 기능 수행 : 논리 이름을 물리이름으로 변환하여 실제 myView를 반환한다.
private static MyView viewResolver(String viewName) {
//컨트롤러에서 처리 결과로 뷰이름(논리)을 받으면 물리 경로로 반환한다.
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
=> 이런 방식으로 작성하면, 뷰 폴더의 위치가 변경되어도 프론트 컨트롤러의 물리적 위치만 수정하면 된다.
v3 구조
- 컨트롤러는 뷰 파일이름에 대한 반환만 이루어진다. (실제 물리 이름으로 변경하는 것이 필요하다.)
- 이를 처리해주는 것이 viewResolver이고 viewResolver는 MyView를 반환한다.
- 그 이후에 렌더링이 진행된다.
ModelAndView (ModelView)
컨트롤러에서 처리한 결과 데이터(model에 저장)와 뷰정보(논리적 뷰 이름- ViewName )를 담아서 반환하는 역할을 한다.
따라서, ModelView는 model과 viewName을 가지고 있다.
- 데이터 전달(Model)
- 컨트롤러에서 처리한 결과 데이터를 모델에 담아서 뷰로 전달한다. 모델은 뷰에서 필요한 데이터를 담고 있으며, 뷰는 이 데이터를 사용해서 동적으로 생성한다.
- value 값을 Object 타입으로 받음으로써 처리된 값이 어떤 값이든 담아낼 수 있다.
- 뷰 식별(ViewName)
- 뷰 이름을 설정하여 클라언트에게 보여줄 뷰를 결정한다.
- 뷰 이름은 실제 뷰 리졸버에 의해 뷰 이름을 기반으로 실제 뷰의 경로를 찾아준다.
단순하고 실용적인 컨트롤러 - v4
앞서 만든 v3 컨트롤러는 서블릿 종속성을 제거하고 뷰 경로의 중복을 제거하는 등, 잘 설계된 컨트롤러이다. 그런데 실제 컨트톨러 인터페이스를 구현하는 개발자 입장에서 보면, 항상 ModelView 객체를 생성하고 반환해야 하는 부분이 조금은 번거로운 문제가 있다.
v4 구조
기본적인 틀은 v3와 동일하지만, 컨트롤러가 ModelView를 반환하는 것이 아닌 ViewName 즉, 뷰 이름만을 반환한다.
이전 v3구조에서 ModelView를 만들지 않아도 되어서 개발자 입장에서 훨씬 단순한 코드로 구현이 가능해진다.
- 구조에서 보다시피 v3 구조에서 ModelView를 반환한 것에서 ViewName을 반환하는 것으로 밖에 바뀐 것이 없다.
Model 객체 전달
프론트 컨트롤러에서 Model 객체를 생성하고 이를 url 요청이 들어온 해당 컨트롤러가 전달받아 호출된다.
- 프론트컨트롤러
코드에서 보다시피 ModelView를 반환하는 것이 아닌 ViewName을 반환한다.
컨트롤러에서 모델 객체에 요청을 처리한 값을 담으면 모델 객체에 그대로 담겨있게 된다.
- 요청에 따른 컨트롤러
- 모델이 프론트 컨트롤러로부터 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.
- 보다시피 컨트롤러에서 요청을 처리한 값을 모델에 담고 ViewName을 반환함으로써 코드가 간결해진 것을 볼 수 있다.
프레임워크나 공통 기능(즉, 프론트 컨트롤러와 같이)이 수고로워야 사용하는 개발자가 편리해진다.
유연한 컨트롤러 - v5
앞서 작성한 프론트 컨트롤러 구조들은 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다. 하지만 만약, 개발자마다 다른 구조 즉, 어떤 개발자는 v3를 사용하고 어떤 개발자는 v4를 사용한다고 하면 위와 같은 구조의 프론트 컨트롤러는 많은 부분을 수정해야할 것이다.
=> v5 구조는 이렇게 다양한 컨트롤러 구조를 유연하게 사용할 수 있도록 어댑터 패턴을 적용하여 구현된 방식이다.
어댑터 패턴
서로 다른 인터페이스를 가진 여러 클래스들을 자신이 원하는 클래스로 동작할 수 있게끔 도와주는 패턴이다. 어댑터 패턴은 새로운 클래스가 기존의 인터페이스를 변경하지 않고 기능을 확장할 수 있게끔 도와준다.
예를 들어, v3 컨트롤러와 v4 컨트롤러는 서로 다른 인터페이스로 구현되어 있지만 이를 모두 호환하게끔 하는 어댑터 클래스를 생성하여 유연하게 사용할 수 있다. 마치 220v를 사용하는 콘센트에 110v 전기를 사용할 때 어댑터에 연결하여 사용할 수 있듯이 하는 것이다.
이를 어댑터 패턴을 사용해서 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경할 것이다.
v5 구조
- 핸들러 : 컨트롤러보다 더 넓은 범위의 개념. (즉, 컨트롤러를 포함) 어댑터가 있기 때문에 꼭 컨트롤러의 개념 뿐만 아니라 어떠한 것이든 해당하는 종류의 어댑터만 있으면 다 처리할 수 있다.
- 핸들러 어댑터 : 해당 핸들러를 사용할 수 있게끔 변환시키는, 단어 그대로의 어댑터 역할을 수행한다. 이 핸들러 어댑터로 인해 유연하게 컨트롤러들을 사용할 수 있다.
v5 구조 방식
- 핸들러 조회
- 요청 url로 클라이언트로부터 url을 얻어 핸들러 매핑 정보에 있는 해당 핸들러를 찾아 반환한다. - 핸들러를 처리할 수 있는 핸들러 어댑터 조회
- 핸들러어댑터들을 모아 놓은 핸들러 어댑터 목록에서 핸들러를 처리할 수 있는 핸들러 어댑터를 찾아서 반환한다. - 핸들러 어댑터.handle(handler) => 3-1. 핸들러 실행 => 3-2. ModelView 반환
-핸들러를 호출하여 요청에 대한 처리를 진행한다.
-처리에 대한 데이터는 model에 담고, 최종적으로 ModelView 반환한다. - ViewResolver 호출 => 4-1. MyView 반환
-ModelView 담긴 뷰 이름(논리 이름)을 가지고 실제 물리경로로 변환하여 MyView를 반환한다. - 렌더링 진행 MyView.render(model)
-model에 담긴 데이터들과 실제 뷰(물리 경로 이름)를 호출하여 렌더링 한 후, 브라우저에 출력한다.
=> 방식을 그림에 적용하면 다음과 같다.
MyHandlerAdapter - 핸들러 어댑터
- 인터페이스로 생성하여 핸들러 어댑터는 이러한 기능을 구현해야한다고 명시적으로 보여준다.
- boolean supports(Object handler)
- 어댑터가 해당 컨트롤러를 처리할 수 있는지 판단하는 메서드
- ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
- 어댑터는 실제 컨트롤러를 호출하고, 그 결과로 ModelView를 반환해야 한다. 실제 컨트롤러가 ModelView를 반환하지 못하면, 어댑터가 ModelView를 직접 생성해서라도 반환해야 한다.
- ControllerHandlerAdaptorV3 (어댑터 구현)
- ControllerHandlerAdaptorV4 (어댑터 구현)
이와 같이 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계가 가능해졌다.
정리
버전 | 주요 변경 사항 | 설명 |
V1 | 프론트 컨트롤러 도입 | 기존 구조 유지, 프론트 컨트롤러를 도입 |
V2 | View 분류 | 반복되는 뷰 로직 분리 |
V3 | Model 추가 | 서블릿 종속성 제거, 뷰 이름 중복 제거 |
V4 | 단순하고 실용적인 컨트롤러 | ModelView를 직접 생성하는 대신 편리한 인터페이스 제공 |
V5 | 유연한 컨트롤러 | 어댑터 도입, 프레임워크 유연성과 확장성 추가 |
'Spring > SpringMVC' 카테고리의 다른 글
스프링MVC의 기본 기능 (0) | 2023.12.07 |
---|---|
스프링 MVC 구조 (0) | 2023.10.24 |
Spring으로 간단한 회원관리 만들기(3) - MVC 패턴으로 회원관리 화면 구성 (0) | 2023.09.28 |
Spring으로 간단한 회원관리 만들기(3) -멤버 컨트롤러 (0) | 2023.09.28 |
Spring으로 간단한 회원관리 만들기(2) - 회원서비스와 서비스테스트 (0) | 2023.09.28 |