반응형

MVC(Model-View-Controller)


JSP에서 MVC pattern 이해는 필수
JSP 웹 어플리케이션은 Model-1, Model-2로 나눠짐

 

Model-1(MVC-1)
    처음 jsp를 학습할 때 사용하는 방식
    클라이언트 요청이 JSP에 바로 전달
    요청을 받은 JSP가 서비스클래스/자바빈클래스 등을 이용하여 요청을 처리
    처리한 결과를 클라이언트에 직접 응답

 

MVC(Model-View-Controller)
    JSP에서 MVC pattern 이해는 필수
    JSP 웹 어플리케이션은 Model-1, Model-2로 나눠짐

Model-2(MVC-2)
    클라이언트의 요청을 하나의 서블릿이 받음
    서블릿에서 요청을 분석하고 서비스클래스 등을 이용하여 로직을 처리
    서블릿의 처리결과를 보여줄 JSP로 포워딩
    JSP는 결과화면을 클라이언트에 응답

 

MVC는 과거 Smalltalk 언어에서 사용되던 패턴
Swing 등의 UI 컴포넌트로 활용범위 확장
JSP에서 모델2 구조를 사용하면서 개발영역에서도 많이 사용되는 패턴

 

MVC의 각 요소는 다음과 같은 역할
    Model – 비즈니스 영역의 상태 정보를 처리
    View – 사용자에게 보여질 결과 화면 처리
    Controller – 사용자의 입력 및 흐름제어

 

MVC의 핵심
    비즈니스 로직을 처리하는 모델과 결과 화면을 보여주는 뷰가 분리
    어플리케이션의 흐름제어, 사용자의 요청은 컨트롤러에 집중
    모델과 뷰는 낮은 결합도를 가짐
    유지보수 작업 – 컨트롤러만 수정
    새로운 기기에 서비스하는 경우 – 뷰 추가/수정

 

 

모델2 구조에서 서블릿은 MVC패턴의 컨트롤러와 역할이 같다.
웹 브라우저의 요청과 웹 어플리케이션의 흐름제어를 담당

 

JSP의 모델2 구조에서 JSP는 뷰의 역할
    사용자의 요청을 받을 수 있는 form을 제공(HTML과 함께 사용)
    제공된 form을 이용하여 클라이언트의 요청을 서버로 전달
    서버에서 처리된 결과를 클라이언트의 브라우저로 응답
    로직/컨트롤러에서 어떤 처리가 이루어지는지 관려하지 않음(낮은 결합도)

    모델을 구현하는 명확한 규칙은 없음
    컨트롤러는 요청을 받아 요청을 비즈니스 로직으로 처리하여 알맞은 뷰에 전달
    요청에 대한 비즈니스 로직을 처리하는 부분이 모델

 

다음의 실습을 진행하여 MVC구현 방법을 이해하도록 한다.

 

기본 MVC패턴 구현 기법

1. 컨트롤러-모델-뷰 구현 및 동작과정 이해

2. Command pattern이해 및 적용 방법

3. 요청 URL 매핑 과정 이해

4. Model1 – Model2 구조의 비교

 

실습 시작

 

1. MVC 패턴 구현 기법
    MVC패턴을 구현하기 위해 JSP의 Model2 구조를 구현하는 방법을 이해해야 함.

실습에서 구현할 모델2 구조의 예는 다음과 같은 기능을 가짐.
    a. 서블릿은 화면에 출력할 메시지를 생성하여 JSP에 전달한다.
    b. JSP는 서블릿으로부터 전달받은 메시지를 화면에 출력한다.

 

실습 1.
컨트롤러부터 작성할 것인데 컨트롤러는 보통 다음과 같은 형태로 만들어진다.(흐름만 보기)

package controller;
 
import java.io.IOException;
 
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
public class ControllerServlet extends HttpServlet{
    //1단계 - HTTP 요청을 받음(GET/POST)
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException{
        processRequest(request, response);
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException{
        processRequest(request, response);            
    }
    private void processRequest(HttpServletRequest request, 
            HttpServletResponse response)
        throws ServletException, IOException{
        //2단계 요청 분석
        //request 객체로부터 사용자의 요청을 분석하는 코드 작성
        ...
        //3단계 모델을 사용하여 분석된 요청을 처리
        ...
        //4단계 request나 session에 처리된 결과를 저장
        /*
         * request.setAttribute("ret", retObject);
         * session.setAttribute("ret", retObject);
         * 위와 같은 형태의 코드이다.
         * ...
         */
        //5단계 RequestDispatcher를 사용하여 알맞은 뷰로 포워딩 | 리다이렉트
        /*
         * RequestDispatcher dispatcher = 
         *         request.getRequestDispatcher("/view.jsp");
         * dispatcher.forward(request, response);
         * 위와 같은 형태의 코드
         */
    }
}
 
cs

실제로 예제 테스트를 위한 컨트롤러(서블릿) 구현
이 서블릿은 웹 브라우저가 전송한 type파라미터의 값에 따라 다른 결과를 생성하여 뷰를 통해 클라이언트에 응답을 할 수 있도록 하는 기능의 서블릿이다.

 

src/controller/SimpleController.java
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
package controller;
 
import java.io.IOException;
 
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
public class SimpleController extends HttpServlet{
    //1단계 - HTTP 요청을 받음(GET/POST)
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException{
        processRequest(request, response);
    }
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException{
        processRequest(request, response);            
    }
    private void processRequest(HttpServletRequest request, 
            HttpServletResponse response)
        throws ServletException, IOException{
 
        //2단계 요청 분석
        String type = request.getParameter("type");
    
        //3단계 모델을 사용하여 분석된 요청을 처리
        Object resultObject = null;
        if( type == null || type.equals("greeting")){
            resultObject = "안녕하세요.";
        } else if(type.equals("date")){
            resultObject = new java.util.Date();
        } else {
            resultObject = "Invalid Type!";
        }
        
        //4단계 request나 session에 처리된 결과를 저장
        request.setAttribute("result", resultObject);
        
        //5단계 RequestDispatcher를 사용하여 알맞은 뷰로 포워딩 또는
                                    리다이렉트
        RequestDispatcher dispatcher = 
                 request.getRequestDispatcher("/simpleView.jsp");
        dispatcher.forward(request, response);
    }
}
 
cs

 

요청을 처리하고 결과를 보여주는 뷰 페이지 작성
WebContent/simpleView.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title></title>
</head>
<body>
결과 : <%= request.getAttribute("result"%>
</body>
</html>
 
cs

 

요청 URL과 위에서 만든 서블릿을 매핑 할 수 있도록 web.xml에 설정 추가
WebContent/WEB-INF/web.xml 파일에 서블릿 설정 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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>mvc2test</display-name>
  <!-- 웹 컨테이너가 이해하도록 서블릿 정보 등록 -->
  <servlet>
      <servlet-name>SimpleController</servlet-name>
      <servlet-class>controller.SimpleController</servlet-class>
  </servlet>
  
  <!-- 클라이언트의 URL요청에 따라 서블릿이 사용되도록 매핑 -->
  <servlet-mapping>
      <servlet-name>SimpleController</servlet-name>
      <url-pattern>/simple</url-pattern>
  </servlet-mapping>
</web-app>
 
cs

 

Tomcat 재시작 후 브라우저를 이용하여 요청

 
type값을 전달하도록 get방식으로 요청
- ?type=date 요청


- ?type=아무문자 요청

 

실습 2.
    커맨드 패턴 기반의 MVC2 실습 
 

다음 실습을 통해 사용자의 요청을 명령어로 취급하도록 하고 서블릿에서 요청 명령을 구분하여 로직을 처리하는 방식을 이해한다.
 

웹 브라우저를 통해 명령어를 전달하는 방법 두 가지
    1. 특정 이름의 파라미터에 명령어 정보를 전달
    2. 요청 URL자체를 명령어로 사용

 

특정 이름의 파라미터에 명령어를 전달하는 방식 테스트

 

명령어에 해당하는 코드를 별도의 클래스(인터페이스)로 작성
src/cmd/CommandHandler.java 인터페이스 작성

1
2
3
4
5
6
7
8
9
10
package cmd;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
public interface CommandHandler {
    public String process(HttpServletRequest request, 
            HttpServletResponse response) throws Throwable;
}
 
cs

 

클라이언트의 요청 명령을 처리할 핸들러 클래스는 모두 위의 인터페이스를 구현하도록 한다.
핸들러 클래스 작성 시 다음 작업을 처리하도록 한다.
1. 명령어와 관련된 비즈니스 로직 처리
2. 뷰 페이지에서 사용할 정보 저장
3. 뷰 페이지의 URI 반환

클라이언트 요청 명령의 구분을 위해 서블릿에서 if~else구문으로 명령어를 구분하는 것은 효율적이지 않으므로 설정파일에 별도의 명령어 정보를 저장하고 서블릿 초기화 시 init()메서드에서 명령어 정보를 초기화 하도록 구현한다.
WebContent/WEB-INF/commandHandler.properties

1
2
hello=cmd.HelloHandler
#command=command mapping handler class name
cs

 

위와 같이 명령어와 핸들러클래스를 매핑한 정보를 properties파일로 작성해 둔다.
Hello 요청을 처리할 HelloHandler클래스를 작성한다.
src/cmd/HelloHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package cmd;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
public class HelloHandler implements CommandHandler{
 
    @Override
    public String process(HttpServletRequest request, 
            HttpServletResponse response) throws Throwable {
        request.setAttribute("hello""안녕하세요~");
        return "/view/helloCommand.jsp";
    }
 
}
 
cs

 

설정파일에서 매핑정보를 읽어와서 핸들러 인스턴스를 생성한 후 process()메서드에서 사용하도록 하는 서블릿을 작성해본다.
src/controller/ControllerUsingFile.java

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
package controller;
 
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Properties;
 
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
import cmd.CommandHandler;
 
public class ControllerUsingFile extends HttpServlet{
    private Map<String, CommandHandler> commandHandlerMap = 
            new HashMap<String, CommandHandler>();
    
    public void init() throws ServletException{
        String configFile = getInitParameter("configFile");
        Properties property = new Properties();
        FileInputStream fis = null;
        try{
            String configFilePath = 
                getServletContext().getRealPath(configFile);
            fis = new FileInputStream(configFilePath);
            property.load(fis);
        }catch(IOException e){
            throw new ServletException(e);
        } finally{
            if(fis != null){
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        Iterator keyIt = property.keySet().iterator();
        while(keyIt.hasNext()){
            String command = (String)keyIt.next();
            String handlerClassName = 
                property.getProperty(command);
            try{
                Class<?> handlerClass =  
                Class.forName(handlerClassName);
    
                CommandHandler handlerInstance = 
                (CommandHandler) 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);
            }
        }
    }
    protected void doGet(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException{
        processRequest(request, response);
    }
 
    protected void doPost(HttpServletRequest request,
                         HttpServletResponse response)
            throws ServletException, IOException{
        processRequest(request, response);            
    }
    private void processRequest(HttpServletRequest request, 
                        HttpServletResponse response)
            throws ServletException, IOException{
        String command = request.getParameter("cmd");
        CommandHandler handler = 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);
    }
}
 
cs

간단한 소스설명

- configFile 초기화 파라미터로부터 설정파일의 경로를 구한다.
- 설정파일로부터 매핑경로를 읽어 Properties객체에 저장
- 저장된 매핑정보를 이용하여 <명령어, 핸들러객체>정보를 commandHandlerMap에 저장
- 클라이언트의 요청에서 cmd파라미터로 전달받은 명령어를 구한다.
- 요청을 처리할 명령어 핸들러 객체를 commandHandlerMap으로부터 구한다.
- 명령어에 해당하는 핸들러가 없으면 NullHandler를 사용
- 핸들러의 process()메서드를 호출하여 요청을 처리하고 뷰에 해당하는 URI정보를 반환받음
- 핸들러의 process()메서드는 처리 결과를 request 또는 session에 저장해 두어야 함
- 핸들러가 반환한 URI를 이용하여 뷰 페이지로 이동시킴

 

서블릿 작성 후 web.xml에 설정파일과 서블릿 등록 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  <servlet>
      <servlet-name>ControllerUsingFile</servlet-name>
      <servlet-class>controller.ControllerUsingFile</servlet-class>
      <init-param>
          <param-name>configFile</param-name>
          <param-value>/WEB-INF/commandHandler.properties
        </param-value>
      </init-param>
  </servlet>
  
  <servlet-mapping>
      <servlet-name>ControllerUsingFile</servlet-name>
      <url-pattern>/controllerUsingFile</url-pattern>
  </servlet-mapping>
 
cs

 

응답을 보여줄 view페이지 작성
WebContent/view/helloCommand.jsp

 

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Hello Command 처리</title>
</head>
<body>
<%= request.getAttribute("hello"%> 응답받음
</body>
</html>
 
cs

 

NullHandler 추가하고 설정파일에 null일 때 사용할 핸들러로 지정
src/cmd/NullHandler.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package cmd;
 
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
 
public class NullHandler implements CommandHandler{
 
    @Override
    public String process(HttpServletRequest request, 
            HttpServletResponse response) throws Throwable {
        return "/view/nullCommand.jsp";
    }
}
cs

 

설정파일에 =cmd.NullHandler 지정

 

cmd가 null일 때 응답 페이지 작성
WebContent/view/nullCommand.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Null Command</title>
</head>
<body>
요청에 명령이 없습니다.
</body>
</html>
 
cs

 

Tomcat 재 실행 후 브라우저로 cmd로 명령어 요청 ( ?cmd=hello )


cmd에 명령어가 없는 경우 nullCommand.jsp응답 확인 ( ?cmd= )


위 작업을 다른 방법으로 적용해 본다.

요청 URI를 명령어로 사용하는 방법

 앞의 예제는 cmd파라미터를 브라우저에서 전달받아 명령어로 사용하였다.
이 방식은 컨트롤러에서 어떤 명령을 받아 동작하는지 URL을 통해 노출되는 것이 단점.
즉, 크래커들이 악의적인 목적으로 아무런 명령어나 전달해보면서 서버에 부하를 주고 오작동을 만들어 낼 여지가 있다는 것이다.
 그래서 URL에서 URI부분을 이용하여 명령어로 활용하는 것이 권장된다.

 

 URI을 명령어로 사용하는 방법은 앞서 작성한 process()메서드에서 요청에서 cmd의 파라미터를 받아왔었는데 이것을 URI을 받아오도록 변경하면 된다.
먼저 앞에서 작성한 ControllerUsingFile코드를 그대로 복사해서 ControllerUsingURI를 만든다.

그리고 명령어 추출 부분만 변경한다.


src/controller/ControllerUsingURI.java의 processRequest()메서드에서 명령어 추출하는 부분 수정

 
79
 
 
79
80
81
82
83
84
//요청 파라미터에서 명령어 추출
//String command = request.getParameter("cmd");
 
//URL을 얻어와 명령어로 활용
String command = request.getRequestURI();    //요청 URI정보
if(command.indexOf(request.getContextPath()) == 0){
    //ContextPath와 같은 요청이라면
    command = command.substring(request.getContextPath().length());
    //ContextPath를 제거하고 URI정보만 명령어(command)로 활용
}
cs

 

이어서 web.xml에 특정 확장자에 대한 요청을 처리할 수 있도록 <servlet-mapping>을 추가
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  <servlet>
    <servlet-name>ControllerUsingURI</servlet-name>
    <servlet-class>controller.ControllerUsingURI</servlet-class>
    <init-param>
      <param-name>configFile</param-name>
      <param-value>/WEB-INF/commandHandler.properties</param-value>
      </init-param>
  </servlet>
  
  <servlet-mapping>
     <servlet-name>ControllerUsingURI</servlet-name>
     <url-pattern>*.do</url-pattern>
  </servlet-mapping>
 
cs

 

명령어 정보가 저장된 properties파일에 해당 명령어(/hello.do)의 핸들러를 설정한다

1
2
3
4
hello=cmd.HelloHandler
=cmd.NullHandler
/hello.do=cmd.HelloHandler
#command=command mapping handler class name
cs

 

hello.do 요청

 

test.do 요청

 

모델1과 모델2의 장단점 비교
대형 프로젝트는 모델2 적용이 유리함
변경될 일이 적고 로직이 복잡하지 않은 소형 사이트는 모델1 적용이 유리

 

 

 

 

반응형

+ Recent posts