Servlet기반 MVC 구조 및 동작 순서

image

  1. 클라이언트로부터 요청이 Dispatcher Servlet으로 들어온다.
  2. Dispatcher Servlet은 DAO를 통해서 데이터를 가져온다.
  3. Http Session에 DB에서 가져온 데이터를 저장한다.(굳이 이렇게 할 필요는 없음)
  4. Dispatcher Servlet이 해당 뷰로 매칭시킨다.
  5. JSP 뷰에서 Session 객체에 저장되어진 데이터를 가져와 뷰를 구성한다
  6. 구성되어진 뷰는 클라이언트로 다시 보내어진다.

Spring MVC 구조 및 동작 순서

image

  1. 클라이언트에서 서버로 요청을 보내면 제일 먼저 Dispatcher Servlet이 요청을 받는다.
  2. Dispatcher Servlet은 받은 요청 URI를 HandlerMapping으로 보내면 HandlerMapping은 해당 URI와 매핑되는 Controller객체를 받는다.
  3. 매핑된 Controller에서 DAO로 부터 데이터를 받아 business logic을 처리하고 View Name을 리턴한다
  4. View Name은 ViewResolver를 통해서 Prefix나 Suffix등의 붙어지고 해당 뷰는 Client로 보내어진다.

Spring MVC 구조 직접 만들기

  1. web.xml 설정

    1
    2
    3
    4
    5
    6
    7
    8
    <servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    • 모든 요청이 Dispatcher Servlet을 거치도록 설정한다.
    • Dispatcher Serlvet이 모든 클라이언트 요청을 첫 번째로 받는다.
  2. Dispatcher Servlet 구현

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    public class DispatcherServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private HandlerMapping handlerMapping;
    private ViewResolver viewResolver;


    public DispatcherServlet() {
    System.out.println("===> DispatcherServlet 생성");
    }

    @Override
    public void init() throws ServletException {
    handlerMapping = new HandlerMapping();
    viewResolver = new ViewResolver();

    // ViewResolver의 접두사와 접미사를 결정한다
    viewResolver.setPrefix("./");
    viewResolver.setSuffix(".jsp");
    }

    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    // 1. 사용자 요청 path 정보를 추출한다.
    String uri = request.getRequestURI();
    String path = uri.substring(uri.lastIndexOf("/"));
    System.out.println(path);

    // 2. HandlerMapping을 통해 path에 해당하는 Controller를 검색한다.
    Controller ctrl = handlerMapping.getController(path);

    // 3. 검색된 Controller를 실행한다
    String viewName = ctrl.handleRequest(request, response);

    // 4. ViewResolver를 통해 viewName에 해당하는 화면을 검색한다
    String view = null;
    if (!viewName.contains(".do")) {
    if (viewName.contains(".html")){
    view = viewName;
    } else {
    view = viewResolver.getView(viewName);
    }
    } else {
    view = viewName;
    }

    // 5. ViewResolver가 검색해준 화면으로 이동한다.
    response.sendRedirect(view);
    }
    }
    • 요청이 들어오면 Dispatcher Serlvet에서 URI에 해당하는 Controller를 HandlerMapping 객체를 통해 받아온다
    • Controller에서 logic을 처리하고 View Name을 Return해준다
    • Return받은 View Name을 ViewResolver로 다시 재 조합한다
    • 재조합된 View Name으로 응답을 보낸다
  3. Handler Mapping 구현

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class HandlerMapping {
    private Map<String, Controller> mappings;

    public HandlerMapping() {
    // 모든 컨트롤러 객체를 Map에 등록한다
    mappings = new HashMap<String, Controller>();
    // USERS관련 컨트롤러 등록
    mappings.put("/login.do", new LoginController());
    mappings.put("/logout.do", new LogoutController());

    // BOARD 관련 컨트롤러 등록
    mappings.put("/getBoardList.do", new GetBoardListController());
    mappings.put("/insertBoard.do", new InsertBoardController());
    mappings.put("/updateBoard.do", new UpdateBoardController());
    mappings.put("/deleteBoard.do", new DeleteBoardController());
    mappings.put("/getBoard.do", new GetBoardController());
    }

    public Controller getController(String path) {
    // 매개변수로 받은 path에 해당하는 컨트롤러 객체를 검색하여 리턴한다.
    return mappings.get(path);
    }
    }
    • 각 요청에 대한 Controller 객체를 HashMap에 저장한다
    • 요청이 들어오면 HashMap에서 해당 Controller 객체를 가져온다
  4. 해당 Controller 구현

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class InsertBoardController implements Controller {
    @Override
    public String handleRequest(HttpServletRequest request, HttpServletResponse response) {
    String title = request.getParameter("title");
    String writer = request.getParameter("writer");
    String content = request.getParameter("content");

    BoardVO vo = new BoardVO();
    vo.setTitle(title);
    vo.setWriter(writer);
    vo.setContent(content);

    BoardDAO boardDAO = new BoardDAOJDBC();
    boardDAO.insertBoard(vo);

    return "getBoardList.do";
    }
    }
    • DAO에서 요청한 데이터를 가져오는 등의 Business Logic을 진행한다.
    • 응답으로 보내져야할 View Name을 Return한다.
  5. ViewResolver 구현

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    public class ViewResolver {
    //접두사
    private String prefix;

    //접미사
    public String suffix;

    public void setPrefix(String prefix) {
    this.prefix = prefix;
    }

    public void setSuffix(String suffix) {
    this.suffix = suffix;
    }

    public String getView(String viewName) {
    return prefix + viewName + suffix;
    }
    }
    • Controller에서 받아온 View Name에 경로설정과 파일 확장자와 같은 부분을 Prefix나 Suffix로 붙혀서 Return한다

Spring MVC (XML을 통한 설정)

Servlet Container와 Spring Container와의 관계

  • Web이 아닌 Spring Project에서 Spring Container를 구성할 때에는 GenericXmlApplicationContext나 AnnotationApplicationContext와 같은 Class를 통해서 xml이나 java파일을 설정파일로 등록하고 만들었다.

  • 하지만 Spring Web Project는 Spring Container를 구성할 때 XmlWebApplicationContext같은 WebApplicationContext Class로 형성되어진다.

  • Spring Web Project의 Spring Container 형성과정은 Web이 아닌 Spring Project와는 형성과정에 차이가 있다

  • Spring Web Project Container 형성과정

    image

    1. Tomcat과 같은 WAS가 처음 로딩될 때 Servlet Container를 생성한다

    2. Servlet Container를 통해서 DispatcherServlet에 요청이 들어왔을 때 DispatcherServlet이 메모리에 형성된다

    3. DispatcherServlet이 처음 생성되어 질 때 init() 메소드가 호출되어 지는데 그 메소드에 Spring Container 생성 코드가 포함되어져 있다.

    4. XmlWebAppolication과 같은 객체에 Spring Bean 정보가 들어있는 action-servlet.xml을 받아서 Spring Container를 생성한다

      Servlet Container는 Servlet, Filter, Listener와 같은 클래스 밖에 관리를 못하지만, Spring Container는 일반 클래스들도 Bean으로 등록하여 관리할 수 있다.

  1. web.xml에 Spring DispatcherServlet 등록

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <servlet>
    <servlet-name>action</servlet-name>
    <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
    </servlet-class>
    <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/config/presentation-layer.xml</param-value>
    </init-param>
    </servlet>
     
    <servlet-mapping>
    <servlet-name>action</servlet-name>
    <url-pattern>*.do</url-pattern>
    </servlet-mapping>
    • 이전에는 직접 구현한 DispatcherServlet을 web.xml에 등록했다
    • 지금은 Spring Framework에서 만들어진 Dispatcher Servlet을 모든 요청에 매핑되도록 등록했다
    • Spring Container 빈 설정파일은 action-servlet.xml와 같은 이름 기본 등록되어있다
    • 다른 이름의 xml로 바꾸고싶다면 param에 contextConfigLocation을 변경해주면 된다
  2. presentation-layer.xml 설정 (Spring Bean 설정파일)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

    <!-- 모든 컨트롤러 클래스들을 bean으로 등록한다 -->
    <bean id="getBoardList" class="com.rubypaper.web.controller.board.GetBoardListController">
    </bean>
    <bean id="login" class="com.rubypaper.web.controller.user.LoginController">
    </bean>
    <bean id="logout" class="com.rubypaper.web.controller.user.LogoutController">
    </bean>
    <bean id="getBoard" class="com.rubypaper.web.controller.board.GetBoardController">
    </bean>
    <bean id="insertBoard" class="com.rubypaper.web.controller.board.InsertBoardController">
    </bean>
    <bean id="updateBoard" class="com.rubypaper.web.controller.board.UpdateBoardController">
    </bean>
    <bean id="deleteBoard" class="com.rubypaper.web.controller.board.DeleteBoardController">
    </bean>

    <!-- 클라이언트의 요청을 어떤 컨트롤러가 처리할지 HandlerMapping으로 매핑한다 -->
    <!-- handleMapping id 고정 (spring에서 등록한 bean은 마지막 단어 두개 중 첫 단어만 소문자로 변경해서 id로 등록)-->
    <bean id="handlerMapping" class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
    <property name="mappings">
    <props>
    <prop key="/login.do">login</prop>
    <prop key="/logout.do">logout</prop>
    <prop key="/getBoardList.do">getBoardList</prop>
    <prop key="/getBoard.do">getBoard</prop>
    <prop key="/insertBoard.do">insertBoard</prop>
    <prop key="/updateBoard.do">updateBoard</prop>
    <prop key="/deleteBoard.do">deleteBoard</prop>
    </props>
    </property>
    </bean>

    <!-- View Resolver를 등록한다
    브라우저는 절대 서버가 관리하는 프로젝트의 WEB-INF폴더에 접근할 수 없다.
    따라서 브라우저가 직접 접근해서는 안되는 파일은 WEB-INF폴더에 은닉한다.
    -->
    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/WEB-INF/board/"/>
    <property name="suffix" value=".jsp"/>
    </bean>

    </beans>
    • 모든 컨트롤러 클래스를 bean으로 등록한다
    • 클라이언트 각 요청의 URI마다 어떤 Controller가 처리할 지 Spring HandlerMapping으로 매핑한다
    • Spring ViewResolver을 등록한다
    • 브라우저는 WEB-INF 폴더에 접근할 수 없으므로 브라우저가 접근하면 안되는 파일은 WEB-INF 디렉토리에 은닉한다
  3. Spring Controller Interface를 구현한 Controller 클래스 생성

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class LoginController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) {
    String id = request.getParameter("id");
    String password = request.getParameter("password");

    UserVO vo = new UserVO();
    vo.setId(id);
    vo.setPassword(password);

    UserDAO userDAO = new UserDAOJDBC();
    UserVO user = userDAO.getUser(vo);

    ModelAndView mav = new ModelAndView();
    if (user != null) {
    HttpSession session = request.getSession();
    session.setAttribute("user", user);
    // forward: 이나 redirect: 을 뷰 이름 앞에 붙이면 ViewResolver를 무시한다
    mav.setViewName("forward:getBoardList.do");
    } else {
    mav.setViewName("redirect:login.jsp");
    }
    return mav;
    }
    }
    • org.springframework.web.servlet.mvc.Controller의 구현체 클래스를 만든다
    • 이전에는 View Name을 String 값으로 return했다면 지금은 ModelAndView 객체로 Return한다
    • Spring ViewResolver를 거치지 않고 ViewName을 설정하려면 redirect: 나 forward: 를 ViewName에 추가한다

Spring MVC (Annotation을 통한 설정)

  1. ComponentScan 설정

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/context
    http://www.springframework.org/schema/context/spring-context-4.2.xsd">
     
    <!-- 컴포넌트 스캔 설정 -->
    <context:component-scan base-package="com.rubypaper"/>>
    </beans>
    • @Controller를 Bean으로 인식하기 위해서 ComponentScan을 할 package를 지정한다
    • xml로 component scan을 설정하지 않고 @ComponentScan Annotation을 시작 패키지안의 클래스에 붙여줘도 된다
  2. Controller 설정

    1
    2
    3
    4
    5
    6
    7
    8
    @Controller
    public class InsertBoardController {
     
    @RequestMapping(value="/insertBoard.do")
    public void insertBoard(BoardVO vo) {

    }
    }
    • Controller Class에 @Controller Annotation을 추가해서 Spring Container에게 Controller클래스로 Bean을 등록한다

    • Controller Class안의 메소드에 매핑할 URI값을 @RequsetMapping(value = “URI”)로 설정한다

    • Controller 메소드의 Parameter 매핑
      image

      • Client에서 input 태그의 name값으로 요청이 들어오면 매핑되는 Method의 파라미터값에서 name과 매핑되는 setter메소드를 통해 값이 바인딩된다
    • 요청 방식에 따른 @RequestMapping 사용

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      @Controller
      public class LoginController {
      @RequestMapping(value="/login.do", method=RequestMethod.GET)
      public String loginView(UserVO vo) {
      vo.setId("test");
      vo.setPassword("test123");
      return "login.jsp";
      }
       
      @RequestMapping(value="/login.do", method=RequestMethod.POST)
      public String login(UserVO vo, UserDAO userDAO) {
      if(userDAO.getUser(vo) != null) return "getBoardList.do";
      else return "login.jsp";
      }
      }
    • @RequestMapping에서 매핑될 URI를 value로 정하고 Http Method를 method값에 지정한다

    • @RequestMapping은 Http Method에 따라서 @GetMapping, @PostMapping등으로 사용될 수 있다

Layer 통합하기 (Presentation-Layer, Business-Layer)

DispatcherServlet를 통해 만들어진 Spring Container만 사용

image

  • 요청이 들어오면 DispatcherServlet이 생성되고 생성될 때 presentation-layer.xml을 가져와서 xml에 설정되어진 Bean들을 가지고 Spring Container가 생성된다
  • 해당 Spring Container에는 Controller Bean들만 등록되어지고 직접 DAO에 접근해서 로직을 처리한다

Service를 통해서 Controller가 접근 시 문제점

image

  • Controller를 직접 DAO로 접근시키지 않고 Service Class를 통해서 접근한다
  • 하지만 presentaion-layer.xml에 등록되어진 Bean은 Controller Bean 밖에 없다
  • 따라서 해당 Spring Container는 Service Bean들에 접근할 수 없다

Layer를 분리하여 연결

  • Presentation Layer와 Business Layer를 분리한다
    image

    • Controller에 해당하는 부분은 Presentation Layer로 분리한다
    • DAO, Service에 해당하는 부분은 Business Layer로 분리한다
    • Controller는 분리된 두개의 개층을 Service Class를 통해서 접근한다
  • Business Layer용 Spring Container 생성

    • ContextLoaderListener 등록
      1
      2
      3
      4
      5
      6
      7
      8
      9
      <context-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>classpath:business-*.xml</param-value>
      </context-param>
      <listener>
      <listener-class>
      org.springframework.web.context.ContextLoaderListener
      </listener-class>
      </listener>
      • Servlet 설정 파일인 web.xml에 ContextLoaderListener를 등록한다
      • 해당 Listener는 business layer 설정파일 xml을 가지고 있다
      • ContextLoaderListener는 ServletContextListener를 구현하고 있다.
      • ServletContextListener는 Servlet Container가 시작하거나 종료 될 때 동작하는 Listenr이다.
      • 따라서 ContextLoaderListener는 Servlet Container가 시작할 때 contextLoader로 business layer xml 정보를 가지고 WebApplicationContext를 만들어서 Spring Container를 생성한다
  • Container간의 관계
    image

    • Root Spring Container가 ContextLoaderListener를 통해서 생성된 Container로 Service와 DAO Bean들을 가지고 있다

    • 하위 Spring Contaienr를 DispatcherServlet을 통해서 생성된 Container로 Controller를 Bean으로 가지고 있다

    • 하위 Container는 Root Container의 Bean들을 상속받아서 사용한다

    • 이제 Controller는 Root Spring Container에 생성되어진 Service Bean에 접근이 가능해진다

  • Container 생성과정

    1. Tomcat Server를 가동하면 Servlet Container가 생성된다

    2. Servlet Container가 생성될 때 ContextLoadListener가 동작해여 businees layer xml 설정정보를 가지고 Spring Container가 생성된다

    3. Server가 가동된 이후에 Client를 통해서 요청이 들어오면
      DispatcherServlet이 메모리에 생성되어진다

    4. DispatcherServlet이 생성되어 초기화 될 때 presentation layer xml 설정정보를 가지고 또 다른 Spring Container를 생성한다

      Web Application에서 생성되어진 Container는 Servlet Container, Businees Spring Container, Presentation Layer Container 총 3개이다