주요글: 도커 시작하기
반응형
모델 2 구조를 객체 지향적으로 구현하는 방법에 대해서 살펴본다.

모델 2 구조의 올바른 구현 방법

1부에서 살펴보았듯이 모델 2 구조에서 모든 클라이언트의 요청은 서블릿으로 전달되며, 서블릿은 그 요청을 알맞게 처리해야 할 책임이 따른다. 여기서 모든 클라이언트의 요청이라는 것은 연관성이 있는 기능등을 함께 묶어 놓은 집합을 의미한다. 예를 들어 게시판을 생각해보자. 게시판은 읽기/쓰기/삭제/목록보기 등의 기능을 필요로 하며 이러한 각 기능을 하나의 서블릿에서 구현하는 것이 바로 모델 2 구조의 알맞은 구현 방법이라 할 수 있다.

언뜻 생각해보면 모든 기능을 하나의 서블릿에서 처리한다는 것이 매우 이상하게 느껴질 것이다. 아마 여러분은 게시판을 서블릿으로 구현하고자 할 때 글 목록을 보여주는 ListServlet, 글을 작성하는 WriteServlet 그리고 글을 삭제하는 DeleteServlet 처럼 기능등을 각각 하나의 서블릿을 통해서 구현하고자 할 것이다. 이는 여러분들이 지금까지 살펴본 대부분의 서적에서는(또한 필자와 이동훈씨가 함쎄 지은 JSP Professional에서 조차도) 게시판의 각 기능을 별도의 서블릿 또는 JSP 페이지에서 처리하고 있기 때문이며, 또한 여러분이 이러한 사고 방식에 익숙해져 있기 때문이다.

이 시점에서 그러면 다음과 같은 질문이 떠오를 것이다.

  그렇다면 모든 일련의 기능을 하나의 서블릿에서 구현하는 것이 정말로 좋은가?

이 질문에 대한 대답은 바로 "그렇다" 이다. 조금은 못미덥겠지만 이 글을 끝까지 읽어나가면 여러 서블릿에 분산시키는 것 보다 하나의 서블릿에서 관련된 일련의 기능을 구현하는 것이(좀더 정확하게 표현하면 관련된 일련의 기능을 제어하는 것이) 효과적이며 또한 확장성도 뛰어나다는 것을 알 수 있을 것이다.

하나의 서블릿에 모든 관련된 기능을 집중시키자!

이제부터 서블릿에서 관련된 모든 기능을 구현하는 것에 대해서 살펴보자. 일단 모든 관련된 기능을 하나의 서블릿에서 구현하기 위해서는 서블릿이 각각의 요청이 어떤 기능을 요구하는 것인지 구분할 수 있어야 한다. 어떤 클라이언트는 게시판 목록을 보길 원할 것이고, 어떤 클라이언트는 글쓰기를 원할 것이고 그리고 어떤 클라이언트는 글에 대한 답변을 작성하기를 원할 것이다. 이처럼 각각의 요청은 서블릿으로부터 서로 다른 서비스를 받길 원하며 서블릿은 이를 구분하여 각각의 요청에 대해 알맞은 응답을 해 주어야 한다.

서블릿이 각 기능을 구분할 수 있는 한 가지 방법은 각 기능마다 고유의 명령어를 부여하는 것이다. 예를 들어, 게시판 목록 보기는 "List" 명령어를, 글 쓰기 작성 폼은 "WriteForm" 명령어를, 그리고 작성한 글을 저장하는 것은 "Write" 명령어를 부여하는 것이다. 이제 클라이언트는 이렇게 기능별로 부여한 명령어를 서블릿에 전달하기만 하면 된다. 몇몇 독자는 짐작했겠지만 서블릿에 명령어를 전달하는 방법은 파라미터를 통해서 이루어진다. 예를 들어, command 라는 파라미터를 통해서 명령어를 전달한다고 할 경우 게시판 목록 보기 요청은 /servlet/BoardServlet?command=List 와 같은 URL을 통해서 표현될 것이다.

각 기능별로 고유의 명령어를 부여했기 때문에 서블릿은 클라이언트가 보내온 명령어에 따라 알맞은 응답을 해 주기만 하면 된다. 간단하게 전체적인 코드의 형태를 보여준다면 다음과 같다.

   public void doGet(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
   
      String command = request.getParameter("command");
      String nextPage = "";
      
      if (command.compareTo("List") == 0) {
         // 목록 보여주기 위한 처리를 한다.
         BoardManager bMgr = BoardManager.getInstance();
         BoardList[] bList = bMgr.getList(request.getParameter("pageno");
         ...
         nextPage = "/board/list.jsp";
      } else if (command.compareTo("WriteForm") == 0) {
         // 글을 작성할 수 있는 폼을 보여준다.
         ...
         nextPage = "/board/writeform.jsp";
      } else if (command.compareTo("Write") == 0) {
         // 사용자가 입력한 데이터를 알맞게 저장한다.
         ...
         nextPage = "/board/write.jsp";
      }
      
      RequestDispatcher rd = request.getRequestDispatcher(nextPage);
      rs.forward(request, response);
   }

위 코드를 보면 command 파라미터의 값에 따라 알맞은 처리를 한 후 결과를 보여줄 JSP 페이지를 nextPage에 저장한느 것을 알 수 있다. 모든 처리가 끝나면 서블릿은 RequestDispatcher를 사용하여 nextPage에서 지정한 JSP 페이지를 보여준다. 위 코드의 경우 간단하게 "..."을 삽입하긴 했지만 실제로 "..."이 대신 완전한 코드를 넣었다면 위 코드는 아마 매우 길어졌을 것이다. 또한 위와 같이 하나의 메소드에 모든 구현을 다 넣는 것이 이상하게 느껴질 것이다.

물론, 위와 같이 하나의 메소드에 모든 기능을 다 넣는 것은 좋지 않은 방법이며 요구하는 기능이 많아질 경우 소스 코드가 복잡해지는 단점이 있다. 일단 덜 복잡한 코드를 작성하기 위해서는 기능들을 각각 별도의 메소드에서 구현해야 한다. 다음은 각각의 명령어를 별도의 메소드를 통해서 처리하도록 수정한 서블릿의 형태이다.

   public void doGet(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
   
      String command = request.getParameter("command");
      String nextPage = "";
      
      if (command.compareTo("List") == 0) {
         nextPage = processListCommand(request, response);
      } else if (command.compareTo("WriteForm") == 0) {
         nextPage = processWriteFormCommand(request, response);
      } else if (command.compareTo("Write") == 0) {
         nextPage = processWriteFormCommand(request, response);
      }
      
      RequestDispatcher rd = request.getRequestDispatcher(nextPage);
      rs.forward(request, response);
   }
   
   private String processListCommand(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
      // 목록 보여주기 위한 처리를 한다.
      BoardManager bMgr = BoardManager.getInstance();
      BoardList[] bList = bMgr.getList(request.getParameter("pageno");
      ...
      return "/board/list.jsp";
   }

   private String processWriteFormCommand(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
      // 글을 작성할 수 있는 폼을 보여준다.
      ...
      return "/board/writeform.jsp";
   }

   private String processWriteFormCommand(HttpServletRequest request,
                     HttpServletResponse response)
                     throws IOException, ServletException {
      // 사용자가 입력한 데이터를 알맞게 저장한다.
      ...
      return "/board/write.jsp";
   }

위 코드를 보면 하나의 메소드에서 하나의 명령어를 처리하는 것을 알 수 있다. 각각의 메소드는 자신의 처리해야 할 명령어에 대한 알맞은 처리를 한 후 request나 session의 setAttribute() 메소드를 사용하여 그 결과값을 저장해서 결과 화면을 보여주는 JSP 페이지에서 사용할 수 있도록 하고, 또한 결과를 보여줄 JSP 페이지를 리턴값으로 돌려준다. 그려면 서블릿의 doGet()이나 doPost() 메소드에서는 메소드가 리턴한 URI(즉, 결과를 보여줄 JSP 페이지)로 포워딩시키면 된다. 이것이 모델 2 구조의 가장 기본적인 구조라 할 수 있다.

명령어 기반 모델 2 구조의 가장 기본적인 구현 형태를 정리하면 다음과 같다.

  public class CommandBaseServlet extends HttpServlet {
     
     public static final String DEFAULT_COMMAND = "...";
     
     public void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        processCommand(request, response);
     }
  
     public void doProcess(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        process(request, response);
     }
     
     private void process(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        String command = request.getParameter("command");
        if (command == null) command = DEFAULT_COMMAND;
        
        String nextPage = null;
        
        if (command.compareTo("Command1") == 0) {
           nextPage = processCommand1(request, response);
        } else if (command.compareTo("Command2") == 0) {
           nextPage = processCommand2(request, response);
        } else ...
           ...
        }
        RequestDispatcher rd = request.getRequestDispatcher(nextPage);
        rs.forward(request, response);
     }
     
     private String processCommand1(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        // 어떤 처리를 한다.
        ...
        return "/process/command1result.jsp";
     }
     
     ...
     
     private String processCommandN(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        // 어떤 처리를 한다.
        ...
        return "/process/commandNresult.jsp";
     }
  }

위 코드에서 doGet() 메소드와 doPost() 메소드에서 모두 process() 메소드를 호출하는 것을 알 수 있다. 이렇게 함으로써 doGet()과 doPost()에서 중복되는 코드를 없앨 수 있다. 단 하나 주의할 점이 있다면 인코딩 타입이 multipart/form-data로 전송될 경우 추가적으로 이에 알맞은 처리를 해 주어야 한다.

모델 2 구조와 커맨드 패턴

명령어 기반의 모델 2 구조의 구현을 잘 살펴보면 하나의 명령어는 하나의 작업(또는 역할)과 관련된 것을 알 수 있다. 예를 들어, 게시판과 관련된 명령어가 "List", "Write", "Edit"라고 할 경우 이 명령어들 각각은 "목록보기", "글쓰기", "글 수정하기"라는 역할을 나타내고 있는 것이다.

여기서부터 우리는 좀더 객체 지향적으로 나아가 보자! 하나의 명령어가 하나의 작업을 처리한다는 것은 또는 하나의 역할을 의미한다는 것은 각각의 명령어들을 하나의 클래스로 표현할 수 있다는 것을 의미한다. (객체 지향에서 대부분의 클래스는 그 클래스만의 역할을 갖고 있다.) 즉, 커맨드 패턴을 적용할 수 있는 것이다. 커맨드 패턴은 하나의 명령어에 대하여 하나의 클래스를 대응시키는 것으로서 이에 대한 자세한 내용은 필자가 자바캔에 기고했던 글인 '커맨드(Command) 패턴과 그 구현'을 참고하기 바란다. (이 부분을 읽기 전에 먼저 커맨드 패턴에 대한 것부터 반드시 숙지하기 바란다!)

'커맨드 패턴과 그 구현'을 읽었다면 (또는 커맨드 패턴에 대해서 이해하고 있다면) 이제부터 커맨드 패턴을 모델 2 구조에 적용하는 것에 대해서 살펴보자. (이 글에서는 각각의 커맨드의 기능을 실제로 구현한 클래스를 커맨드 클래스라고 표현하고 모든 커맨드 클래스가 공통으로 구현해야 하는 인터페이스를 커맨드 인터페이스라고 표현할 것이다.)

먼저 커맨드 인터페이스에 대해서 살펴보자. 모델 2 구조에서 사용될 커맨드 인터페이스는 HTTP 요청으로부터 정보를 추출할 수 있어야 하고 또한 세션, 요청 객체(HttpServletRequest 클래스의 객체) 등에 접근할 수 있어야 한다. 뿐만 아니라 HTTP 응답 객체(HttpServletResponse 클래스의 객체)를 사용할 수 있어야 한다. 이러한 것을 충족시키기 위해서는 다음과 같이 커맨드 인터페이스를 정의해주어야 한다.

  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  import com.board.command.ProcessingException;
  
  public interface CommandIF {
     public String processCommand(HttpServletRequest request,
                                  HttpServletResponse response)
                                  throws ProcessingException;
  }

여기서 processCommand() 메소드의 리턴 타입이 String인 것을 알 수 있는데, 여기서 String은 CommandIF를 통해서 커맨드를 처리한 후 서블릿이 포워딩할 페이지의 URI를 나타낸다. 이에 대한 것은 뒤에서 좀더 구체적으로 설명하도록 하자.

명령어를 처리해주는 모든 커맨드 클래스가 구현해야할 인터페이스를 정의하였으므로, 그 다음으로 해야 할 것은 각각의 명령어에 알맞게 커맨드 클래스를 처리해주는 것이다. 예를 들어, 앞에서 예로 들었던 게시판 관련 명령어 중에서 "List"를 처리해주는 커맨드 클래스를 com.board.command.ListCommand 라고 해 보자. 이 경우 ListCommand 클래스는 다음과 비슷할 것이다.

  package com.board.command;
  
  import com.board.BoardMgr;
  import com.board.BoardData;
  import com.board.command.ProcessingException;
  
  import javax.servlet.http.HttpServletRequest;
  import javax.servlet.http.HttpServletResponse;
  
  public class ListCommand implements CommandIF {
     
     public String processCommand(HttpServletRequest request,
                                  HttpServletResponse response)
                                  throws ProcessingException {
        try {
           String pageNo = request.getParameter("page");
           String boardCode = request.getParameter("board");
           
           int page = Integer.parseInt(pageNo);
           
           BoardMgr boardMgr = BoardMgr.getInstance();
           BoardData[] list = boardMgr.getBoardDataAtPage(boardCode, page);
           
           request.setAttribute("boardMgr.boardList", list);           
           return "/board/list.jsp";
        } catch(Exception ex) {
           throw new ProcessingException(ex);
        }
     }
  }

위 코드에서 ListCommand 클래스의 processCommand() 메소드는 파라미터로 전달받은 request 객체를 통해서 서블릿에서 해야 할 모든 작업을 할 수 있다. (그리고 서블릿에서 해야 할 작업을 커맨드 클래스로 옮겨놓은 것이 커맨드 패턴을 모델 2 구조에 적용한 목적이었다.) 따라서 request 기본 객체의 속성(attribute)을 사용하여 서블릿이나 JSP 페이지에 특정한 값을 전달할 수 있으며, 위 코드의 경우는 게시판 글 목록을 저장하고 있는 BoardData 배열인 list를 request 기본객체의 속성에 저장하고 있음을 알 수 있다.

ListCommand와 비슷하게 모든 다른 명령어에 대해서 각각 하나의 커맨드 클래스를 작성을 하면 비로서 서블릿에서 커맨드 클래스들을 사용할 수 있게 된다. 다음은 서블릿에 커맨드 패턴을 적용했을 때의 코드 형태이다.

  public class BoardServlet extends HttpServlet {
  
     public void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        process(request, response);
     }
  
     public void doPost(HttpServletRequest request,
                        HttpServletResponse response)
                        throws IOException, ServletException {
        process(request, response);
     }
     
     private void process(HttpServletRequest request,
                          HttpServletResponse response)
                          throws IOException, ServletException {
        String command = request.getParameter("command");
        CommandIF processor = null;
        // 명령어에 따라서 알맞은 커맨드 클래스의 인스턴스를 생성한다.
        if (command == null) {
           command = new NullCommand();
        } else if (command.compareTo("List") == 0) {
           command = new ListCommand();
        } else if (command.compareTo("Write") == 0) {
           command = new WriteCommand();
        }
        
        ...
        
        String nextPage = null; // 명령어를 처리한 후 보여줄 JSP 페이지
        try {
           nextPage = processor.processCommand(request, response);
        } catch(ProcessingException ex) {
           request.setAttribute("javax.servlet.jsp.jspException", ex);
           nextPage = "/error/noticeError.jsp";
        }
        ServletContext sc = getServletContext();
        RequestDispatcher rd = sc.getRequestDispatcher(nextPage);
        rd.forward(request, response);
     }
  }

앞에서 살펴봤던 코드가 command 파라미터의 값에 따라 같은 클래스에 정의되어 있는 알맞은 메소드를 호출하였다면, 커맨드 패턴을 적용한 이후에는 알맞은 명령어에 따라 알맞은 커맨드 클래스의 인스턴스를 생성하고 그 인스턴스의 processCommand() 메소드를 호출한다는 점이 다르다.

팩토리 패턴을 커맨드 패턴과 함께 사용한다면 위 코드는 더욱 간단해진다. 예를 들어, CommandFactory 라는 클래스가 있고, 이 클래스의 createCommand(String command) 메소드는 파라미터로 전달받은 command의 값에 따라 알맞은 커맨드 객체를 리턴한다고 해 보자. 이는 다음과 같이 CommandFactory를 통해서 커맨드 객체를 생성할 수 있다는 것을 의미한다.

  CommandIF processor = CommandFactory.createCommand("List");

이를 모델 2의 서블릿 코드에 적용하면 다음과 같이 변한다.

  public class BoardServlet extends HttpServlet {
  
     public void doGet(HttpServletRequest request,
                       HttpServletResponse response)
                       throws IOException, ServletException {
        process(request, response);
     }
  
     public void doPost(HttpServletRequest request,
                        HttpServletResponse response)
                        throws IOException, ServletException {
        process(request, response);
     }
     
     private void process(HttpServletRequest request,
                          HttpServletResponse response)
                          throws IOException, ServletException {
        String command = request.getParameter("command");
        CommandIF processor = CommandFactory.createCommand(command);
        String nextPage = null; // 명령어를 처리한 후 보여줄 JSP 페이지
        try {
           nextPage = processor.processCommand(request, response);
        } catch(ProcessingException ex) {
           request.setAttribute("javax.servlet.jsp.jspException", ex);
           nextPage = "/error/noticeError.jsp";
        }
        ServletContext sc = getServletContext();
        RequestDispatcher rd = sc.getRequestDispatcher(nextPage);
        rd.forward(request, response);
     }
  }

if - else 가 사라짐으로써 서블릿 코드가 매우 간결해진다는 것을 알 수 있다.

커맨드 패턴을 모델 2 구조에 적용했을 때의 장점

이제 커맨드 패턴을 모델 2 구조에 적용했을 때의 장단점에 대해서 간략하게 정리해보도록 하자. 먼저 커맨드 패턴을 적용했을 때의 장점은 좀더 객체 지향적이라는 점이다. 즉, 각각의 명령어를 처리하는 클래스를 별도로 작성함으로써 서블릿은 전체적인 흐름 제어에만 신경을 쓰면 되고, 또한 각각의 명령어를 처리하는 클래스는 오직 그 명령어를 처리하는 작업에만 신경을 쓰면 된다.

또한, 각 기능별로 클래스를 분리해냈기 때문에 문제가 발생할 경우 어떤 부분에서 문제가 발생했는지 쉽게 발견할 수 있다는 장점도 있다. 예를 들어, "List"라는 명령어와 관련해서 예외가 발생했다면 "List" 명령어를 처리해주는 클래스를 살펴보면 된다. 이는 디버깅을 위해 불필요하게 서블릿 클래스를 이리 저리 살펴볼 필요가 없어진다는 것을 의미하며 더 나아가 유지 보수 작업 역시 덜 복잡해진다는 것을 의미한다.

기능별로 클래스를 분리해냈기 때문에 각 클래스의 코드가 복잡하지 않다는 것이다. 서블릿은 단순히 명령어에 알맞은 커맨드 클래스의 객체를 사용하는 역할만 맡고 있기 때문에 서블릿 코드는 매우 간단해지며, 또한 각각의 커맨드 클래스 역시 자신이 맡은 역할만 처리하기 때문에 코드가 복잡하지도 길지도 않게 된다.

결론

2부에서는 커맨드 패턴을 적용함으로써 모델 2 구조를 좀더 객체 지향적으로 구현하는 것에 대해서 살펴보았다. 이제 마지막으로 3부에서는 모델 2 구조와 흐름제어에 대해서 살펴볼 것이다.

관련링크:


+ Recent posts