framework/spring

스프링 MVC 4 - 스프링 MVC 구조 이해

wooweee 2023. 11. 5. 13:23
728x90

0. 스프링 MVC 전체 구조

0.1. 큰 구조

 

  • Tocat
    • Request, Response
    • Servlet Container
  • Spring Conatiner
    • ServletContext
    • AnnotaionApplicationContext

webServlet과 spring Container 연관 작동

 

0.2. Spring MVC 구조

직접 만든 frameWork


Spring MVC

 

  • 직접 만든 프레임워크   스프링 MVC 비교  
    • FrontController  ->  DispatcherServlet
    • handlerMappingMap  ->  HandlerMapping 
    • MyHandlerAdapter  ->  HandlerAdapter  
    • ModelView  ->  ModelAndView  
    • viewResolver  ->  ViewResolver
    • MyView  ->  View

 

1. dispatcherServlet

1.1. dispatcher 개요

  • tip : intellij 해당 class 명 위에서 우클릭 ->다이어그램 -> show 다이어그램

 

DispatcherServlet 구조

  • spring MVC 핵심
  • DispatcherServlet은 조상 클래스에서 HttpServlet을 상속 받아서 서블릿으로 동작한다.
  • springboot는 DispatcherServlet을 서블릿으로 자동으로 Bean 등록(인스턴스가 default로 만들어짐) 해서 모든 경로(urlPatterns="/")에 대해서 매핑한다.

 

DispatcherServlet 장점

  • 스프링 MVC의 큰 강점은  DispatcherServlet  코드의 변경 없이, 원하는 기능을 변경하거나 확장할 수 있다는 점.
  • 이미지의 각 파트 대부분을 확장 가능할 수 있게 인터페이스로 제공 -> 사실 필요로 하는 대부분의 기능은 이미 다 구현되어있음

 

1.2. doDispatch()

DispatcherServlet.doDispatch() - 웹 mvc에 필요한 기능을 가진 method

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    ModelAndView mv = null;
    
    // 1. 핸들러 조회
    mappedHandler = getHandler(processedRequest); // controller 조회
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    
    // 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); // apater 조회
    
    // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); // mv&view 반환
    processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, HandlerExecutionChain mappedHandler, ModelAndView mv, Exception exception) throws Exception {
    // 뷰 렌더링 호출
    render(mv, request, response);
}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    View view;
    String viewName = mv.getViewName();
    
    // 6. 뷰 리졸버를 통해서 뷰 찾기, 7. View 반환
    view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
    
    // 8. 뷰 렌더링
    // render() = view 물리 이름 반환, view로 렌더링
    // myView 때 해당 class 들어가면 setAttribute, dispatcher, forwarding 수행기능 존재
    view.render(mv.getModelInternal(), request, response);
}

 

 

 

2. 핸들러 매핑과 핸들러 어댑터

Spring MVC

 

2.1. 작동 순서

  • 서블릿 호출 -> HttpServlet이 제공하는 service()가 호출 
    • DispatcherServlet 조상인 FrameworkServlet에 service() overriding 되어있다.
  • FrameworkServlet의 service() 작동 -> 여러 method 호출 -> DispatcherServlet.doDispatch() 호출

 

  1. 핸들러 조회: 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회

  2. 핸들러 어댑터 조회: 핸들러를 실행할 수 있는 핸들러 어댑터

  3. 핸들러 어댑터 실행

  4. 핸들러 실행: 핸들러 어댑터가 실제 핸들러를 실행

  5. ModelAndView 반환: 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환
    Model model 이용해서 논리 url string만 반환도 가능

  6. viewResolver 호출 : 논리 url -> 물리 url 변경

  7. View 반환: 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환
    view 객체 반환 : 직접 만들었던 view도 물리 url을 담은 Myview 객체를 반환
    그리고 렌더링 할 때, jsp 기준(model, request, response) 담음

  8. 뷰 렌더링: view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP를 실행

 

2.2. 핸들러 매핑, 핸들러 어댑터

  • OldController
  • Controller를 상속받는데 이는 spring에서 만든 Controller interface이다. cf) @Controller와 다르다.
@Component("/springmvc/old-controller") // handlerMapping 구현체중 2순위 : @bean
public class OldController implements Controller { // 3순위 handleradapter
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        return null;
    }
}

 

  • 다른 old version
@Component("/springmvc/request-handler") // handlerMapping 구현체중 2순위 : @bean
public class MyHttpRequestHandler implements HttpRequestHandler { // 2순위 handlerAdapter
    @Override 
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MyHttpRequestHandler.handleRequest");
    }
}

 

  • 강력 권장 방식
    • @RequestMapping : 해당 어노테이션만 붙이면 아래의 handler와 handlerAdapter가 다 적용된다. 
      1. RequestMappingHandlerMapping
      2. RequestMappingHandlerAdapter
    • 같은 어노테이션에 역할을 2개 수행한다는 뜻은 2번 어노테이션을 써야한다는 의미
      • class 단위 : RequestMappingHandlerMapping
      • method 단위 : RequestMappingHandlerAdapter
      • 참고 : class 단위의 구현체가 필요하므로 @RequestMapping @Component를 같이 작성해야함
        하지만 스프링 6.0 , 스프링 부트 3.0 이상부터는 @Controller만 적용 가능

 

 

  • 해당 springBean 이름을 url로 호출시 작동을 한다.
  • 호출될수 있는 이유
    1. 앞서 DispatcherServlet(DS)은 Spring이 자동 등록 
    2. 해당 url 요청을 DS가 받으면 핸들러매핑->핸들러어댑터조회->핸들러 호출이 진행
    3. 스프링 부트가 자동으로 등록하는 핸들러 매핑과 핸들러 어댑터 구현체를 통해서 핸들러가 호출
  • 자동으로 등록이 되어서 당장 눈에 보이지 않는 것: DS, 핸들러매핑, 핸들러어댑터

 

2.3. 구현된 handlerMapping과 handlerAdapter

  • HandlerMapping : 해당 형식의 controller(controller 외에도 가능) 
    • (0순위) RequestMappingHandlerMapping   : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
    • (1순위)  BeanNameUrlHandlerMapping        : 스프링 빈의 이름으로 핸들러를 찾는다.

  • HandlerAdapter: 해당 형식의 controller를 담당하는 frontController
    • (0순위) RequestMappingHandlerAdapter   : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용
    • (1순위) HttpRequestHandlerAdapter           : HttpRequestHandler 처리
    • (2순위) SimpleControllerHandlerAdapter    : Controller 인터페이스(애노테이션X, 과거에 사용) 처리

 


1. 핸들러 매핑으로 핸들러 조회

    HandlerMapping 을 순서대로 실행해서, 핸들러를 찾는다. 

    이 경우 빈 이름으로 핸들러를 찾아주는BeanNameUrlHandlerMapping 가 실행에 성공하고 OldController 반환


2. 핸들러 어댑터 조회 

   HandlerAdapter 의  supports() 를 순서대로 호출 

   SimpleControllerHandlerAdapter 가  Controller  인터페이스를 지원하므로 대상이 된다.

 

3. view resolver

  1. 핸들러 어댑터 호출 : 논리 view 이름 획득
  2. ViewResolver 호출 : ViewResolver 우선순위대로 호출
    1. BeanNameViewResolver는 new-form이라는 이름의 스프링 빈으로 등록된 view를 찾아야 하는데 없다.
    2. InternalResourceViewResolver가 호출
  3. InternalResourceViewResolver : InternalResourceView 반환
  4. 뷰 - InternalResourceView : JSP처럼 forward()를 호출해서 처리할 수 있는 경우 사용
  5. view.render() : view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP 실행

3.1. old/handler

@Component("/springmvc/old-controller") 
public class OldController implements Controller { 
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        System.out.println("OldController.handleRequest");
        // return null;
        return new ModelAndView("new-form");
    }
}

 

3.2. InternalPresourceViewResolver

  • 규칙: 해당위치에 지정할 물리적 위치를 설정하지 않으면 controller는 정상 호출되지만 UI에 error page 반환 
  • viewResolver를 구현하지 않는 이유: springboot는 자동으로 jsp 관련 viewResolver 등록해줌
# resources/application.properties

# InternalResourceViewResolver 방식 - Springboot가 구현한 것의 속성만 변경 
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

 

  • 해당 @Bean 과정을 springboot가 직접 해준다.
package hello.servlet;

@ServletComponentScan
@SpringBootApplication
public class ServletApplication {

   public static void main(String[] args) {
      SpringApplication.run(ServletApplication.class, args);
   }

   @Bean
   InternalResourceViewResolver internalResourceViewResolver() {
      return new InternalResourceViewResolver("/WEB-INF/views/",".jsp");
   }
}

 

3.3. ViewResolver 우선순위

  • (1순위) BeanNameViewResolver            : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)
  • (2순위) InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.

 

3.4. 참고

  • InternalResourceResolver는 만약 JSTL library가 있으면 InternalResourceView를 상속받은 JstView를 반환한다.
  • 다른 뷰는 실제 뷰를 렌더링하지만 JSP같은 경우 forward()를 통해서 해당 JSP로 이동해야 렌더링이 된다.
    JSP를 제외한 나머지 뷰 템플릿은 forward() 과정없이 바로 렌더링된다.
  • Thymeleaf 뷰 템플릿을 사용하면 ThymeleafViewResolver를 등록해야한다. 최근에는 library만 추가하면 스프링부트가 이런 작업 모두 자동화

 

4. spring mvc - 시작

@RequestMapping

  • RequestMappingHandlerMapping  : handler 의미
  • RequestMappingHandlerAdapter : handlerAdapter 의미

 

@Controller

  1. @Component 기능
  2. 스피링 MVC에서 어노테이션 기반 컨트롤러로 인식 = RequestMappingHandlerMapping에서 꺼낼수있는 기반이 된다.
    → RequestMappingHandlerMapping 은 스프링 빈 중에서 @RequestMapping 또는 @Controller클래스레벨에 붙어 있는 경우에 매핑 정보로 인식한다.

@Requestmapping

  • 요청정보 매핑 = 해당 url 호출시 해당 method 호출

 

4.1. Handler

  • process() 내부 params: 자율적으로 넣고 빼고 가능
  • 나머지 작동방식은 직접 만들어봤던 mvc와 동일
  • modelAndview
    • new ModelAndView 할 때 논리 url를 string으로 넣어줘야 한다.
    • addObject(): 스프링이 제공하는 ModelAndView에 Model data 추가시 사용하는 method
package hello.servlet.web.springmvc.v1;

@Controller
public class SpringMemberFormControllerV1 {

    @RequestMapping("/springmvc/v1/members/new-form")
    public ModelAndView process(){
        return new ModelAndView("new-form");
    }
}

 

package hello.servlet.web.springmvc.v1;

@Controller
public class SpringMemberSaveControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("springmvc/v1/members/save")
    public ModelAndView process(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save-result");
//        mv.getModel().put("member", member);
        mv.addObject("member", member); // 더 깔끔한 방법
        return mv;

    }
}
package hello.servlet.web.springmvc.v1;

@Controller
public class SpringMemberListControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();
    @RequestMapping("springmvc/v1/members")
    public ModelAndView process() {
        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.getModel().put("members", members);
        return mv;
    }
}

 

5. spring mvc - controller 통합

  • 가능한 이유: @RequestMapping이 메서드 단위에 적용되었기 때문
  • 통합할 때 method명은 각각 다르게 작성해야함. - 딱히 큰 의미는 없다.
package hello.servlet.web.springmvc.v2;

@Controller
@RequestMapping("/springmvc/v2/members") // 조합기능
public class SpringMemberControllerV2 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping("/new-form")
    public ModelAndView newForm(){
        return new ModelAndView("new-form");
    }

    @RequestMapping("/save")
    public ModelAndView save(HttpServletRequest request, HttpServletResponse response) {
        String username = request.getParameter("username");
        int age = Integer.parseInt(request.getParameter("age"));

        Member member = new Member(username, age);
        memberRepository.save(member);

        ModelAndView mv = new ModelAndView("save-result");
      //mv.getModel().put("member", member); 이것도 가능한데 아래방법이 좋음
        mv.addObject("member", member);
        return mv;
    }
    
    @RequestMapping // 추가 url 없으면 url 작성 안해도 됨
    public ModelAndView members() {
        List<Member> members = memberRepository.findAll();

        ModelAndView mv = new ModelAndView("members");
        mv.getModel().put("members", members);
        return mv;
    }
}

 

 

6. spring mvc - 실용적인 방식

  • 2가지 방식
    1. ModelAndView 반환 방식
      • modelAndView 생성하면서 초기화시, 논리 path 넣기
      • mv.addOject() 으로 view에 전달할 리소스 넣기
        cf) mv.getModel().put("member", member); 이것도 가능한데 위의 방법이 좋음

    2. String 논리url 반환 방식
      • dispatcherServlet에서 이미 만든 Model 객체를 이용(model.addAttribute())해서 view로 전달할 리소스 넣기
      • String으로 논리 url만 반환

 

  • request params를 직접 받는 방법 : @RequestParam()

  • getMapping, postMapping 
    • request message 중에서 method가 get 일 때, post 일 때 구분해서 받는다.
  • @RequestParam("username") String username : 예전에 ParamMap 만들필요없이 요청 params를 가져올 수 있다.
  • @RequestParam("username") String username = request.getParameter("username");
  • @RequestMapping  -> @GetMapping, @PostMapping : url 매핑뿐만이 아니라 http Method를 함께 구분 가능
  • @PostMapping = @RequestMapping(value = "/new-form", method = RequestMethod.POST)

 

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    @RequestMapping(value = "/new-form" , method = RequestMethod.GET)
    public String newForm(){
        return "new-form";
    }
    
    // String, model 이용한 save
    // @RequestParam()으로 queryparams 받기
    @PostMapping("/save")
    public String save(@RequestParam("username") String username, @RequestParam("age") int age, Model model) {

        Member member = new Member(username, age);
        memberRepository.save(member);

        model.addAttribute("member", member);
        return "save-result";

    }

    // model에 직접 저장
    @GetMapping
    public String members(Model model) {
        List<Member> members = memberRepository.findAll();

        model.addAttribute("members", members);
        return "members";
    }
}

 

 

 

 

이전 발행글 : 스프링 MVC 3 - MVC 프레임워크 만들기

 

다음 발행글 : 스프링 MVC 5 - 기본 기능

 


출처 인프런 김영한의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술