반응형
개요
얼마전에 네이버에서 기술 면접을 보게 되었는데 질문으로 MVC 패턴에 대해서 설명해달라는 질문을 받게 되었다. 설명은 했으나 너무 오래전에 학습해서 내가 알고있는 전부를 제대로 전달하지는 못한거 같아서 글로 다시 정리해볼려고 한다.
1.스프링 MVC 전체 구조
스프링의 MVC 구조는 간단하게(?) 아래와 같은 구조로 이루어져있다.
- 먼저 Http 요청이 이루어진다 이후 DispatcherServelet의 doDispatch()가 실행된다.
- 현재 스프링 부트의 핸들러 매핑 정보를 탐색한다.
- handler 동작을 수행해줄 Adapter의 정보를 보고 handler 동작을 수행할 수 있는 Adapter가 존재한다면 반환하게 된다.
- 다시 DispatcherServlet으로 돌아온뒤 3번에서 받아온 Controller를 동작시키기 위해 handle이라는 메소드가 수행된다.
- handle 메소드안에는 handler의 동작을 수행할 수 있는 process라는 메서드가 존재하고 process를 동작시키게된다. 이를 통해 핵심 로직을 수행한다.
- 이후 핸들러 Adapter로 돌아와서 handle 메서드를 통해 반환되는 ModelAndView가 생성된다.
- ViewResolver를 실행해 논리 주소를 물리 주소로 만들어준다.
- View를 활용해 render 메서드를 동작한다.
스프링 MVC 구조의 장점
- 스프링 MVC 구조의 장점은 FrontController(DispatcherServlet)의 코드 변경없이 원하는 기능을 변경하거나 확장이 가능하다.
- 필요 기능은 인터페이스화 시키고 상속을 받아 재정의를 통해 확장이 가능하기 때문이다.
2.HandlerMapping과 HandlerAdapter
- 위에서 그림을 보면 HandlerMapping(핸들러의 정보들을 조회하는 부분)과 HandlerAdapter가 있는것을 확인할 수 있다.
- 중요한 로직인만큼 조금 더 알아봤다.
- HandlerMapping과 HandlerAdapter는 각각 중요한 역할이 있다.
HandlerMapping
- HandlerMapping에서는 handler(Controller)를 찾게 해준다.
- 우선순위
- (1순위) RequestMappingHandlerMapping → 어노테이션 기반 컨트롤러 @RequestMapping 기준을 의미
- (2순위) BeanNameUrlHandlerMapping → 스프링 빈 이름으로 검색을 의미
- 이외에도 우선순위는 존재한다.
HandlerAdapter
- HandlerMapping을 통해 찾은 Handler는 동작 시킬 수 있는 HandlerAdapter가 필요하다.
- HadlerAdapter를 통하여 HandlerMapping으로 찾은 Handler의 Adpater를 반환해줘야 한다.
- 우선순위
- (1순위) RequestMappingHandlerAdapter → 어노테이션 기반 Controller인 @RequeestMapiing에서 사용
- (2순위) HttpRequestHandlerAdapter → HttpRequestHandler 처리
- (3순위) SimpleControllerHandlerAdapter : Controller 인터페이스
HandlerMapping과 HandlerAdapter 조회 실패할 경우는?
💡 HandlerMapping과 HandlerAdpater의 우선순위를 차례대로 실행하게 된다.
3.뷰 리졸버
- Spring Boot는 InternalResourceViewResolver를 통해 View Resolver를 자동으로 등록한다. 아래와 같이 application.properties에 설정 정보에 정보를 추가하면 된다.
//application.properties
spring.mvc.view.prefix = /WEB-INF/views/
spring.mvc.view.suffix= .jsp
//위 두 줄을 추가하면 Controller Anotation을 사용할 때 자동으로 viewResolver가 추가해서 양식을 찾아줌
4.Spring MVC 구현
- 위에 내용대로 Spring의 MVC 체계를 구현해보자
- 먼저 Handler가 상속받을 인터페이스를 만들어준다.
- 어떤 Handler가 들어왔는지 알 수 있도록 각 Handler는 인터페이스를 상속 받는다.
- 이를 통해 다양한 Handler 종류가 들어와도 확장성 있는 설계가 가능하다.
- 아래와 같은 구조로 구현을 해보자
- FrontController 역할을 하는 PracticeDispatchServelet을 만들어야 한다.
package hello.servlet.web.frontcontroller.makepratice;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.MyView;
import hello.servlet.web.frontcontroller.makepratice.handleradapter.ControllerType1HandlerAdapter;
import hello.servlet.web.frontcontroller.makepratice.handleradapter.ControllerType2HandlerAdapter;
import hello.servlet.web.frontcontroller.makepratice.type1controller.type1MemberFormController;
import hello.servlet.web.frontcontroller.makepratice.type1controller.type1MemberSaveController;
import hello.servlet.web.frontcontroller.makepratice.type2controller.type2MemberSaveController;
import hello.servlet.web.frontcontroller.v5.MyHandlerAdapter;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV3HandlerAdapter;
import hello.servlet.web.frontcontroller.v5.adapter.ControllerV4HandlerAdapter;
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 java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@WebServlet(name = "SpringDispatchServlet", urlPatterns = "SpringDispatchServlet/*")
public class PracticeDispatchServlet extends HttpServlet {
private final Map<String, Object> handlerMappingInfo = new HashMap<>();
private final List<HandlerAdapter> handlerAdapters = new ArrayList<>();
public PracticeDispatchServlet() {
initHandlerMappingMap();
handlerAdapters.add(new ControllerType1HandlerAdapter());
handlerAdapters.add(new ControllerType2HandlerAdapter());
}
private void initHandlerMappingMap() {
handlerMappingInfo.put("type1MemberFormController/new-form", new type1MemberFormController());
handlerMappingInfo.put("type1MemberFormController/save", new type1MemberSaveController());
handlerMappingInfo.put("type2MemberFormController/new-form", new type2MemberSaveController());
handlerMappingInfo.put("type2MemberFormController/save", new type2MemberSaveController());
}
private void initHandlerAdapters() {
handlerAdapters.add(new ControllerType1HandlerAdapter());
handlerAdapters.add(new ControllerType2HandlerAdapter());
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Object handler = getHandler(request);
if(handler == null){
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
HandlerAdapter adapter = getHandlerAdapter(handler);
ModelView mv = adapter.handle(request, response, handler);
String viewName = mv.getViewName();
MyView view = viewResolver(viewName);
view.render(mv.getModel(), request, response);
}
private HandlerAdapter getHandlerAdapter(Object handler) {
for(HandlerAdapter adapter : handlerAdapters){
if(adapter.supports(handler)) {
return adapter;
}
}
throw new IllegalArgumentException("handler adapter를 찾을 수 없습니다. handler = " + handler);
}
private Object getHandler(HttpServletRequest request){
String requestURI = request.getRequestURI();
return handlerMappingInfo.get(requestURI);
}
private MyView viewResolver(String viewName){
return new MyView("/WEB-INF/views/" + viewName + ".jsp");
}
}
- 다음으로는 들어온 Handler의 동작을 수행할 수 있는 HandlerAdpater interface를 선언하자
package hello.servlet.web.frontcontroller.makepratice;
import hello.servlet.web.frontcontroller.ModelView;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public interface HandlerAdapter {
boolean supports(Object handler);
ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SecurityException, IOException, ServletException;
}
- 들어올 수 있는 Controller의 첫 번째 경우인 type1Controller의 인터페이스를 구현한다.
package hello.servlet.web.frontcontroller.makepratice.type1controller;
import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;
public interface type1Controller {
ModelView process(Map<String, String> paramMap);
}
- type1Controller를 상속받아 new-form으로 이동하는 기능과 회원 정보를 저장하는 두 가지 class를 구현한다
package hello.servlet.web.frontcontroller.makepratice.type1controller;
import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;
public class type1MemberFormController implements type1Controller{
@Override
public ModelView process(Map<String, String> paramMap) {
return new ModelView("new-form");
}
}
package hello.servlet.web.frontcontroller.makepratice.type1controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import hello.servlet.web.frontcontroller.ModelView;
import java.util.Map;
public class type1MemberSaveController implements type1Controller{
MemberRepository memberRepository = MemberRepository.getInstance();
@Override
public ModelView process(Map<String, String> 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;
}
}
- 들어올 수 있는 Controller의 두 번째 경우인 type2Controller의 인터페이스를 구현한다.
package hello.servlet.web.frontcontroller.makepratice.type2controller;
import java.util.Map;
public interface type2Controller{
String process(Map<String, String> paramMap, Map<String,Object> model);
}
- type2Controller를 상속받아 new-form으로 이동하는 기능과 회원 정보를 저장하는 두 가지 class를 구현한다
package hello.servlet.web.frontcontroller.makepratice.type2controller;
import hello.servlet.domain.member.Member;
import hello.servlet.domain.member.MemberRepository;
import java.util.Map;
public class type2MemberFormController implements type2Controller {
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 "new-form";
}
}
package hello.servlet.web.frontcontroller.makepratice.type2controller;
import java.util.Map;
public class type2MemberSaveController implements type2Controller{
@Override
public String process(Map<String, String> paramMap, Map<String, Object> model) {
return "save-result";
}
}
- 마지막으로 handler의 종류가 결정되고 handler의 동작을 수행할 type1Controller와 type2의 Adapter을 구현하자.
package hello.servlet.web.frontcontroller.makepratice.handleradapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.makepratice.HandlerAdapter;
import hello.servlet.web.frontcontroller.makepratice.type1controller.type1Controller;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class ControllerType1HandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof type1Controller);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SecurityException, IOException {
type1Controller controller = (type1Controller) handler;
Map<String, String> paramMap = new HashMap<>();
createParamMap(request, paramMap);
ModelView mv = controller.process(paramMap);
return mv;
}
private void createParamMap(HttpServletRequest request, Map<String, String> paramMap) {
request.getParameterNames().asIterator().forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
}
}
package hello.servlet.web.frontcontroller.makepratice.handleradapter;
import hello.servlet.web.frontcontroller.ModelView;
import hello.servlet.web.frontcontroller.makepratice.HandlerAdapter;
import hello.servlet.web.frontcontroller.makepratice.type2controller.type2Controller;
import hello.servlet.web.frontcontroller.v4.ControllerV4;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class ControllerType2HandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof type2Controller);
}
@Override
public ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException {
type2Controller controller = (type2Controller) handler;
Map<String, String> paraMap = createParmaMap(request);
HashMap<String, Object> model = new HashMap<>();
String viewName = controller.process(paraMap, model);
ModelView mv = new ModelView(viewName);
mv.setModel(model);
return mv;
}
private Map<String, String> createParmaMap(HttpServletRequest request){
Map<String, String> paramMap = new HashMap<>();
request.getParameterNames().asIterator().forEachRemaining(paramName -> paramMap.put(paramName, request.getParameter(paramName)));
return paramMap;
}
}
반응형
'Spring' 카테고리의 다른 글
Spring 메시지, 국제화 (0) | 2022.06.27 |
---|---|
Spring의 Validation이란 (0) | 2022.06.27 |
스프링 빈 기능 (0) | 2022.04.12 |
Spring의 종류와 장점 (0) | 2022.04.12 |
좋은 객체 지향 설계의 5가지 원칙(SOLID) (0) | 2021.09.30 |