[Spring] MVC

한호성·2022년 8월 7일
0

글의 목적


Spring framework 의 Presentation layer 에서 사용되는 MVC Pattern에 대해 공부해보자.

MVC 패턴 -개요


등장배경

너무 많은 역할

하나의 서블릿을 통해 비지니스 로직과 뷰 렌더링을 모두 처리하게 되면, 하나의 영역에서 너무 많은 역할을 부담지게 되고, 이는 필연적으로 유지보수가 어려워지는 결과를 만든다.
생각해보자, 비지니스 로직과 뷰 렌더링 기타 등등..을 수정해야 하는 상황에서 매번 하나의 자바 파일에서 코드를 수정해야하는데, 엄청나게 힘들 것으로 생각된다.

Model View Controller

  • Controller : HTTP 요청을 받아 파라미터를 검증하고 비지니스 로직을 실행한다. 그리고 뷰에 전달할 결과 데이터를 조회해서 모델에 담는다.

  • Model : 뷰에 출력할 데이터를 담아둔다. 뷰가 필요한 데이터를 모두 모델에 담아 전달해주기 때문에, 뷰는 비지니스 로직이나 데이터 접근에 대해 알 필요 없고 화면 렌더링에만 집중하면 된다.

  • View : 모델에 담겨있는 데이터를 사용해 화면을 그리는일에 집중한다. 여기서 HTML을 생성하는 부분을 말한다.

#cf) MVC만 보면 비지니스 로직을 전부 컨트롤러에 두는 것으로 보일 수 있다. 그렇기 때문에, 일반적으로 Servcie layer를 두고 거기서 비지니스 로직을 따로 만들어 처리한다.

아래 사진은 스프링 웹 MVC에서 사용되는 구조이다

프론트 컨트롤러 패턴

기존의 MVC Pattern만을 사용한 경우 공통부분의 처리가 힘들고, 많은 부분이 중복되서 사용한다는 단점이 있다. 이를 해결하기 위해서 프론트 컨트롤러 패턴을 사용한다. 컨트롤러 패턴은 서블릿 하나로 클라이언트의 요청을 받아 이 프론트 컨트롤러가 요청에 맞는 컨트롤러를 찾아서 호출을 해주는 역할로 공통 영역을 한군대에 몰아넣을 수 있다고 한다.

실제로 내가 사용하고 있는 Spring에서도 DispathcerServlet이 이 역할을 해주고 있다.

두 사진으로 각 패턴을 비교해보자

기존 mvc pattern

프론트 컨트롤러 패턴

Spring MVC 구조

각각 어떤 역할을 하는지 알아보자

DispatcherServlet 구조

이 서블릿도 부모 클래스에서 HttpServlet상속 받아 사용한다.
스프링 부트 구동시 DispatcherServelet을 서블릿으로 자동등록하며, 모든 경로(urlPatterns::"/")에 대해 맵핑한다.

요청흐름

  1. 서블릿이 호출되면 HttpServlet이 제공하는 service() 메서드 호출
  2. 스프링 MVC는 DispatcherServlet의 부모인 FrameworkServlet에서 serivce()를 오버라이드 했다.
  3. FrameworkSErvlet.service()를 시작으로 여러 매소드가 실행되며 DispatcherServlet.doDispatch()가 호출된다.

DispatcherServlet.doDispatch()

메서드 이름대로 해당 메서드에서 적절한 콘트롤러를 찾아 매핑해주고 뷰까지 찾아 view를 반환해 렌더링까지 해주는 메서드이다.

이 메소드가 핵심역할을 한다고 해서.. 일단 Debugging 모드를 통해서 해당 메소드를 가져와 봤따..

	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HttpServletRequest processedRequest = request;
		HandlerExecutionChain mappedHandler = null;
		boolean multipartRequestParsed = false;

		WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

		try {
			ModelAndView mv = null;
			Exception dispatchException = null;

			try {
				processedRequest = checkMultipart(request);
				multipartRequestParsed = (processedRequest != request);

				// Determine handler for the current request.
				mappedHandler = getHandler(processedRequest);
				if (mappedHandler == null) {
					noHandlerFound(processedRequest, response);
					return;
				}

				// Determine handler adapter for the current request.
				HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

				// Process last-modified header, if supported by the handler.
				String method = request.getMethod();
				boolean isGet = HttpMethod.GET.matches(method);
				if (isGet || HttpMethod.HEAD.matches(method)) {
					long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
					if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
						return;
					}
				}

				if (!mappedHandler.applyPreHandle(processedRequest, response)) {
					return;
				}

				// Actually invoke the handler.
				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

				if (asyncManager.isConcurrentHandlingStarted()) {
					return;
				}

				applyDefaultViewName(processedRequest, mv);
				mappedHandler.applyPostHandle(processedRequest, response, mv);
			}
			catch (Exception ex) {
				dispatchException = ex;
			}
			catch (Throwable err) {
				// As of 4.3, we're processing Errors thrown from handler methods as well,
				// making them available for @ExceptionHandler methods and other scenarios.
				dispatchException = new NestedServletException("Handler dispatch failed", err);
			}
			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
		}
		catch (Exception ex) {
			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
		}
		catch (Throwable err) {
			triggerAfterCompletion(processedRequest, response, mappedHandler,
					new NestedServletException("Handler processing failed", err));
		}
		finally {
			if (asyncManager.isConcurrentHandlingStarted()) {
				// Instead of postHandle and afterCompletion
				if (mappedHandler != null) {
					mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
				}
			}
			else {
				// Clean up any resources used by a multipart request.
				if (multipartRequestParsed) {
					cleanupMultipart(processedRequest);
				}
			}
		}
	}

아래의 내용은 참고한 블로그에서 가져온 내용이다.. 이런 역할들을 한다는 것을 알 수 있었다.

  • mappedHandler = getHandler(processedRequest);
    ⇒ 요청에 맞는 적절한 핸들러를 찾아 반환해준다.
  • noHandlerFound(processedRequest, response);
    ⇒ 적절한 핸들러를 찾지 못한경우 404 에러코드를 반환해준다.
  • HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
    ⇒ 찾은 핸들러를 처리할 수 있는 핸들러 어댑터를 찾아준다.
    ⇒ 만약 찾지 못할경우 ServletException 발생
  • mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    ⇒ 찾은 핸들러 어댑터를 이용해 로직을 수행하는 handle 메서드를 호출한다.
    ⇒ 결과로 ModelAndVIew를 반환받고, 이를 이용해 렌더링까지 수행된다.
  • processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    ⇒ 실제 코드는 복잡하게 되있는데 결국 render() 메서드를 호출해준다.
    ⇒ render() 에서는 ModelAndView에서 View를 찾아 뷰 리졸버를 이용해 뷰의 물리적 이름을 완성해서 forward 해준다.

Spring MVC 동작 순서

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

#cf)
1. MVC에 대해 조사하면서 Servlet에 대해 이야기가 많이 나왔다. 솔직히 많이 들어보고, 변수명으로 사용되는것은 본적있지만, 실제로 이 개념에 대해 알지 못했고 간단히 조사해본 결과 springboot 의 내장되어있는데 톰캣 서버 에서 처리하는 무엇인가로 생각된다. 이건 꼭 조사해서 따로 이해하는 시간을 갖도록 해야겠다.
2. Spring MVC에서 사용하는 주요 인터페이스에 대해서도 알아볼 필요가 있겠다. 필요하면 ,custom하고 오류가 날 때, debugging에 유리할 것으로 생각된다.

Reference

https://catsbi.oopy.io/441b4af6-e877-4dc5-9695-2983bbe22799#e64f58ba-c6cd-4697-b039-544e77541d03

https://nyximos.tistory.com/61

profile
개발자 지망생입니다.

0개의 댓글