[Servlet] Jave EE 패턴

About java EE pattern

Posted by JongMin-Lee on June 27, 2019

Java EE 패턴

Java EE 패턴은 자바 기반의 엔터프라이즈 웹 애플리케이션 개발을 위한 패턴입니다. 어디선가 들어본 ValueObject, DataAcessObject 등은 모두 Java EE패턴에서 나온 용어들 입니다. Java EE패턴에는 여러가지 패턴이 있는데 어떤 패턴이 있는지 먼저 알아봅시다!


1. Java EE 패턴 목록

1) Intercepting Filter

요청에 대한 전처리 및 후처리
요구사항에 대해서 전처리와 후처리에 대한 솔루션을 제공하고 이를 통해 유동적인 아키텍처를 가능하게 합니다.

2) Front Controller

요청에 대한 처리를 관리하는 중앙 컨트롤러
프레젠테이션 레이어에 일어나는 일들의 창구로 facade 패턴의 역할과 MVC패턴에서 controller의 역할을 함으로써 보안, 뷰 관리, 탐색들을 관리합니다.

3) View Helper

뷰의 표현을 위해 비즈니스 로직을 가지고 있는 개념상의 Helper
비즈니스 로직과 프레젠테이션 로직의 결합도를 낮추기 위해 사용합니다.

4) Composite View

레고 블럭 같은 작은 뷰들을 조합해서 만드는 전체의 뷰
복잡한 뷰를 만들기 위해서 기본적인 뷰 레이어를 융통성 있게 하고, 개인화 영역과 커스터마이징을 보다 수월하게 합니다.

5) Service to worker

Front Controller와 View Helper Pattern을 이용해 dispatcher 컴포넌트를 구성
대규모 애플리케이션에서 이용되는 기법으로 뷰에 대한 처리 이전에 동작합니다.

6) Dispatcher View

Service to Worker와 동일하며 차이점은 뷰에 대한 처리 중에 수행되어야 하고, 작은 시스템에서 더 안정적입니다.

Command 패턴에 앞서 대부분의 웹 프레임워크에서 차용하고 있는 Front Controller 패턴에 대해서 알아보자


2. Front Controller

Front Controller 패턴은 컨트롤러가 공통 요청을 먼저 수행하고 뷰를 호출하는 패턴입니다.

요청에 대해서 컨트롤러가 응답하고, 결과에 따라 서블릿이나 JSP로 만든 뷰를 보여주게 됩니다. 서버 측에서 메서드를 사용하여 화면을 전환하는 방법에는 두 가지가 있습니다.

  • Response 객체의 sendRedirect 메서드
  • RequestDispatcher 객체의 forward 메서드

sendRedirect는 속성을 저장할 수 없고 다른 로직을 추가할 수 없습니다. 반면에 forward 메서드는 서버 내부에서만 흐름이 이동하므로 속성을 저장할 수 있고, 브라우저(클라이언트)에게 바로 전달하지 않고 원하는 작업을 처리한 후에 응답을 전환할 수 있습니다.

1) sendRedirect

sendRedirect 메서드는 HttpServletResponse에 속한 메서드인데 다음과 같이 사용할 수 있습니다.

1
response.sendRedirect("경로");


2) forward

forward 메서드를 사용하기 위해서는 requestDispatcher 객체를 생성해야 합니다.

1
2
RequestDispatcher rd = request.getRequestDispatcher("경로");
rd.forward(ServletRequest request, ServletResponse response);

여기서 requestDispatcher 객체의 경로는 절대경로로 지정해야 됩니다. 다음은 Front Controller를 이용한 분기처리의 모습입니다.

1
2
3
4
5
6
7
if(url == "list"){
    RequestDispatcher rd = req.getRequestDispatcher(url);
    rd.forward(request, response);
}else if(url == "write"){
    RequestDispatcher rd = req.getRequestDispatcher(url);
    rd.forward(request, response);
}

컨트롤러에서 화면에 보여 주는 구문은 위와 같이 if문으로 분기처리하게 되는데, 이렇게 직접적으로 forward메서드를 사용하게 될 경우 URL이 변경되거나 뷰가 변경될 때마다 컨트롤러를 변경하게 되어서 추후에 유지보수가 어려워집니다. 이럴 때는 Command pattern을 이용해서 복잡도를 낮출 수 있습니다. 그러면 이제 Command pattern에 대해서 알아보자!


3. Command Pattern

커맨드 패턴은 명령(로직)을 객체 안에 캡슐화해서 저장함으로써 컨트롤러와 같은 클래스를 수정하지 않고 재사용할 수 있게 하는 패턴입니다.

Invoker역할은 컨트롤러가 담당합니다. 아래의 Command pattern의 예시를 보자

Command.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
package command.pattern;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public abstract class Command{
    private HttpServletRequest req;
    private HttpServletResponse res;
    private ServletContext servletContext;

    abstract public void execute();

    public void forward(String url){
        try{
            RequestDispatcher rd = req.RequestDispatcher(url);
            rd.forward(getReq(), getRes());
        }catch (IOException ioe){
            servletContext.log("forward Error", ioe);
        }catch (ServletException servletEx){
            servletContext.log("servlet Error", servletEx);
        }
    }

    public HttpServletRequest getReq(){
        return req;
    }
    public void setReq(HttpServletRequest req){
        this.req = req;
    }
    public HttpServletResponse getRes(){
        return res;
    }
    public void setRes(HttpServletResponse res){
        this.res = res;
    }
    public ServletContext getServletContext(){
        return servletContext;
    }
    public void setServletContext(ServletContext servletContext){
        this.servletContext = servletContext;
    }
}

커맨드 클래스는 abstract로 만들었습니다. 서블릿 클래스가 아니므로 HttpServletRequset와 httpServletResponse를 변수로 선언하여 getter와 setter메소드를 만들었습니다.

FrontController.java (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
50
51
52
53
54
package command.pattern;

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 java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

@WebServlet(urlpatterns = "/controller", initParams= {@WebInitParam(name="mapping", value="/WEB-INF/command.properties")})
public class FrontController extends HttpServlet{
    private Properties cmdMapping;

    @Override
    public void init(ServletConfig config) throws ServletException{
        super.init(config);
        InputStream is = null;
        try{
            String location = config.getInitParameter("mapping");
            is = getServletContext().getResourceAsStream(location);
            cmdMapping = new Properties();
            cmdMapping.load(is);
        }catch(IOException e){
            getServletContext().log("I/O Error", e);
        }finally{
            try{
                is.close();
            }catch(IOException iog){

            }
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException{
        String cmd = req.getParameter("cmd");
        String cmdClass = (String)cmdMapping.get(cmd);
        Command command = null;

        try{
            command = (Command)Class.forName(cmdClass).newInstance();
        }catch(ClassNotFoundException | IllegalAccessException | InstantiationException ex){
            getServletContext().log("Class not found", ex);
        }
        command.setReq(req);
        command.setRes(resp);
        command.setServletContext(getServletContext());
        command.execute();
    }
}

doGet 메서드는 command객체로 사용될 클래스의 이름을 입력 받은 후 리플랙션을 이용해서 인스턴스를 생성하고 execute 메서드를 호출합니다. execute메서드는 입력받은 파라미터와 뷰 클래스들을 매칭해서 응답을 전달하는 역할을 합니다. 뷰 클래스들의 패키지 위치 정보는 command.properties 파일로 관리합니다.

  • ServletConfig : 하나의 Servlet에서 사용합니다.(config 객체라고도 하면 Servlet당 하나 존재합니다.) get메소드만 지원하여 Read-only입니다. init()메서드를 호출하여 초기화된 parameter를 ‘name / value’형식으로 읽습니다.
  • ServletContext : servlet과 jsp에서 모두 접근가능하며 전체(JVM)에서 한 개만 존재합니다.
    위의 둘다 Servlet에 상속되어 있어 다른 선언없이 바로 get() 이 가능합니다.

properties파일은 다음과 같습니다.

command.properties

1
2
home=command.pattern.HomeView
list=command.pattern.listView

command.properties 파일은 입력된 파라미터가 view 클래스들과 연결되도록 하는 역할을 합니다.

properties 파일은 key / value 형태로 데이터를 저장하는 설정 파일 입니다. 위의 코드중 cmdMapping.load(is)를 보면 properties파일에서 key / value를 읽어들여 key / value형태로 저장하는 것을 볼 수 있습니다.

HomeView.java

1
2
3
4
5
6
public class HomeView extends Command{
    @Override
    public void execute(){
        forward("/home.jsp");
    }
}

HomeView 클래스는 Command를 상속받아서 home.jsp로 리퀘스트를 forwarding합니다. 브라우저에서 localhost:8080/controller?cmd=home을 입력하고 결과를 확인해 보면 home.jsp에 작성한 내용이 출력됩니다.

페이지 경로를 모르더라고 cmd 파라미터에 따라서 페이지로 이동됩니다. 지금까지 클래스들의 흐름을 정리해보면 다음과 같습니다.

모든 요청은 FrontController 클래스가 받습니다. HomeView는 Command 클래스를 상속받았고 요청 파라미터가 Home인 경우 homew.jsp를 호출합니다. 이와 같이 프론트 컨트롤러 패턴은 뷰 페이지 요청을 한 곳에서 관리할 수 있게 해줍니다.


4. 글을 마치며

기존에 미니 프로젝트를 진행하면서 Front Controller를 if문을 이용한 분기처리로 작성하였습니다. 코드작성도 간결하고 코드의 흐름이 간단해 보였기 때문에 괜찮은 방식이라 생각했습니다. 그러나 Command pattern에 대해 공부하며 기존에 작성하던 방식의 단점을 보았습니다. 또한 Command pattern을 사용함으로써 오는 장점이 더욱 크다는 것을 확인하였습니다. 짧게 작성하는 코드가 나중에 더욱 큰 프로젝트가 되었을때 좋지만은 않다는 것을 느꼈습니다. 각각의 프로젝트에 알맞는 방식으로 작성하는 것이 가장 좋은 방법이라고 생각합니다. 프로젝트를 진행하기에 앞서 어떠한 패턴으로 진행할지 먼저 결정한 후 진행하는 것이 중요하다고 생각합니다. 부족하지만 긴 글을 읽어주셔서 감사합니다!!


5. Reference

  • 스프링 부트로 배우는 자바 웹개발 / 윤석진