IT recording...

[스프링 MVC1] 5. MVC 패턴 본문

Spring

[스프링 MVC1] 5. MVC 패턴

I-one 2022. 1. 14. 12:45

원문 링크

https://adorable-aspen-d23.notion.site/MVC1-5-MVC-7377de4e5388412ab6d5988e16dc0a8f

 

[스프링 MVC1] 5. MVC 패턴

스프링 MVC 구조 살펴보기

adorable-aspen-d23.notion.site

김영한님의 [스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술] 강의를 듣고 작성한 글입니다.

 

스프링 MVC 구조 살펴보기

→ 우리가 만들었던 프레임워크와 스프링MVC는 매우 유사한 모양을 띄는 것을 알 수 있다.

동작 순서

  1. 핸들러 조회 : 핸들러 매핑을 통해 요청 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
  2. 핸들러 어댑터 조회 : 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다.
  3. 핸들러 어댑터 실행 : 핸들러 어댑터를 실행한다. (모양 맞추기 위해)
  4. 핸들러 실행 : 핸들러 어댑터가 실제 핸들러를 실행한다.
  5. ModelAndView반환 : 핸들러 어댑터는 핸들러가 반환하는 정보를 ModelAndView로 변환해서 반환한다.
  6. ViewResolver호출 : 뷰 리졸버를 찾고 실행한다.
  7. View 반환 : 뷰 리졸버는 뷰의 논리 이름을 물리 이름으로 바꾸고, 렌더링 역할을 담당하는 뷰 객체를 반환한다.
  8. 뷰 렌더링 : 뷰를 통해서 뷰를 렌더링한다.

1. Dispatcher Servlet

: 기존에 FrontController의 역할을 한다.

  • HttpServlet을 상속 받아 사용하며 서블릿을 동작한다.
  • 스프링 부트는 DispatcherServlet을 서블릿으로 자동으로 등록하면서, 모든 경로 (urlPatterns=”/”)에 대해 매핑한다. 따라서 모든 요청은 dispatcher servlet을 통해 들어오게 된다.
    • DispathcerServlet.doDispatch()함수
protected void doDispatch(HttpServletRequest request, HttpServletResponse
response) throws Exception {
	HttpServletRequest processedRequest = request;
	HandlerExecutionChain mappedHandler = null;
	ModelAndView mv = null;

	**// 1. 핸들러 조회
	mappedHandler = getHandler(processedRequest);**
	if (mappedHandler == null) {
	noHandlerFound(processedRequest, response);
	return;
	}

	**// 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);**
}

2. HandlerMapping, HandlerAdapter

** 스프링은 이미 필요한 핸들러 매핑과 핸들러 어댑터를 대부분 구현해두었으므로,

개발자가 직접 핸들러 매핑과 핸들러 어댑터를 구현하는 일은 거의 없다.

1) HandlerMapping

: 요청된 주소를 통해서 들어와서 어떤 controller(handler)가 실행이 되어야 하는지를 확인한다.

0 = **RequestMappingHandlerMapping** : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용

1 = BeanNameUrlHandlerMapping : 스프링 빈의 이름으로 핸들러를 찾는다.

2) HandlerAdapter

: 찾은 controller(handler)를 처리할 수 있는 adapter가 존재하는지를 확인한다.

0 = **RequestMappingHandlerAdapter** : 애노테이션 기반의 컨트롤러인 @RequestMapping에서 사용

1 = HttpRequestHandlerAdapter : HttpRequestHandler 처리

2 = SimpleControllerHandlerAdapter : Controller 인터페이스(과거 사용)

** 실무의 99.9%는 @RequestMapping이 사용하는 RequestMappingHandlerMapping, RequestMappingHandlerAdapter를 사용한다.

example

: 스프링MVC 패턴에서 HandlerMapping과 HandlerAdapter가 어떤 방식으로 찾아지고 동작하는지 예시를 살펴보자.

  1. OldController
@Component("/springmvc/old-controller") //spring 빈의 이름을 이렇게 지정(urlPattern이랑 맞춤)
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");
    }
}

: @Component("/springmvc/old-controller") 로 스프링 빈으로 등록되었으므로, 빈의 이름으로 URL을 매핑한다.

    1. 핸들러 매핑으로 핸들러 조회
    • HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다.
    • 빈의 이름으로 URL이 등록되었으므로 BeanNameUrlHandlerMapping이 실행에 성공하고 핸들러인 OldController를 반환한다.
    1. 핸들러 어댑터 조회
    • HandlerAdapter의 supports()를 순서대로 호출한다.
    • OldController은 Controller를 상속받은 형태인데, SimpleControllerHandlerAdapter가 Cotroller 인터페이스를 지원하므로 대상이 된다.
    1. 핸들러 어댑터 실행
    • 디스패처 서블릿이 조회한 SimpleControllerAdapter를 실행하면서 핸들러 정보도 함께 넘겨준다.
    • 어댑터는 핸들러인 OldController를 내부에서 실행하고 그 결과를 반환한다.
    1. 그 후 view 등 공통 처리를 진행한다.
  1. HttpRequestHandler
@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");
    }
}

: @Component("/springmvc/request-handler") 로 스프링 빈으로 등록되었으므로, 빈의 이름으로 URL을 매핑한다.

    1. 핸들러 매핑으로 핸들러 조회
    • HandlerMapping을 순서대로 실행해서, 핸들러를 찾는다.
    • 빈의 이름으로 URL이 등록되었으므로 BeanNameUrlHandlerMapping이 실행에 성공하고 핸들러인 MyHttpRequestHandler를 반환한다.
    1. 핸들러 어댑터 조회
    • HandlerAdapter의 supports()를 순서대로 호출한다.
    • HttpRequestHandlerAdapter가 HttpRequestHandler 인터페이스를 지원하므로 대상이 된다.
    1. 핸들러 어댑터 실행
    • 디스패쳐 서블릿이 조회한 어댑터를 실행하면서 핸들러 정보도 함께 넘겨준다.
    • 어댑터는 핸들러인 MyHttpRequestHandler를 내부에서 실행하고, 그 결과를 반환한다.
    1. 그 후 view등 공통 처리를 진행한다.

3. ViewResolver

  • application.properties
**spring.mvc.view.prefix**=/WEB-INF/views/
**spring.mvc.view.suffix**=.jsp

→ 스프링 부트는 InternalResourceViewResolver라는 뷰 리졸버를 자동으로 등록하는데, 이때 이 정보를 이용해서 등록한다.

(따라서 뷰의 논리 이름으로 사용할 수 있다.)

1 = BeanNameViewResolver : 빈 이름으로 뷰를 찾아서 반환한다. (예: 엑셀 파일 생성 기능에 사용)

2 = InternalResourceViewResolver : JSP를 처리할 수 있는 뷰를 반환한다.
    1. 핸들러 어댑터 호출
    • 핸들러 어댑터를 통해 ‘new-form’ 이라는 뷰의 논리 이름을 획득한다.
    1. ViewResolver 호출
    • new-form이라는 뷰 이름으로 viewResolver를 순서대로 호출한다.
    • BeanNameViewResolver는 new-form이라는 이름의 스프링 빈으로 등록된 뷰를 찾아야 하는데 없다.
    • 따라서 InternalResourceViewResolver가 호출된다.
    1. InternalResouceVeiwResolver
    • 뷰 리졸버는 InternalResourceView를 반환한다.
    • InternalResourceView는 JSP처럼 포워드를 호출해서 처리할 수 있는 경우에 사용한다.
    1. view.render()
    • view.render()가 호출되고 InternalResourceView는 forward()를 사용해서 JSP를 실행한다.

스프링 MVC 진짜 사용해보자!

: 스프링이 제공하는 컨트롤러는 애노테이션 기반으로 동작한다 (짱짱맨)

→ 매우 유연하고 실용적이다.

@RequestMapping

  • RequestMappingHandlerMapping
  • RequestMappingHandlerAdapter

1. 기존 프레임워크 → @RequestMapping 기반 스프링MVC - V1

**@Controller**
public class SpringMemberFormControllerV1 {
    **@RequestMapping("/springmvc/v1/members/new-form")**
    public ModelAndView process(){
        return new ModelAndView("new-form"); //자동으로 viewResolver가 view 반환해줌
    }
}
  • @Controller
    • 스프링이 자동으로 빈으로 등록한다. (내부에 @Component가 있기 때문)
    • 스프링 MVC에서 애노테이션 기반 컨트롤러로 인식한다.
  • @RequestMapping
    • 요청 정보를 매핑한다.
  • ModelAndView
    • 모델과 뷰 정보를 담아 반환한다.

** RequestMappingHandlerMapping은 스프링 빈 중에서 @RequestMapping 또는 @Controller가 클래스 레벨에 붙어 있는 경우에 매핑 정보로 인식한다.

**@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.addObject("member",member);
        return mv;**
    }
}
**@Controller**
public class SpringMemberListControllerV1 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    **@RequestMapping("/springmvc/v1/members")**
    protected ModelAndView process(){
        List<Member> members = memberRepository.findAll();

        **ModelAndView mv = new ModelAndView("members");
        mv.addObject("members",members);
        return mv;**
    }
}

2. 스프링 MVC - 컨트롤러 통합

: @RequestMapping이 메서드 단위이므로 컨트롤러 클래스를 유연하게 하나로 통합할 수 있다.

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

    **@RequestMapping("/new-form")**
    public ModelAndView newForm(){
        return new ModelAndView("new-form"); //자동으로 viewResolver가 view 반환해줌
    }

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

    **@RequestMapping**
    protected ModelAndView members(){
        List<Member> members = memberRepository.findAll();

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

3. 스프링 MVC - 실용적인 방식

변화된 것

  1. 각 RequestMapping 에서 인자로 request, response를 받았었는데 이렇게 하지 않고
    • @RequestParam을 이용하여 request를 받고,
    • model을 이용하여 response를 전달할 수 있다.
  2. View의 논리적인 이름만 반환한다.

⇒ RequestMappingAdapter에서 Model을 파라미터로 받는 방식, @RequestParam을 사용하는 방식, 뷰의 논리적인 이름을 리턴하는 방식을 모두 지원하므로 가능하다.

⇒ 각 컨트롤러에서 이와 같이 넘겨주어도 RequestMappingAdapter에서 해당 내용을 변환해서 DispatcherServlet이 동작하므로 정상적으로 동작 가능하다.

  1. GET,POST,PUT,DELETE 등 각 요청은 목적이 분명하다.
    • @GetMapping(”/주소”)
    • @PostMapping(”/주소”)
@Controller
@RequestMapping("/springmvc/v3/members")
public class SpringMemberControllerV3 {
    private MemberRepository memberRepository = MemberRepository.getInstance();

    //@RequestMapping(value = "/new-form", method = RequestMethod.GET) // GET일때만 호출됨
    **@GetMapping("/new-form")**
    public String newForm(){
        //그냥 문자를 반환해도 view이름으로 알고 알아서 진행해줌
        **return "new-form";** //자동으로 viewResolver가 view 반환해줌
    }

    //@RequestMapping(value = "/save", method = RequestMethod.POST)
    @PostMapping("/save")
    public String save(**@RequestParam("username") String username, //httprequest이런거 안받아도 됨(애노테이션 강점)
                             @RequestParam("age") int age,
                             Model model**){
        //비즈니스 로직
        Member member = new Member(username,age);
        memberRepository.save(member);

        //모델에 담기
        **model.addAttribute("member",member);
        return "save-result";**
    }

    //@RequestMapping(method = RequestMethod.GET)
    @GetMapping
    protected String members(Model model){
        //비즈니스 로직
        List<Member> members = memberRepository.findAll();

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