[WEB] MVC 패턴 | 서버 START부터 동작 순서 | 구조도

웹 애플리케이션 개발 모델2로 불리는 MVC 패턴은 반드시 알아야 하는 중요 개념이다. 차근차근 이해해보자.

  • 동작 순서 :

server start → web.xml 검사 → web.xml 실행(서블릿 URL 패턴 요청 대기) → URL 패턴 최초 요청 → 서블릿 객체 생성(컨트롤러) → 서블릿 init() 메서드 실행 → 서블릿 doGet/doPost 메서드 실행 → URL 패턴에 맞는 모델 경로 확인 → 모델(Command, service, dao) 클래스가 비즈니스 로직 처리하고 결과 값 저장 → 뷰(JSP) 경로 값 컨트롤러에 반환 → 컨트롤러가 뷰 경로 포워드 → 뷰가 저장된 결과값으로 화면 작성 및 클라이언트에 전송

 

화면 하나 만드는데 참 길다. 그래도 패턴에 익숙해지면 점차 보기가 수월해진다. 예시를 통해 서버가 시작되며 순차적으로 이루어지는 연산 과정을 하나씩 짚어보고 구조를 확인해보자.

브라우저에 이처럼 DB에 저장된 고객 리스트를 출력하는 예제다(박지성 맨유.. 대체 언제 자료여). 참고로 메이븐 프로젝트이고 DB는 오라클, 라이브러리는 JDBC, 커넥션 풀, JSTL 등을 사용했다. 

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1">
  <display-name>BookStore</display-name>
  <servlet>
      <servlet-name>dbcpinit</servlet-name>
      <servlet-class>jdbc.DBCPInit</servlet-class>
      <load-on-startup>1</load-on-startup>
  </servlet>
  <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    <welcome-file>index.htm</welcome-file>
    <welcome-file>index.jsp</welcome-file>
    <welcome-file>default.html</welcome-file>
    <welcome-file>default.htm</welcome-file>
    <welcome-file>default.jsp</welcome-file>
  </welcome-file-list>
</web-app>

web.xml은 서버가 시작되면 가장 먼저 반응한다. 즉각 dbcpinit 객체가 생성되고 @WebServlet으로 만든 urlPatterns을 확인한다. 다만 실제 요청이 들어오기 전까지는 서블릿 객체를 생성하지 않고 대기 상태로 기다린다.

index.jsp

<html>
<body>
<h2>Hello World!</h2>
<% response.sendRedirect("clist.do"); %>
</body>
</html>

index.jsp는 원하는 요청을 하기 위해 임시로 사용한다. web.xml에 welcome file로 등록돼 있기 때문에 사용 가능하다. index.jsp를 실행하면 clist.do 형식의 url을 최초로 요청하게 된다. 대기하던 컨트롤러 서블릿 객체가 생성된다.

ControllerUsingURI.java

package mvc.controller;

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import mvc.command.CommandHandler;
import mvc.command.NullHandler;

@WebServlet(
        urlPatterns = { "*.do" }, 
        initParams = { 
                @WebInitParam(name = "configFile", value = "/WEB-INF/commandHandlerURI.properties")
        })
public class ControllerUsingURI extends HttpServlet {
    private static final long serialVersionUID = 1L;

    private Map<String,Object> commandHandlerMap = new java.util.HashMap<>();

    public void init(ServletConfig config) throws ServletException {

        String configFile = config.getInitParameter("configFile"); 
        Properties prop = new Properties(); 
        FileInputStream fis = null; 
        try {
            String configFilePath = config.getServletContext().getRealPath(configFile);

            fis = new FileInputStream(configFilePath);
            prop.load(fis);
        } catch (IOException e) {
            throw new ServletException(e);
        } finally {
            if (fis != null)
                try {fis.close();} catch (IOException ex) {}
        }

        Iterator keyIter = prop.keySet().iterator();
        while (keyIter.hasNext()) {
            String command = (String) keyIter.next();
            String handlerClassName = prop.getProperty(command); 
            try { 
                Class handlerClass = Class.forName(handlerClassName); 
                Object handlerInstance = handlerClass.newInstance(); 
                commandHandlerMap.put(command, handlerInstance);
            } catch (ClassNotFoundException e) {
                throw new ServletException(e);
            } catch (InstantiationException e) {
                throw new ServletException(e);
            } catch (IllegalAccessException e) {
                throw new ServletException(e);
            }
        }
    }


    public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        process(request,response);
    }
    public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        process(request,response);
    }

    private void process(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        String command = request.getRequestURI();
        if(command.indexOf(request.getContextPath())== 0) {
            command = command.substring(request.getContextPath().length());
        }

        CommandHandler handler = (CommandHandler) commandHandlerMap.get(command);
        if (handler == null) {
            handler = new NullHandler();
        }
        String viewPage = null;
        try {
            viewPage = handler.process(request, response);
        } catch (Throwable e) {
            throw new ServletException(e);
        }

        RequestDispatcher dispatcher = request.getRequestDispatcher(viewPage);
        dispatcher.forward(request, response);
    }
}

init() 메서드를 실행하고 요청 uri와 모델 클래스 객체를 짝지어서 map 형태로 저장한다. 이후 요청 방식에 따라 doGet() 또는 doPost() 메서드를 실행한다. 만약 위 코드가 이해되지 않는다면, 그래도 일단은 괜찮다. 개발할 때 코드를 추가하는 부분은 주로 모델과 뷰 쪽이다. 컨트롤러에서는 사용자의 요청을 받고 그에 맞게 일을 시킨다는 개념을 이해하고 넘어가면 된다.

commandHandlerURI.properties

/clist.do=mvc.command.CList

uri 패턴에 맞는 모델 클래스를 찾는다.

CommandHandler.java

package mvc.command;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface CommandHandler {
    public String process(
            HttpServletRequest request, HttpServletResponse response);
}

복수로 생겨나는 모델들은 CommandHandler를 상속받아 사용한다.

CList.java

package mvc.command;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import store.dto.CustomerDto;
import store.service.CustomerService;

public class CList implements CommandHandler {

    @Override
    public String process(HttpServletRequest request, HttpServletResponse response) {

        CustomerService cs = CustomerService.getCs();
        List<CustomerDto> clist = cs.clist();

        request.setAttribute("clist", clist);

        return "/view/clist.jsp";
    }
}

CommandHandler를 상속받은 모델 클래스다. 비즈니스 로직은 service, dao로 나누어 처리한다. cs.clist();를 통해 서비스 객체의 메서드를 호출하는 게 확인된다. 

CustomerDto.java

package store.dto;

public class CustomerDto {

    private int custid;
    private String name;
    private String Address;
    private String phone;

    public int getCustid() {
        return custid;
    }
    public void setCustid(int custid) {
        this.custid = custid;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAddress() {
        return Address;
    }
    public void setAddress(String address) {
        Address = address;
    }
    public String getPhone() {
        return phone;
    }
    public void setPhone(String phone) {
        this.phone = phone;
    }
}

DB의 속성들을 getter setter로 만들어 자바에서 사용한다. 비즈니스 로직을 처리할 때 값을 dto 객체에 담아야 하기 때문에 필요하다.

CustomerService.java

package store.service;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.List;

import store.dao.StoreDao;
import store.dto.BookDto;
import store.dto.CustomerDto;

public class CustomerService {

    private CustomerService() {}
    private static CustomerService cs = new CustomerService();
    public static CustomerService getCs() {
        return cs;
    }
    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:apache:commons:dbcp:pool");
    }

    StoreDao dao = StoreDao.getInstance();

    public List<CustomerDto> clist(){
        List<CustomerDto> clist = null;
        try {
            clist =  dao.clist(getConnection());
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return clist;
    }
}

서비스 클래스에서는 커넥션 풀에서 커넥션을 꺼내오고 원하는 로직을 dao 메서드를 호출해서 수행한다.

StoreDao.java

package store.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jdbc.JdbcUtil;
import store.dto.BookDto;
import store.dto.CustomerDto;
import store.dto.SalesDto;

public class StoreDao {
    private StoreDao() {}
    private static StoreDao instance = new StoreDao();
    public static StoreDao getInstance() {
        return instance;
    }

    public List<CustomerDto> clist(Connection conn){
        List<CustomerDto> list = new ArrayList<>();
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = conn.prepareStatement("select * from customer");
            rs = pstmt.executeQuery();

            while(rs.next()) {
                CustomerDto dto = new CustomerDto();
                dto.setCustid(rs.getInt("custid"));
                dto.setName(rs.getString("name"));
                dto.setAddress(rs.getString("address"));
                dto.setPhone(rs.getString("phone"));

                list.add(dto);
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            JdbcUtil.close(rs);
            JdbcUtil.close(pstmt);            
        }
        return list;
    }

DAO 클래스는 SQL 문을 수행하고 결과값을 서비스 클래스에 반환해준다.

clist.jsp

<%@ page contentType="text/html; charset=UTF-8" %>
<%@ taglib prefix = "c" uri = "http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<title>고객리스트</title>
</head>
<body>
<h2>고객 리스트 출력</h2>
<ul>
    <c:forEach var="cust" items="${clist}">
        <li>
        ${cust.name} ${cust.address} ${cust.phone}
        </li>
    </c:forEach>
</ul>
</body>
</html>

EL로 request에 저장된 값을 표현했다.

 

정리하면

최초 요청이 들어오면 컨트롤러가 비즈니스 로직을 수행할 모델 클래스에 일을 시킨다. 이로써 CommandHandler를 상속받은 모델 클래스 → service 클래스 → dao 클래스 → service 클래스 → CommandHandler를 상속받은 모델 클래스 순으로 메서드 호출과 결과값 반환이 이어진다. 모델 클래스는 request.setAttribute()를 통해 값을 저장하고 컨트롤러에 view의 경로 값을 반환한다. 최종적으로는 화면을 구성하는 view로 도달하고 클라이언트에 DB 값을 보여줄 수 있게 된다.

MVC 패턴 구조도

 


관련 포스팅: [WEB] MVC 패턴을 쓰는 이유 | 모델1 vs 모델2

반응형

댓글

Designed by JB FACTORY