[Spring MVC 1편] 5. 스프링 MVC 구조

HJ·2022년 8월 16일
0

Spring MVC 1편

목록 보기
5/8

김영한 님의 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술 강의를 보고 작성한 내용입니다.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard


1. 스프링 MVC 전체 구조

1-1. 지난 게시글과 차이점

  • FrontController ➔ DispatcherServlet

  • handlerMappingMap ➔ HandlerMapping ( 인터페이스 )

  • MyHandlerAdapter ➔ HandlerAdapter ( 인터페이스 )

  • ModelView ➔ ModelAndView

  • viewResolver ( 메서드 ) ➔ viewResolver ( 인터페이스 )

  • MyView ( 클래스 ) ➔ View ( 인터페이스 )


1-2. 동작 순서

  1. 핸들러 조회 : 요청 URL에 매핑된 핸들러( Controller )를 조회

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

  3. 핸들러 어댑터 실행

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

  5. ModelAndView 반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환

    • ModelAndView 는 view 의 이름과 ModelMap 이라는 이름의 LinkedHashMap 를 가진다
  6. viewResolver 호출

    • JSP의 경우 InternalResourceViewResolver 가 자동 등록되고, 사용된다

    • 템플릿 엔진마다 다른 viewResolver가 호출

    • viewResolver는 view의 논리 이름을 물리 이름으로 바꾸고 렌더링 역할을 하는 View 객체를 반환

    • JSP의 경우 InternalResourceView(JstlView) 를 반환하는데, 내부에 forward() 로직이 있다

  7. 뷰 렌더링 : 뷰를 통해 뷰를 렌더링




2. DispatcherServlet

2-1. 설명

  • DisPatcherServletHttpServlet을 상속 받아서 사용, 서블릿으로 동작

  • 스프링부트는 DispacherServlet 을 서블릿으로 자동으로 등록하면서 모든 경로( urlPatterns="/" )에 대해서 매핑한다

    • 모든 경로에 매핑하지만 자세한 경로가 매핑된 서블릿의 우선순위가 더 높기 때문에 개발자가 등록한 서블릿이 동작 가능
  • 서블릿이 호출되면 HttpServlet이 제공하는 service()가 호출

  • FrameworkServletservice()를 오버라이딩함

  • 즉, FrameworkServlet.service()에서 시작되고 여러 메서드를 거쳐 DispatcherServlet.doDispatch()가 호출


2-2. 동작 설명

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        
    // 1. 핸들러 조회
    mappedHandler = getHandler(processedRequest);
    
    // 2. 핸들러 어댑터 조회 - 핸들러를 처리할 수 있는 어댑터
    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    // 3. 핸들러 어댑터 실행 -> 4. 핸들러 어댑터를 통해 핸들러 실행 -> 5. ModelAndView 반환
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

    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. 뷰 렌더링
    view.render(mv.getModelInternal(), request, response);
}
  1. doDispatch()

    • request를 이용해 핸들러를 꺼낸다

    • 핸들러를 처리할 수 있는 핸들러 어댑터를 찾아서 반환

    • 핸들러 어댑터의 handle()을 호출

      • 그러면 핸들러 어댑터가 핸들러를 호출하고 결과를 받음

      • ModelAndView로 반환

    • 반환된 ModelAndView를 processDispatchResult()를 호출하면서 넘겨준다

  1. processDispatchResult()에서 뷰 렌더링 호출 ( render() )

  2. render()에서 뷰 리졸버를 통해 view를 찾고 반환 & 뷰 렌더링




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

3-1. 핸들러 호출 방법

  • 핸들러가 호출되기 위해 HandlerMapping, HandlerAdapter가 필요

  • HandlerMapping( 핸들러 매핑 )

    • 핸들러 매핑에서 해당 컨트롤러를 찾을 수 있어야한다

    • ex> 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요하다

  • HandlerAdapter( 핸들러 어댑터)

    • 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요

    • ex> Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾고 실행해야한다


3-2. 핸들러 매핑

  • 0 순위 : RequestMappingHandlerMapping

    • @RequestMapping을 사용하는 어노테이션 기반의 컨트롤러에서 사용

    • 가장 먼저 실행되는 객체

  • 1순위 : BeanNameUrlHandlerMapping

    • 스프링 빈의 이름으로 핸들러를 찾는 객체

    • URL 이름과 동일한 스프링 빈을 찾는다


3-3. 핸들러 어댑터

  • 0 순위 : RequestMappingHandlerAdapter

    • @RequestMapping을 사용하는 어노테이션 기반의 컨트롤러에서 사용
  • 1순위 : HttpRequestHandlerAdapter

    • HttpRequestHandler를 처리하는 어댑터 ( 객체 )
  • 2순위 : SimpleControllerHandlerAdapter

    • Controller 인터페이스 ( 어노테이션 X ) 처리

public class SimpleControllerHandlerAdapter implements HandlerAdapter {

	@Override
	public boolean supports(Object handler) {
		return (handler instanceof Controller);
	}

    @Override
	@Nullable
	public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return ((Controller) handler).handleRequest(request, response);
	}
}
  • 위에서 핸들러 매핑을 통해 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다고 했다

  • 이를 판단하는 기준이 위의 supports() 메서드인데 instanceof 로 인해 Controller 인터페이스를 호출할 수 있는지 여부를 확인할 수 있다

  • dispatcherServlet 이 핸들러 어댑터의 handle() 메서드를 실행한다




4. 핸들러 매핑과 핸들러 어댑터 예시

4-1. 과거의 Controller ( 인터페이스 )

public interface Controller {
    ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception;
}
  • 과거에 사용했던 컨트롤러

  • 현재는 어노테이션 기반의 컨트롤러를 사용 ( @Controller )

  • Controller 인터페이스와 @Controller는 전혀 다름


4-2. 핸들러 매핑과 핸들러 어댑터의 동작

@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;
    }
}
  • @Component

    • /springmvc/old-controller 라는 이름의 스프링 빈으로 등록

    • 빈의 이름으로 URL을 매핑

  • 핸들러 매핑으로 핸들러 조회

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

    • 위의 경우 스프링 빈의 이름으로 핸들러를 찾을 수 있는 핸들러 매핑이 필요

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

  • 핸들러 어댑터 조회

    • 핸들러 매핑을 통해서 찾은 핸들러를 실행할 수 있는 핸들러 어댑터가 필요하다

    • HandlerAdapter 의 supports() 를 순서대로 호출하여 Controller 인터페이스를 실행할 수 있는 핸들러 어댑터를 찾는다

    • 위의 경우 Controller 인터페이스를 지원하는 SimpleControllerHandlerAdapter 가 찾아진다

  • 핸들러 어댑터 실행

    • DispatcherServlet이 조회한 SimpleControllerHandlerAdapter 를 실행하면서 핸들러 정보도 함께 넘겨준다

    • SimpleControllerHandlerAdapter 는 핸들러인 OldController 를 내부에서 실행하고, 결과를 반환


4-3. HttpRequestHandler일 때 핸들러 매핑과 핸들러 어댑터 동작

public interface HttpRequestHandler {

	void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException;
}

@Component("/springmvc/request-handler")
public class MyHttpRequestHandler implements HttpRequestHandler {

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("MyHttpRequestHandler.handleRequest");
    }
}
  • HttpRequestHandler 인터페이스를 구현한 핸들러는 BeanNameUrlHandlerMappingHttpRequestHandlerAdapter 객체를 사용하여 실행된다



5. viewResolver

5-1. viewReolver 가 필요한 이유

@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 new ModelAndView("new-form");
    }
}
  • ModelAndView 에 view 의 논리 이름을 넣고 반환하면 white label page 가 출력된다

  • view 를 찾지 못해서 white label page 가 출력된 것인데 view 를 찾지 못했다는 것은 viewResolver 가 작동하지 않았다는 것이다

  • 즉, viewResolver 는 Controller 가 반환한 view 의 논리 이름을 가지고 view 를 찾는 기능을 하며 이를 위해서 viewResolver 를 만들어야한다

  • 스프링에서는 여러가지 viewResolver 가 등록되어 있다


5-2. 자동 등록된 viewResolver

  • 1순위 : BeanNameViewResolver

    • 빈 이름으로 뷰를 찾아서 반환
  • 2순위 : InternalResourceViewResolver

    • JSP를 처리할 수 있는 뷰를 반환

5-3. InternalResourceViewResolver

< application.properties >

spring.mvc.view.prefix=/WEB-INF/views
spring.mvc.view.suffix=.jsp
  • InternalResourceViewResolver 가 자동으로 등록될 때 application.properties 에 등록한 설정 정보를 사용

    • spring.mvc.view.prefix

    • spring.mvc.view.suffix

  • 스프링부트가 설정 정보를 가져와 아래의 코드를 대신 해준다고 생각하면 됨
@Bean
InternalResourceViewResolver internalResourceViewResolver() {
    retrn new InternalResourceViewResolver("/WEB-INF/views", ".jsp");
}

5-4. viewResolver 동작 예시

  1. 핸들러 어댑터 호출

    • DispatcherServlet이 핸들러 어댑터를 통해 new-form 이라는 view의 논리 이름을 획득
  1. viewResolver 호출

    • new-form 이라는 뷰 이름으로 viewResolver를 순서대로 호출

    • BeanNameViewResolvernew-form 이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없기 때문에 다음으로 넘어감

    • InternalResourceViewResolver 가 호출 ( 2순위 )

  1. InternalResourceViewResolver

    • InternalResourceViewResolver가 InternalResourceView를 반환
  1. view - InternalResourceView

    • InternalResourceView 는 JSP처럼 포워드 forward() 를 호출해서 처리할 수 있는 경우에 사용
  1. view.render()

    • view.render() 가 호출되고 InternalResourceViewforward() 를 사용해서 JSP를 실행

5-5. 참고사항

  • DispatcherServlet은 viewResolver 목록을 가지고 있고, 핸들러 어댑터를 통해 반환된 논리적인 뷰 이름을 가지고, viewResolver 목록을 순회하며 view를 생성을 시도

  • 다른 뷰는 실제 뷰를 렌더링하지만, JSP의 경우 forward() 통해서 해당 JSP로 이동(실행)해야 렌더링이 된다 ( html로 된 화면이 그려진다 )

  • JSP를 제외한 나머지 뷰 템플릿들은 forward() 과정 없이 바로 렌더링




6. 스프링 MVC

6-1. @RequestMapping

  • @RequestMapping어노테이션을 사용하는 컨트롤러

    • RequestMappingHandlerMapping : 핸들러 매핑 ( 컨트롤러를 찾는다 )

    • RequestMappingHandlerAdapter : 핸들러 어댑터 ( 컨트롤러를 처리할 수 있는 어댑터를 찾는다 )


6-2. @RequestMapping@Controller

@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.addObject("member", member);
        return mv;
    }
}
  • RequestMappingHandlerMapping

    • 스프링 빈 중에서 @RequestMapping 또는 @Controller 가 클래스 레벨에 붙어 있는 경우에 핸들러 매핑 정보로 인식

    • 스프링 부트 3.0(스프링 프레임워크 6.0)부터는 클래스 레벨에 @RequestMapping이 있어도 스프링 컨트롤러로 인식하지 않는다

    • 오직 @Controller가 있어야 스프링 컨트롤러로 인식한다

      • @RestController는 해당 애노테이션 내부에 @Controller를 포함하고 있으므로 인식 된다
  • @Controller

    • 스프링이 자동으로 스프링 빈으로 등록 (내부에 @Component 애노테이션이 있어서 컴포넌트 스캔의 대상이 됨)

    • 스프링 MVC에서 어노테이션 기반 컨트롤러로 인식

    • @Controller 대신 @Component + @RequestMapping를 사용해도 됨

  • @RequestMapping

    • 요청 정보를 매핑한다

    • 메소드 단위이기 때문에 하나의 Controller에 메소드를 여러 개 넣을 수 있음

      • 해당 URL이 호출되면 어노테이션이 붙은 메서드가 호출
    • URL이 중복되기 때문에 클래스 레벨에 @RequestMapping 어노테이션을 붙여서 중복 제거 가능

      • 클래스 레벨의 @RequestMapping과 메서드 레벨의 @RequestMapping가 합쳐져서 매핑이 된다
  • ModelAndView

    • 모델과 뷰 정보를 담아서 반환

    • view의 논리 이름으로 ModelAndView 객체 생성

    • addObject()로 model에 정보 추가


6-3. 개선 사항

@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {

    private MemberRepository memberRepository = MemberRepository.getInstance();

    @GetMapping("/new-form")
    public String newForm() {

        return "new-form";
    }


    @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";
    }

}
  1. @RequestMapping 개선

    • @RequestMapping을 가지고 있는 @GetMapping, @PostMapping을 사용해서 요청 정보를 매핑할 수 있다
  1. 반환형 개선

    • ModelAndView를 반환하는 것이 아니라 view 이름( String )으로 반환해도 된다

    • 스프링이 반환되는 String을 view 이름으로 인식하고 진행

  1. HttpServletRequest 개선

    • @RequestParam으로 요청 파라미터를 직접 받을 수 있다

    • @RequestParam("username")

    • request.getParameter("username")의 기능을 함

  1. Model 개선

    • ModelAndView에 데이터를 담는게 아니라 Model을 사용해서 데이터를 담을 수 있음

    • model.addAttribute()로 추가 가능

profile
공부한 내용을 정리해서 기록하고 다시 보기 위한 공간

0개의 댓글