Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
Tags
- JPA
- 클린 코드
- GitHub
- Python
- 파이썬
- 기초100제
- Spring Boot
- mariadb
- Codeup
- H2 설치
- 티스토리챌린지
- 객사오
- 알고리즘
- thymeleaf
- Git
- golang
- 코드업
- 클린코드
- Gradle
- Vue.js
- Postman
- 오블완
- go
- MySQL
- Spring
- springboot
- spring security
- 롬복
- 스프링
- java
Archives
- Today
- Total
nyximos.log
[Spring] MVC 프레임워크 만들기 본문
프론트 컨트롤러 패턴 소개
프론트 컨트롤러 도입 전
프론트 컨트롤러 도입 후
FrontController 패턴 특징
- 프론트 컨트롤러 서블릿 하나로 클라이언트의 요청을 받음
- 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출
- 입구를 하나로 만들어 공통 처리 가능
- 프론트 컨트롤러를 제외한 나머지 컨트롤러는 서블릿을 사용하지 않아도 된다.
스프링 웹 MVC와 프론트 컨트롤러
스프링 웹 MVC의 DispatchServlet이 FrontController 패턴으로 구현되어 있음
프론트 컨트롤러 도입 v1
- 기존 코드를 최대한 유지하면서 프론트 컨트롤러를 도입한다.
서블릿과 비슷한 모양의 컨트롤러 인터페이스 도입
- 각 컨트롤러는 이 인터페이스를 구현하면 된다.
- 프론트 컨트롤러는 이 인터페이스를 호출해서 구현과 관계없이 로직의 일관성을 가져갈 수 있다.
public interface ControllerV1 {
void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
인터페이스를 구현한 컨트롤러 생성
- 아까 생성한 컨트롤러 인터페이스를 구현한다.
- 내부 로직은 기존 서블릿과 거의 같다.
- process()를 Override하여 로직을 작성한다.
public class MemberListControllerV1 implements ControllerV1 {
private MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public void process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 내부로직
}
프론트 컨트롤러 생성
- controllerMap을 생성하여 controller를 넣어준다.
@WebServlet(name = "frontControllerServletV1", urlPatterns = "/frontcontroller/v1/*")
public class FrontControllerServletV1 extends HttpServlet {
private Map<String, ControllerV1> controllerMap = new HashMap<>();
public FrontControllerServletV1() {
controllerMap.put("파일 경로", new 컨트롤러());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String requestURI = request.getRequestURI();
ControllerV1 controller = controllerMap.get(requestURI);
if (controller == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
controller.process(request, response);
}
}
urlPatterns
- urlPatterns = "/front-controller/v1/*"
- /front-controller/v1" 를 포함한 하위 모든 요청은 이 서블릿에서 받아들인다.
controllerMap
- key : 매핑 URL
- value : 호출된 컨트롤러
service()
- 먼저 requestURI를 조회해서 실제 호출할 컨트롤러를 controllerMap에서 찾는다.
- 없다면 404(SC_NOT_FOUND) 상태 코드를 반환한다.
- 컨트롤러를 찾고 controller.process(request, response);를 호출해서 해당 컨트롤러를 실행한다
View 분리 v2
- 모든 컨트롤러에서 뷰로 이동하는 부분에 중복이 있고 깔끔하지 않다.
String viewPath = "/WEB-INF/views/new-form.jsp";
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
🧚♀️ 해결방법
별도로 뷰를 처리하는 객체를 만들어 깔끔하게 분리하자!
MyView 생성
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);
}
}
컨트롤러 인터페이스 생성
- 컨트롤러는 뷰를 반환한다.
public interface ControllerV2 {
MyView process(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}
인터페이스를 구현한 컨트롤러 만들기
- process()를 Override하여 로직을 작성한다.
- 복잡한 dispatcher.forward()를 직접 생성해서 호출하지 않아도 된다.
- MyView 객체를 생성하고 거기에 뷰 이름만 넣고 반환한다.
return new MyView("viewPath");
프론트 컨트롤러 생성
- controllerMap에서 requestURI가 같은 controller를 가져온다.
- 컨트롤러는 호출결과로 MyView를 반환 받는다.
- view.render()를 호출하면 forward 로직을 수행해서 JSP가 실행된다.
MyView view = controller.process(request, response);
view.render(request, response);
- 프론트 컨트롤러의 도입으로 MyView 객체의 render()를 호출하는 부분을 모두 일관되게 처리할 수 있다.
- 각각의 컨트롤러는 MyView 객체를 생성만 해서 반환하면 된다.
Model 추가 v3
서블릿 종속성 제거
- 컨트롤러 입장에서는 HttpServletRequest, HttpServletResponse가 항상 필요하지 않다.
- 요청 파라미터 정보는 자바의 Map으로 대신 넘기도록 하면 컨트롤러가 서블릿 기술을 몰라도 동작할 수 있다.
- request 객체를 Model로 사용하지 않고 Model 객체를 만들어서 반환하자.
뷰 이름 중복 제거
- 컨트롤러에서 지정하는 뷰 이름에 중복이 있다.
- 컨트롤러 : 뷰 논리 이름 반환
- 프론트 컨트롤러 : 실제 물리 위치 이름 처리
ModelView
- 서블릿의 종속성을 제거하기위해 Model을 직접 만들고 View 이름까지 전달하자.
- 뷰의 이름과 뷰를 렌더링할 때 필요한 model 객체를 가지고 있다.
- 컨트롤러에서 뷰에 필요한 데이터를 key, value로 map에 넣어주자.
public class ModelView {
private String viewName;
private Map<String, Object> model = new HashMap<>();
}
컨트롤러 인터페이스 생성
- 서블릿 기술을 사용하지 않는다. → 구현이 단순해지고 테스트 코드 작성시 테스트 하기 쉽다.
- HttpServletRequest가 제공하는 파라미터는 프론트 컨트롤러가 paramMap에 담아서 호출해준다.
- 응답 결과로 뷰 이름과 뷰에 전달한 Model 데이터를 포함하는 ModelView 객체를 반환하면 된다.
public interface ControllerV3 {
ModelView process(Map<String, String> paramMap);
}
인터페이스를 구현한 컨트롤러 생성
- ModelView를 생성할 때 view의 논리적인 이름을 지정한다.
- 물리적인 이름은 프론트 컨트롤러에서 처리
ModelView mv = new ModelView("save-result");
- 파라미터 정보는 map에 담겨있다. map에서 필요한 요청 파라미터를 조회하면 된다.
paramMap.get("username");
- 모델은 단순한 map이므로 모델에 뷰에서 필요한 member 객체를 담고 반환한다
mv.getModel().put("member", member);
프론트 컨트롤러 생성
- HttpServletRequest에서 파라미터 정보를 꺼내서 Map으로 변환한다.
- 그리고 해당 Map(paramMap)을 컨트롤러에 전달하면서 호출한다.
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
뷰 리졸버
String viewName = mv.getViewName();
MyView view = viewResolver(viewName)
- 컨트롤러가 반환한 논리 뷰 이름을 실제 물리 뷰 경로로 변경
- 실제 물리 경로가 있는 MyView 객체 반환
view.render(mv.getModel(), request, response)
- 뷰 객체를 통해서 HTML 화면 렌더링
- 뷰 객체의 render()는 모델 정보도 함께 받는다.
- JSP는 request.setAttribute()로 데이터를 조회
→ 모델의 데이터를 꺼내서 request.setAttribute()로 담아둔다. - JSP로 포워드해서 JSP를 렌더링한다.
MyView
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);
}
public void render(Map<String, Object> model, HttpServletRequest request, HttprServletResponse response) throws ServletException, IOException{
modelToRequestAttribute(model, request);
RequestDispatcher dispatcher = request.getRequestDispatcher(viewPath);
dispatcher.forward(request, response);
}
private void modelToRequestAttribute(Map<String, Object> model, HttpServletRequest request){
model.forEach((key,value) -> request.setAttribute(key,value));
}
}
단순하고 실용적인 컨트롤러 v4
- 개발자 입장에서 항상 ModelView 객체를 생성하고 반환하는 부분이 번거롭다.
- 개발자들이 편리하게 개발할 수 있는 v4 버전을 개발해보자.
- 기존 구조에서 모델을 파라미터로 넘기고, 뷰의 논리 이름을 반환한다.
- ModelView를 반환하지않고 ViewName만 반환한다.
컨트롤러 인터페이스 생성
- model 객체를 파라미터로 전달되기 때문에 뷰의 이름만 반환하면 된다.
public interface ControllerV4 {
/**
* @param paramMap
* @param model
* @return viewName
*/
String process(Map<String, String> paramMap, Map<String, Object> model);
}
인터페이스를 구현한 컨트롤러 생성
- 모델이 파라미터로 전달되기 때문에, 모델을 직접 생성하지 않아도 된다.
model.put("member", member)
프론트 컨트롤러 생성
- 모델객체 전달
모델 객체를 프론트 컨트롤러에서 생성해서 넘겨준다.
컨트롤러에서 모델 객체에 값을 담으면 여기에 그대로 담겨있게 된다.
Map<String, Object> model = new HashMap<>();
- 뷰의 논리 이름을 직접 반환
String viewName = controller.process(paramMap, model);
MyView view = viewResolver(viewName);
유연한 컨트롤러1 v5
어댑터 패턴
- 이때까지의 프론트 컨트롤러는 한가지 방식의 컨트롤러 인터페이스만 사용할 수 있다.
- 어댑터 패턴을 사용하면 프론트 컨트롤러가 다양한 방식의 컨트롤러를 처리할 수 있도록 변경할 수 있다.
핸들러 어댑터
- 프론트 컨트롤러와 컨트롤러 중간에 어댑터를 두어 다양한 종류의 컨트롤러를 호출한다.
핸들러
- 컨트롤러의 이름을 더 넓은 범위인 핸들러로 변경했다.
어댑터용 인터페이스
- 어댑터는 실제 컨트롤러를 호출하고 ModelView를 반환한다.
public interface MyHandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
- supports는 컨트롤러를 처리할 수 있는지 판단하는 메서드이다.
boolean supports(Object handler)
어댑터 구현
- handler가 ControllerV3을 처리할 수 있는 어댑터인지
public boolean supports(Object handler) {
return (handler instanceof ControllerV3);
}
- handler를 컨트롤러 V3으로 변환 → 형식에 맞게 호출
- ControllerV3는 ModelView를 반환하므로 그대로 Model View를 반환한다.
ControllerV3 controller = (ControllerV3) handler;
Map<String, String> paramMap = createParamMap(request);
ModelView mv = controller.process(paramMap);
return mv;
컨트롤러 → 핸들러
- 생성자는 핸들러 매핑과 어댑터를 초기화(등록)한다.
public FrontControllerServletV5() {
initHandlerMappingMap(); //핸들러 매핑 초기화
initHandlerAdapters(); //어댑터 초기화
}
- 여러 인터페이스에서 아무 값이나 받을 수 있는 Object로 변경됐다.
private final Map<String, Object> handlerMappingMap = new HashMap<>();
- 핸들러 매핑 정보인 handlerMappingMap에서 URL에 매핑된 핸들러(컨트롤러) 객체를 찾아서 반환한다.
private Object getHandler(HttpServletRequest request) {
String requestURI = request.getRequestURI();
return handlerMappingMap.get(requestURI);
}
- 핸들러를 처리할 수 있는 어댑터를 찾는다. adapter.supports(handler)
for (MyHandlerAdapter adapter : handlerAdapters) {
if (adapter.supports(handler)) {
return adapter;
}
}
- 실제 어댑터를 호출한다.
ModelView mv = adapter.handle(request, response, handler);
유연한 컨트롤러2 v5
ControllerV4를 사용할 수 있도록 기능을 추가한다.
프론트 컨트롤러
- 핸들러 매핑( handlerMappingMap )에 ControllerV4 를 사용하는 컨트롤러를 추가하고,
- 해당 컨트롤러를 처리할 수 있는 어댑터인 ControllerV4HandlerAdapter 도 추가하자.
어댑터 구현
- handler가 ControllerV4을 처리할 수 있는 어댑터인지
public boolean supports(Object handler) {
return (handler instanceof ControllerV4);
}
- handler를 ControllerV4로 캐스팅
- paramMap, model을 만들어서 해당 컨트롤러를 호출
- viewName을 반환
ControllerV4 controller = (ControllerV4) handler;
Map<String, String> paramMap = createParamMap(request);
Map<String, Object> model = new HashMap<>();
String viewName = controller.process(paramMap, model);
어댑터 변환
- 어댑터가 호출하는 ControllerV4는 뷰의 이름을 반환한다.
- 어댑터는 ModelView를 만들어서 반환해야한다.
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
정리
v1 프론트 컨트롤러 도입
- 기존 구조를 최대한 유지하면서 프론트 컨트롤러 도입
v2 View 분류
- 단순 반복되는 뷰 로직 분리
v3 Model 추가
- 서블릿 종속성 제거
- 뷰 이름 중복 제거
v4 단순하고 실용적인 컨트롤러
- v3와 거의 비슷
- 구현 입장에서 ModelView를 직접 생성해서 반환하지 않도록 편리한 인터페이스 제공
v5 유연한 컨트롤러
- 어댑터 도입
- 어댑터를 추가해서 프레임워크를 유연하고 확장성 있게 설계
애노테이션을 사용해 컨트롤러를 더 편리하게 사용할 수도 있다.
→ 애노테이션 지원하는 어댑터 추가
참조
김영한, 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술
'Programming > Spring' 카테고리의 다른 글
@Valid (0) | 2024.11.07 |
---|---|
[Spring Cloud] Could not resolve all files for configuration ':compileClasspath (0) | 2024.07.18 |
[Spring] 서블릿, JSP, MVC 패턴 (0) | 2022.02.10 |
[Spring] 서블릿 (0) | 2022.02.09 |
[Spring Boot + Vue.js] 프로젝트 개발 환경 구성 (0) | 2022.02.07 |