1. 개요
이번 게시물에서는 MVC 패턴에 대한 소개, 장점에 대해서 설명할 것이다. 그 후, 스프링은 이 MVC 구조를 어떻게 구현하였는지를 알아보기위해 이와 비슷한 MVC 프레임워크를 만들어 볼 것이다.
2. MVC 패턴의 등장 이유
여러 이유로 인하여 서블릿으로만, 혹은 JSP로만 모든 비즈니스 로직과 뷰 렌더링을 처리해 본 사람들은 알 것이다. 하나의 파일이 너무 많은 역할을 하게 되고, 비즈니스 로직 변경 혹은 UI 변경을 할 일이 있을 경우 모두 함께 있는 파일을 수정해야 한다.
이때, 가장 큰 문제는 UI와 비즈니스 로직은 변경의 라이프 사이클이 전혀 다르다는 점이다. UI를 일부 수정하거나 비즈니스 로직을 수정하는 일은 개별적으로 주로 발생하며, 서로에게 영향을 주지 않는 경우가 많다. (물론 아닌 경우도 있음)
결국 특화된 기능이 아닌 일을 하게 된다. JSP는 화면을 렌더링하는데 최적화되어 있으며, 서블릿은 비즈니스 로직 실행에 특화되어 있다. 결과적으로 유지보수가 어려워진다는 단점이 있다.
간단하게 불편한 점을 예시를 들어보겠다. 서블릿에서 뷰 렌더링을 처리해야 하는 경우,
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
// 비즈니스 코드 작성 후
PrintWriter w = response.getWriter();
w.write("<html>\n" +
"<head>\n" +
" <meta charset=\"UTF-8\">\n" +
"</head>\n" +
"<body>\n" +
"성공\n" +
"<ul>\n" +
" <li>id="+member.getId()+"</li>\n" +
" <li>username="+member.getUsername()+"</li>\n" +
" <li>age="+member.getAge()+"</li>\n" +
"</ul>\n" +
"<a href=\"/index.html\">메인</a>\n" +
"</body>\n" +
"</html>");
}
이런식으로 처리해야 하는데, 상당히 비효율적이라는 것을 확인 할 수 있다.
만일 JSP에서 비즈니스 로직을 처리해야 하는 경우,
<%@ page import="hello.servlet.domain.member.MemberRepository" %>
<%@ page import="hello.servlet.domain.member.Member" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%
// request, response 사용 가능
MemberRepository memberRepository = MemberRepository.getInstance();
System.out.println("save.jsp");
String username = request.getParameter("username");
int age = Integer.parseInt(request.getParameter("age"));
Member member = new Member(username, age);
System.out.println("member = " + member);
memberRepository.save(member);
%>
<%--뷰 렌더링 코드 작성--%>
아래 뷰 렌더링 코드가 있음에도 불구하고, 위에 비즈니스 로직과 관련된 코드가 쭉 나열되어있다.
3. MVC(Model View Controller)
MVC 패턴은 컨트롤러와 뷰라는 영역으로 서로 역할을 나누고, 모델을 통해서 데이터를 전달하는 방식을 말한다. 웹 애플리케이션은 보통 이 MVC 패턴을 사용한다.
MVC 패턴을 도입하기 전에는, 위의 그림처럼 비즈니스 로직과 뷰 로직이 같이 존재하였다.
- 컨트롤러: HTTP 요청을 받아 파라미터를 검증하고, 비즈니스 로직을 담당한다. 그 후, 뷰에 전달할 결과를 조회해서 모델에 담는다.
- 모델: 뷰가 필요한 데이터가 모두 담겨 전달된다. 이 덕분에 뷰는 비즈니스 로직을 몰라도 되고, 화면 렌더링에 집중 할 수 있다.
- 뷰: 모델에 담겨있는 데이터를 사용해서 화면을 구성하는 일에 집중한다.
위 그림을 보면, 컨트롤러가 아닌 서비스/리포지토리 에서 비즈니스 로직과 데이터 접근을 진행한다. 그 이유는 컨트롤러가 모두 담당하기에는 역할이 너무 많기 때문에, 서비스, 리포지토리 계층을 별도로 만들어서 처리하게 된다.
4. 스프링 MVC 구조 이해
사실 스프링 MVC 내부 구조를 모르고 있어도, 코딩이 가능하다. 스프링 MVC 구조의 목적 자체가 해당 방식을 모르더라도, 다양한 형태의 컨트롤러를 실행시키는데 있기 때문이다.
하지만, 내부 구조를 조금이나마 이해하고 있으면 여러 상황에 대해 유연하게 대처할 수 있다. 가령 에러가 발생하여 해결할 때, 전체적인 흐름을 알고 있다면 더 빠르게 대처할 수 있을 것이다.
스프링 MVC 구조를 살펴보자.
다양한 구성요소들이 있는데, 이를 하나하나씩 살펴보자.
1) Dispatcher Servlet
우선 Dispatcher Servlet를 설명하기 위해서는, Front Controller 개념을 먼저 알고 있어야 한다.
<Front Controller>
위 코드는 Front Controller가 적용되지 않았을 때 컨트롤러가 호출되는 형식이다. 모든 컨트롤러가 클라이언트의 요청을 받는데, 이때 공통되는 코드가 발생할 수 밖에 없다. (View로 이동, 한글 인코딩 등등...)
하지만 Front Controller를 생성하여 모든 클라이언트의 요청을 받는다면? 공통된 요소를 Front Controller가 담당하게 된다! Front Controller가 서블릿 하나로 클라이언트의 요청을 받고, 요청에 맞는 컨트롤러를 찾아서 호출하면 된다.
스프링 웹 MVC의 DispatcherServlet이 바로 이 FrontController 패턴으로 구현되어 있다.
자, 그런데 DispatcherServlet에서 공통적인 내용을 처리한 후, 어떤 컨트롤러로 보내야 할지 판단해야 한다. 이를 확인하기 위해 다음과 같은 과정을 거치게 된다.
2) 핸들러 매핑
우선, Dispatcher Servlet에서는 핸들러 조회를 진행한다. 핸들러 매핑을 통해 요청된 URL에 매핑된 핸들러(컨트롤러)를 조회한다.
이때, 스프링에서는 핸들러를 아래와 같은 우선순위로 찾게 된다. 실제로는 더 많은 검색 방식이 있으나, 일부 생략되었다.
최근에는 @RequestMapping 애노테이션을 기반으로 핸들러를 주로 구성하므로, 우선순위가 높다는 사실을 확인할 수 있다.
3) 핸들러 어댑터 목록
그 후, 핸들러를 실행할 수 있는 핸들러 어댑터를 조회한다. 핸들러 어댑터가 무엇인지는 다음 과정에서 자세히 설명할 것이며, 여기서는 어떻게 찾는지 위주로 살펴보자.
스프링에서는 핸들러 어댑터 또한 아래와 같은 우선순위로 찾게 된다. 마찬가지로 일부 생략되었다.
해당 핸들러를 처리할 수 있는 핸들러 어댑터를 찾았다면, 다음 과정으로 넘어간다.
4) 핸들러 어댑터
핸들러 어댑터가 실행되고, 이는 핸들러를 호출한다. 이때, DispatcherServlet이 바로 핸들러를 호출하지 않고, 중간에 핸들러 어댑터를 두는 이유가 뭘까?
스프링은 개발자에게 핸들러(컨트롤러)를 구성할 수 있는 여러가지 방식을 지원한다. @RequestMapping, HttpRequestHandler 상속, Controller 인터페이스 상속 등등 다양한 방식이 있으며, 각각 방식을 수행하기 위해 그에 맞는 핸들러 어댑터를 미리 구현해 두고, 필요한 어댑터를 사용하는 것이다.
5) 핸들러 실행
핸들러 어댑터에 의해서 실제 핸들러가 실행된다.
6) ModelAndView 반환
핸들어 어댑터는 핸들러가 반환하는 정보를 받아, ModelAndView로 변환해서 반환한다. 핸들러의 컨트롤러가 String으로 반환하든, ModelAndView로 반환하든, 파라미터의 Model 객체에 정보를 담든 어댑터가 이를 알아서 변환해준다.
7) viewResolver 호출
이제 스프링은 논리 뷰 이름을 통해서 뷰를 찾아간다. 마찬가지로 아래와 같은 우선순위를 통해 리졸버를 찾으며, 일부는 생략되었다.
스프링 MVC 패턴으로 프로젝트 생성시 자주 사용하는 Thymeleaf 뷰 템플릿을 사용하면, ThymeleafViewResolver를 등록해주어야 한다. 최근 버전같은 경우 라이브러리만 추가하면, 스프링 부트가 이를 모두 자동화시켜준다.
8) View
핸들러를 거쳐 비즈니스 로직이 반환한 정보들이 ModelAndView에 잘 저장되었으며, View 위치 또한 파악되었다. 마지막으로 정보들을 바탕으로 View가 실행된다.
이 모든 흐름들을 실제 DispacherServlet의 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);
}
결국 실제로 코딩할때는 핸들러(컨트롤러)와 View를 뷰 템플릿을 통해 작성하면, 나머지 과정을 스프링이 알아서 다 해주게 된다. 하지만, 이 전체 흐름을 한번쯤은 알아두는 것이 좋지 않을까 싶다.
위 내용은 김영한 님의 인프런 강의 "스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술"의 내용과 강의자료를 토대로 작성된 게시글입니다.
강의 링크:
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
'Web > spring study' 카테고리의 다른 글
[Spring] Spring Controller 어노테이션 정리 (MVC, REST api) (0) | 2023.05.26 |
---|---|
[Spring] 스프링 MVC 패턴에서 Controller가 Model에 정보 저장하는 방법 정리 (0) | 2023.05.25 |
[Spring] SLF4J 로깅 간단한 정리 (0) | 2023.05.24 |
[Spring] 서블릿, HttpServletRequest, HttpServletResponse (0) | 2023.01.17 |
[Spring] 서블릿, 서블릿 컨테이너 (0) | 2023.01.16 |
댓글