저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의

JSTL 1.2의 <fmt:formatDate> 태그는 LocalDateTime과 같이 자바 8부터 제공하는 시간 타입에 대한 포맷팅 출력을 지원하지 않는다. 그래서 JSP에서 LocalDateTime 값을 지정한 형식에 맞춰 출력해주는 간단한 태그 파일을 만들었다.


아래의 태그 파일을 /WEB-INF/tags 폴더에 formatDateTime.tag 이름으로 만들었다.


<%@ tag body-content="empty" pageEncoding="utf-8" %>

<%@ tag import="java.time.format.DateTimeFormatter" %>

<%@ tag trimDirectiveWhitespaces="true" %>

<%@ attribute name="value" required="true" 

              type="java.time.temporal.TemporalAccessor" %>

<%@ attribute name="pattern" type="java.lang.String" %>

<%

if (pattern == null) pattern = "yyyy-MM-dd";

%>

<%= DateTimeFormatter.ofPattern(pattern).format(value) %>


JSP 코드에서는 다음과 같이 태그 파일을 사용해서 원하는 형식으로 값을 출력한다.


<%@ page contentType="text/html; charset=utf-8" %>

<%@ taglib prefix="tf" tagdir="/WEB-INF/tags" %>

<!DOCTYPE html>

<html>

<head>

    <title>회원 조회</title>

</head>

<body>

    ...

    <tf:formatDateTime

            value="${mem.registerDateTime}" 

            pattern="yyyy-MM-dd" />

    ...

</body>

</html>


Posted by 최범균 madvirus

댓글을 달아 주세요

깔끔한 JSP 코드를 유지하면서 메일을 발송하게 해주는 커스텀 태그를 설계한다.

메일 발송 커스텀 태그의 설계 과정

요즘 대부분의 웹 사이트는 가입된 사실을 알리거나 자사의 정보를 회원에게 알리기 위한 방법으로 메일을 사용하고 있다. 이것은 웹 어플리케이션에 알맞은 메일 발송 모듈을 개발할 필요성을 느끼게 하는 부분이기도 하다. 서블릿이나 JSP와 같이 자바 기반의 웹 어플리케이션의 경우는 Java Mail API를 사용하여 손쉽게 메일을 발송할 수 있다. (요즘 술치되는 대부분의 JSP/서블릿 책은 Java Mail API를 사용하여 메일을 전송하는 것에 대해서 나왔을 것이다.)

웹 어플리케이션에서 메일을 전송할 때는 다음과 같은 세 가지 방법 중에 한가지 방법을 사용하는 것이 일반적이다.

  • JSP에서 스크립트릿을 사용하여 메일을 전송한다.
  • 서블릿에서 메일 전송 후 결과를 보여준다. 모델 2 구조를 사용하여 표현부를 보여줄 수도 있다.
  • 메일 전송 관련 모듈을 개발하여 JSP와 서블릿에서 그 모듈을 사용하여 메일을 전송한다.
하지만, 서블릿에서 메일을 발송하거나 별도의 모듈을 사용하지 않고 JSP 페이지에서 바로 메일을 전송하는 방법의 경우는 스크립트릿이 JSP 페이지에 포함되기 때문에 표현부와 구현부의 분리가 되지 않는다. 반드시 표현부와 구현부를 분리해야 하는 것은 아니지만 JSP를 개발한 목적중의 하나가 표현부와 구현부를 분리하기 위한 것이라는 점을 생각해볼 때 JSP 페이지에서 스크립트릿을 사용하여 메일을 전송하는 것이 그리 올바른 방법은 아닐 것이다.

JSP 1.1 규약은 스크립트릿을 대체할 수 있는 방법으로서 커스텀 태그(Custom tag)를 제시하고 있다. 커스텀 태그는 이름에서 알 수 있듯이 JSP 페이지에서 사용되는 태그이다. 개발자들이 직접 커스텀 태그를 제작하기 때문에, 커스텀 태그는 개발자들이 원하는 어떤 기능도 제공할 수 있으며, JSP 페이지는 추상화된 커스텀 태그를 통해서 구현부를 분리해낼 수 있게 된다.

이 글에서는 JSP 페이지에서 메일을 전송할 때 사용할 수 있는 커스텀 태그를 설계해 볼 것이다. 커스텀 태그가 무엇이며 어떤 인터페이스를 제공하고 있는 지에 대한 내용은 이 글에서 설명하지 않을 것이므로, 그에 대한 내용은 JSP 관련 서적을 참고하기 바란다.

메일 전송 커스텀 태그를 설계하기 이전에 우리는 먼저 메일을 전송할 때 어떤 요소가 필요한지에 대해서 생각해보아야 한다. 이러한 요소에는 받는 사람, 보내는 사람, 메일의 주제, 내용 등이 있으며 이를 바탕으로 가장 간단한 형태의 메일 전송 커스텀 태그를 개발한다면 다음과 같은 형태를 띄게 될 것이다.

  <%
   fromEmail = request.getParameter("from");
   toEmail = request.getParameter("to");
   subject = request.getParameter("subject");
   body = "가입해주셔서 감사합니다.";
  %>
  <javacan:sendMail from="<%= fromEmail %>"
                       to="<%= toEmail %>"
                       subject="<%= subject %>"
                       body="<%= body %>"  />

sendMail 커스텀 태그는 보는 바와 같이 네 개의 속성 from, to, subject, body를 사용하여 메일 발송에 필요한 정보를 취합하고 있다. 위와 같이 sendMaile 커스텀 태그를 설계할 경우의 문제점은 메일의 내용을 변경하기 위해서는 스크립트릿을 사용해야 한다는 점이다. 예를 들면 다음과 같다.

  <%
    body = "먼저 저희 OOO.com에 가입해주셔서 감사드립니다.\n\n"+
           "저희는 "+member.getName()+" 회원님께서 편안하고 안전한 쇼핑을\n"+
           "하실 수 있도록 최선을 다하겠습니다.\n\n"+
           ...
           "..";
  %>

  <javacan:sendMail ... body="<%= body %>"  />
  

위와 같이 스크립트릿을 통해서 메일의 내용을 변경해야 할 경우 단순히 메일의 내용을 변경해야 하는 경우 개발자는 메일의 내용을 변경할 사람에게 스크립트릿이 무엇이며 어떤 부분을 건드려서는 안 된다라는 내용을 알려주어야 한다. 또한, HTML 형태의 메일을 전송해야 하는 경우에는 디자이너와 개발자가 반드시 함께 작업을 해야만 한다. 물론, 자바 스크립트를 익힌 디자이너들도 있지만 그렇다 하더라도 조금이라도 복잡한 부분이 스크립트릿에 나온다면 디자이너들은 수정 작업을 꺼려할 것이다.

그렇다면 메일의 내용을 좀더 간단하게 처리하기 위한 방법은 무엇일까? 정답은 sendMail 커스텀 태그의 몸체를 메일의 내용으로 하는 것이다. 즉, 다음과 같이 말이다.

  <javacan:sendMail from="<%= fromEmail %>"
                       to="<%= toEmail %>"
                       subject="<%= subject %>" >

  먼저 저희 OOO.com에 가입해주셔서 감사드립니다.
  
  저희는 <%= member.getName() %> 회원님께서 편안하고 안전한 쇼핑을
  하실 수 있도록 최선을 다하겠습니다.
  
  ...
  ..
  </javacan:sendMail>
  

위와 같이 변경함으로써 이제 디자이너들은 개발자의 도움없이도 메일 내용을 변경할 수 있을 것이다. 하지만, 표현식이 있는 것은 HTML 태그에만 익숙한 디자이너들에게는 여전히 불만족스러울 것이다. 하지만, 여기서 사용된 표현식은 다음과 같이 자바빈 액션 태그를 사용하면 매우 간단하게 태그형태로 변경할 수 있다.

  <javacan:sendMail from="<%= fromEmail %>"
                       to="<%= toEmail %>"
                       subject="<%= subject %>" >

  먼저 저희 OOO.com에 가입해주셔서 감사드립니다.
  
  저희는 <jsp:getProperty name="member" property="name" /> 회원님께서 편안하고 안전한 쇼핑을
  하실 수 있도록 최선을 다하겠습니다.
  
  ...
  ..
  </javacan:sendMail>
  

이 정도면 됐겠지라고 생각하는 사람들도 있을 것이다. 하지만, 위 코드는 여전히 문제점을 안고 있다. 바로 보내는 사람과 받는 사람 그리고 메일의 주제를 동적으로 정하려면 반드시 표현식을 써야한다는 점이다. 예를 들어, 위 코드는 실제로 다음과 같이 앞 부분에 스크립트릿 코드가 존재한다.

  <%   fromEmail = request.getParameter("from");
   toEmail = request.getParameter("to");
   subject = request.getParameter("subject");
  %>  <javacan:sendMail from="<%= fromEmail %>"
                       to="<%= toEmail %>"
                       subject="<%= subject %>" >

  먼저 저희 OOO.com에 가입해주셔서 감사드립니다.
  
  저희는 <jsp:getProperty name="member" property="name" /> 회원님께서 편안하고 안전한 쇼핑을
  하실 수 있도록 최선을 다하겠습니다.
  
  ...
  ..
  </javacan:sendMail>

여전히 스크립트릿 코드가 존재하는 것을 알 수 있다. 또한, sendMail 태그의 속성인 from, to, subject 의 값 역시 표현식이다. 이러한 스크립트 요소를 가급적 덜 사용하기 위해서는 from, to와 같은 속성값을 sendMail 커스텀 태그에 중첩되는 또 다른 커스텀 태그로 변환하는 것이 좋다. 다음은 그 예이다.

  <javacan:sendMail>
    <javacan:from>madvirus@tpage.com</javacan:from>
    <javacan:to><%= request.getParameter("toEmail") %></javacan:to>
    <javacan:subject>가입을 축하드립니다.</javacan:subject>
    <javacan:body>    먼저 저희 OOO.com에 가입해주셔서 감사드립니다.
    
    저희는 <jsp:getProperty name="member" property="name" /> 회원님께서 편안하고 안전한 쇼핑을
    하실 수 있도록 최선을 다하겠습니다.
    ...
    </javacan:body>
  </javacan:sendMail>

sendMail 태그의 from, to, subject 속성값을 사용할 때 보다는 복잡하지만 이제 보내는 사람을 지정하거나 주제를 지정할 때 표현식이나 일반 텍스트 뿐만 아니라 액션 태그도 사용할 수 있게 되었다. 예를 들어, 받는 사람의 이메일 주소를 다음과 같이 액션 태그를 사용하여 지정할 수도 있을 것이다.

  <javacan:to><jsp:getProperty name="member" property="email" /></javacan:to>

이렇게 속성 대신에 커스텀 태그를 사용함으로써 값을 지정할 때 표현식 대신 HTML 태그와 같은 액션 태그나 커스텀 태그를 사용할 수 있게 된다. 따라서 JSP 페이지에서 스크립트 요소를 좀더 줄일 수 있게 된다.

sendMail 커스텀 태그

지금까지 살펴본 내용을 바탕으로 sendMail 커스텀 태그의 기본 사양을 정의해보자. sendMail 태그의 전체 구성은 다음과 같다.

  <javacan:sendMail smtpServer="localhost" charset="euc-kr" >
     <javacan:from>fromEmail</javacan:from>
     <javacan:to>email1 ; email2 ; email3</javacan:to>
     <javacan:cc>email1 ; email2 ; email3</javacan:cc>
     <javacan:subject>주제</javacan:subject>
     <javacan:body html="true">
     내용
     </javacan:body>
  </javacan:sendMail>

위 태그의 구성만으로도 어느 정도 알 수 있겠지만 각 태그의 의미와 각 태그에서 사용되는 속성에 대해서 하나 하나 살펴보도록 하자.

  • sendMail : 이 태그는 메일 관련 커스텀 태그 중 가장 바깥에 위치하는 태그로서 smtpServer 속성과 charset 속성을 갖고 있다. smtpServer 속성은 메일 전송을 위해 연결할 SMTP 서버의 주소값을 가지며, charset은 메일을 구성하고 있는 텍스트의 캐릭터셋을 나타낸다.

  • from : 보내는 사람의 이메일 주소를 나타낸다. 보내는 사람의 이메일 주소의 형식은 다음과 같이 둘 중 하나를 사용할 수 있다.

    • abc@de.com(name)
    • abc@de.com

  • to : 받는 사람의 이메일 주소를 나타낸다. 여러 개의 이메일 주소를 입력할 수 있으며 각 이메일은 ; 기호를 사용하여 구분한다. 각각의 이메일 주소의 형식은 from 태그와 같다.

  • cc : 참조인의 이메일 주소를 나타낸다. to 태그와 마찬가지로 여러 개의 이메일 주소를 입력할 수 있으며, 이메일 형식 또한 from 태그와 같다.

  • subject : 주제를 입력받는다.

  • body : 본문 내용을 입력받는다. body 태그는 html 속성을 갖고 있는데 html 속성은 본문이 HTML 문서인지 단순 텍스트 인지를 나타낸다. html 속성의 값이 true일 경우 본문의 MIME 타입을 text/html 로 지정하고 false일 경우 text/plain 으로 지정한다.
결론

이번 1부에서는 메일 발송 커스텀 태그인 sendMail 태그를 설계하는 과정을 살펴보았다. 구체적인 구현에 대해서는 살펴보지 않았지만 sendMail 커스텀 태그를 사용함으로써 JSP 페이지에서 메일을 전송하기 위해 사용해야 했던 스크립트 코드가 상당량 줄어들게 되는 것을 알 수 있다. 또한, 커스텀 태그는 스크립트 코드에 비해 구분하기도 쉬우며 배우기도 쉽기 때문에 디자이너들이 어렵지 않게 몸체의 내용을 수정할 수 있게 된다.

다음 2부에서는 sendMail 커스텀 태그를 실제로 어떻게 구현하는 지 자세하게 살펴볼 것이다. 그 전까지 Java Mail API를 이용하여 메일을 전송하는 것에 대한 글을 읽어두면 좀더 쉽게 2부 글을 읽을 수 있을 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

모델 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 구조와 흐름제어에 대해서 살펴볼 것이다.

관련링크:


Posted by 최범균 madvirus

댓글을 달아 주세요

모델2 구조에 대한 기본적인 내용을 살펴본다.

모델1 구조

JSP는 이제 더 이상 일부 자바 개발자만 사용하는 기술이 아닌 웹 프로그래밍을 해야 하는 개발자들이 알아야 하는 기술이 되고 있다. 서점에 가보면 20여종에 가까운 JSP 서적이 존재하고 있으며 JSP 서적을 찾는 사람들 역시 많아지고 있다.

JSP가 이처럼 인기를 얻어 가고 있지만 막상 JSP 자체를 올바르게 사용하는 개발자는 매우 드문 것 같다. 특히 JSP 페이지로만 모든 걸 해결하는 개발자들도 많다고 볼 수 있다. 심지어는 ASP 코드를 그대로 JSP에서 사용하고(물론, VB 스크립트를 Java 언어로 변경하지만) 또는 PHP와 비슷한 형태로 사용하는 경우도 있다. 하지만, JSP가 세상에 나온 이유 중의 하나는 로직(logic)과 프리젠테이션(presentation)을 구분하기 위한 것이었다.

JSP는 로직과 표현부를 알맞게 구분하기 위해 모델 2 구조를 지원하고 있다. 이 글에서는 모델 2 구조가 무엇이며 어떤 식으로 구현되는 지, 그리고 모델 2 구조를 사용했을 때의 장단점에 대해서 살펴보도록 하자.

모델 1 구조와 단점

모델 2 구조에 대해 알아보기 전에 먼저 모델 1 구조에 대해서 살펴보자. 모델 1 구조는 JSP 페이지만으로 구성되어 있는 구조를 말한다. 즉, JSP 페이지에서 동적인 부분(즉, 로직 부분)은 스크립트릿으로 처리하고 그외 나머지 부분은 템플릿으로 처리하는 것이다. 그림1은 모델 1 구조의 전체 흐름을 보여주고 있다.


모델 1 구조의 전체 흐름

위 그림에서 클라이언트는 JSP 콘테이너에 HTTP 요청을 전송한다. 그러면 JSP 콘테이너는 HTTP 요청에 따라 알맞은 JSP에 그 요청을 전달하며, JSP 페이지는 클라이언트의 요청을 알맞게 처리한 후 응답을 클라이언트에 전송한다. 여기서 JSP 페이지는 클라이언트의 요청을 알맞게 처리하는 로직 부분을 구현하고 있다. 때에 따라 자바빈 컴포넌트에 로직 부분을 옮길 수도 있으나, 대부분의 경우 자바빈 컴포넌트는 단순히 데이터를 저장하는 역할만을 맡게 되며 모델 1 구조에서 JSP 페이지는 로직과 프리젠테이션의 역할을 동시에 맡게 된다.

예를 들어, 사용자가 특정한 JSP 페이지를 몇번 보았는지 알려주는 것을 생각해보자. 이 경우 세션이나 쿠키를 사용하여 사용자가 방문한 회수를 저장할 수 있다. 세션을 사용한다고 할 경우, JSP 페이지만을 사용하는 모델 1 구조에서는 다음과 같은 JSP 페이지를 작성할 것이다.

  <%@ page contentType="text/html; charset="euc-kr" %>
  <%@ page import="SessionCountBean" %>
  <%
     SessionCountBean count = (SessionCountBean)session.getAttribute("count");
     if (count == null) {
        count = new SessionCountBean();
        session.setAttribute("count", count);
     } else {
        count.increase();
     }
  %>
  <html>
  <head><title>방문회수</title></head>
  <body>
  지금까지 <%= count.getPageCount() %> 만큼 이 페이지에 방문하셨습니다.
  </body>
  </html>

위 코드를 보면 알겠지만 이 간단한 JSP 페이지에서조차도 HTML 코드 못지 않게 많은 양의 스크립트 코드가 JSP에 삽입되어 있다. JSP 페이지에서 데이터베이스부터 시작해서 모든 걸 처리한다고 할 경우 스크립트 코드와 HTML 코드는 스파게티처럼 막 뒤 섞이게 된다.

이렇게 로직 부분과 프리젠테이션 부분이 함께 섞여 있는 것은 JSP 개발자와 웹 디자이너가 함께 작업하는 데 많은 불편을 제공한다. 예를 들어, 개발자는 웹 디자이너가 작업한 HTML 문서의 알맞은 위치를 찾아서 스크립트 코드를 삽입해야 한다. 반대로 웹 디자이너는 디자인을 변경해야 할 때 최소한 JSP 스크립트 코드가 어떤 것인지 알아야 하며, 또한 HTML 코드 사이에 난잡하게 존재하는 스크립트 코드 때문에 사소한 디자인 변경 작업조차도 어려운 상황이 발생한다. 심지어, 개발자가 없이는 디자인을 변경하지 못하는 상황이 발생하기도 한다.

물론 위 코드를 다음과 같이 변경할 수도 있다.

  <%@ page contentType="text/html; charset="euc-kr" %>
  <jsp:useBean id="count" scope="session" class="SessionCountBean" />
  <%
     SessionCountBean count = (SessionCountBean)session.getAttribute("count");
     count.increase();
  %>
  <html>
  <head><title>방문회수</title></head>
  <body>
  지금까지 <jsp:getProperty name="count" property="pageCount" />
  만큼 이 페이지에 방문하셨습니다.
  </body>
  </html>

위 예제는 자바빈 컴포넌트 관련 JSP 태그를 사용한 것이다. 이전 예제에 비해 확실히 스크립트 코드의 양이 준것을 알 수 있다. 하지만, 여전히 JSP 페이지에서 로직과 관련된 부분을 처리하고 있으며 좀더 복잡한 로직을 갖는 웹 어플리케이션을 개발해야 할 경우 JSP 페이지는 로직과 프리젠테이션이 복잡하게 뒤섞이는 스파게티 코드가 될 것이다.

지금까지 살펴본 바와 같이 JSP 페이지가 모든 역할을 맡는 모델 1 구조의 경우는 JSP의 기본 목적인 로직과 프리젠테이션의 분리를 제대로 할 수 없음을 알 수 있다. 물론, 기본의 CGI나 서블릿에 비교해보면 많은 발전이 있지만 ASP나 PHP와 비교해볼 때 별다른 장점을 보이고 있지 못하다.

모델 2 구조

앞에서 살펴본 바와 같이 모델 1 구조는 로직과 프리젠테이션의 구분에 한계를 갖고 있다. 이를 알았는지 JSP 규약을 만든 사람들은 모델 2 구조라는 멋진 걸 만들어냈다. 모델 2 구조는 JSP와 서블릿 그리고 자바빈 컴포넌트를 함께 사용한다. 다음 그림은 모델 2 구조의 전체적인 흐름을 보여주고 있다.


모델 2 구조의 전체 흐름

위 그림을 보면 모델 1 구조와 달리 모델 2 구조는 서블릿이 클라이언트 요청을 처리하게 된다. 서블릿은 클라이언트의 요청을 받으면 그에 알맞은 로직을 실행한 후, 그 결과를 자바빈 컴포넌트와 같은 형태로 JSP 페이지에 전달한다. 객체를 전달받은 JSP 페이지는 그 결과를 사용하여 알맞은 결과 화면을 보여주기만 하면 된다. 즉, 모델 2 구조를 사용함으로써 JSP 페이지에서 로직을 처리하기 위한 스크립트 코드가 사라지는 것이다.

모델 2 구조의 구현

모델 2 구조를 구현하기 위해서는 기본적으로 서블릿에 대해 이해하고 있어야 하며, 더 나아가 RequestDispatcher 클래스에 대한 이해가 필요하다. javax.servlet.RequestDispatcher 클래스는 현재 클라이언트의 요청을 다른 서블릿/JSP 페이지로 전달하거나 또는 다른 서블릿/JSP 페이지의 결과를 현재 위치에 포함하고자 할 때 사용된다.

RequestDispatcher 객체는 이러한 기능을 제공하기 위해 forward() 메소드와 include() 메소드를 제공하고 있으며, 이 두 메소드는 JSP의 <jsp:forward> 액션태그와 <jsp:include> 액션태그와 같은 기능을 제공한다. 예를 들어, 현재 요청을 showCount.jsp로 보내고 싶다면 다음과 같이 RequestDispatcher 클래스의 forward() 메소드를 사용하면 된다.

  public void doGet(HttpServletRequest request, HttpServletResponse response) .. {
     ...
     
     RequestDispatcher rd = request.getRequestDispatcher("/showCount.jsp");
     rs.forward(request, response);
  }

모델 2 구조는 바로 이 RequestDispatcher.forward() 메소드를 통해서 구현된다. 모델 1 구조에서 예로 들었던 페이지 카운터를 모델 2 구조로 구현해보자. 먼저 로직을 처리하는(즉, 카운트를 1 증가시켜주는) 서블릿은 다음과 같다.

  import javax.servlet.*;
  import javax.servlet.http.*;
  import java.io.IOException;
  
  public class CounterServlet extends HttpServlet {
  
     public void doGet(HttpServletRequest request,
                       HttpServletResponse reponse)
                       throws ServletException, IOException {
        HttpSession session = request.getSession(true);
        SessionCountBean count = 
                    (SessionCountBean)session.getAttribute("count");
        if (count == null) {
           count = new SessionCountBean();
           session.setAttribute("count", count);
        }
        count.increase();
        
        RequestDispatcher rd = request.getRequestDispatcher("/showCount.jsp");
        rs.forward(request, response);
     }
  }

CounterServlet은 로직에 해당하는 부분을 모두 처리하고 있으며, 그 결과를 세션에 저장한다. 그런 후, RequestDispatcher.forward() 메소드를 사용하여 프리젠테이션 역할을 담당하는 JSP 페이지에 클라이언트의 요청을 전달한다. 이것으로서 CounterServlet의 모든 임무는 끝이난다. CounterServlet은 프리젠테이션과 관련된 어떤 것도 처리하지 않으며, 오직 로직을 처리하는 임무만을 담당한다.

이제 showCount.jsp를 살펴보자. showCount.jsp는 이미 CountServlet은 로직과 관련된 부분을 모두 처리해주고 있기 때문에 프리젠테이션과 관련된 부분만 처리하면 된다. 실제로 이 예제에 알맞은 JSP 페이지는 다음과 같다.

  <%@ page contentType="text/html; charset="euc-kr" %>
  <jsp:useBean id="count" scope="session" class="SessionCountBean" />
  <html>
  <head><title>방문회수</title></head>
  <body>
  지금까지 <jsp:getProperty name="count" property="pageCount" />
  만큼 이 페이지에 방문하셨습니다.
  </body>
  </html>

위 코드를 살펴보면 어떤 스크립트 코드도 존재하지 않는 것을 알 수 있다. 물론, 이 예제는 모델 2 구조를 설명하기 위해 만들어지긴 것이긴 하지만, 모델 2 구조를 사용할 경우 모델 1 구조를 사용할 때에 비해 로직과 프리젠테이션을 확실하게 구분할 수 있다는 것을 알 수 있다.

비니니스 로직이 제 아무리 복잡해진다 해도 JSP 페이지에 삽입되는 스크립트 코드의 양은 크게 증가하지 않는다. 왜냐면 서블릿이 전송한 자바빈 컴포넌트는 모두 <jsp:useBean>과 <jsp:getProperty>와 같은 HTML과 비슷한 JSP 태그를 사용하여 처리할 수 있기 때문이다.

모델 2 구조의 장단점

지금까지 설명한 내용을 살펴보면 모델 2 구조를 사용할 경우 모델 1 구조로는 흉내낼 수 없을 정도로 로직과 프리젠테이션의 구분을 명확하게 할 수 있다는 것을 알 수 있다. 하지만, 모델 2 구조가 장점만 존재하는 것은 아니다. 이 글에서는 마지막으로 모델 2 구조의 장점과 단점에 대해서 살펴보자.

먼저 모델 2 구조의 장점은 다음과 같다.

  • 로직과 프리젠테이션의 구분을 좀더 명확히 할 수 있다! 역시 모델 2 구조의 최대 장점은 로직과 프리젠테이션의 구분을 명확하게 할 수 있다는 것이다. 로직의 구현을 담당하는 개발자들은 서블릿을 개발하는 데에만 온 신경을 집중하면 된다. 물론, <jsp:getProperty>와 같은 JSP 태그를 알맞게 JSP 페이지에 삽입해야 하지만 이 작업은 스크립트릿이나 표현식과 같은 스크립트 요소를 삽입하는 것에 비해 매우 간단하며 복잡하지도 않다.

    디자이너 역시 작업이 수월해진다. JSP와 관련된 태그가 <jsp:... > 형태를 갖는다는 정도만 알고 있으면 되며, 차후에 디자인을 일부 수정할 때 역시 복잡한 스크립트 코드가 들어가 있지 않기 때문에 개발자의 도움 없이도 손쉽게 원하는 작업을 수행할 수 있게 된다.

  • 유지보수가 편해지고 뛰어난 확장성을 갖는다 모든 로직 부분은 서블릿에서 처리되므로 로직이 변경될 경우 서블릿 또는 서블릿이 사용하는 클래스만 변경하면 되며, 프리젠테이션 부분은 전혀 손댈 필요가 없다. 또한, 프리젠테이션 부분이 변경되더라도 로직 부분은 변경할 필요가 없다. 모델 1 구조의 경우 로직과 프리젠테이션이 한데 섞여 있으므로 변경하는 작업 자체가 고될 수 있다.

    뿐만 아니라 확장성에 있어서도 모델 1 구조와 상대가 안 된다. 모델 1 구조는 프리젠테이션과 로직이 복잡하게 섞여 있기 때문에 확장가능한 형태로 개발한다는 것이 좀처럼 쉬운 일이 아니다. 하지만, 모델 2 구조는 확장 가능한 구조를 갖도록 설계하는 것이 크게 어렵지 않으며, 특히 최근 유행하고 있는 커맨드 패턴이나 팩토리 패턴등을 함께 사용하면 확장성은 극도로 향상된다.

모델 2 구조가 위와 같은 장점을 갖고 있긴 하지만, 단점 역시 갖고 있다. 여기서 나열하는 단점은 모델 1 구조에 있어서는 장점으로 작용한다.

  • 개발이 어려워질 수 있다 모델 2 구조는 기본적으로 서블릿/자바빈 컴포넌트/JSP의 세 가지 요소가 필요하다. 즉, JSP 페이지 하나만을 개발하면 되는 모델 1 구조에 비해 개발해야 할 것이 증가한다는 것이다. 게다가 자바빈 컴포넌트를 올바르게 설계할 능력을 갖추고 있지 않다면 모델 2 구조를 사용하는 것은 오히려 프로젝트의 암덩어리가 될 수도 있다.

    특히 최근에는 JSP 페이지만 슥듭한 개발자들이 많아지고 있으며, 이러한 개발자의 경우 자바 언어 자체에 대한 이해는 상당히 부족한 편이다. 모델 2 구조가 비록 JSP와 서블릿을 사용하는 것이지만, 이 두 기술은 모두 자바에 기반하고 있다는 점을 생각해볼 때 자바에 대한 기초가 약한 개발자들이 증가한다는 것은 그 만큼 모델 2 구조를 사용하는 것 자체가 힘들수도 있다는 것을 의미한다.

  • 비용이 증가한다. 모델 2 구조를 사용하여 개발하기 위해서는 충분한 시간을 갖고서 어플리케이션의 전체 구조를 생각해보야만 한다. 물론, 자바빈 컴포넌트와 같은 추가적인 개발 시간도 필요하다. 게다가 좀더 안정적이고 좀더 확장가능하도록 하려면 그만큼 웹 어플리케이션을 설계하는 데 많은 시간이 소요되며, 이는 곧 비용의 증가와 연결된다.

이처럼 모델 2 구조는 장점도 갖고 있고 단점도 갖고 있다. 여기서 설명하진 않았지만, 모델 2 구조는 또한 모델 1 구조에 비해 웹 어플리케이션의 흐름을 보다 효과적으로 제어할 수 있으며 또한 보다 안정적으로 웹 어플리케이션을 개발할 수 있도록 해준다. 이러한 모델 2 구조의 특징 때문에 모델 2 구조는 EJB를 사용하는 대규모 프로젝트에서 주로 사용되고 있다. 반면에 웹 사이트의 전체 흐름이 비교적 간단하고 빠른 시간내에 제작해야 소규모 프로젝트에서는 모델 1 구조가 주로 사용된다.

결론

이번 글에서는 모델 2 구조가 어떤 흐름을 갖는 지에 대해서 살펴보았고, 모델 2 구조의 구현에 대해서도 간단하게 살펴보았다. 또한, 모델 2 구조의 장단점에 대해서 살펴봄으로써 모델 2 구조를 언제 사용해야 할지에 대한 내용도 살펴보았다.

아직 이번 글만으로는 모델 2 구조가 어떤 면에서 모델 1 구조보다 나은 지 확실하지 않을 것이다. 다음 글에서는 모델 2 구조와 MVC(Model-View-Controller) 패턴에 대해서 살펴볼 것이다. 이를 통해 여러분은 모델 2 구조의 장점을 좀더 명확하게 이해할 수 있을 것이며, 더 나아가 모델 2 구조의 매력을 느끼게 될 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. R_ 2011.08.19 18:45 신고  댓글주소  수정/삭제  댓글쓰기

    안녕하세요 ^^ 좋은자료 잘 보고갑니다

자바빈 컴포넌트에서 데이터베이스에 연결함으로써 표현과 구현의 분리를 좀더 명확하게 할 수 있다.

자바빈 컴포넌트와 데이터 처리

많은 개발자들이 웹 어플리케이션을 개발하기 위해 JSP(Java Server Pages)를 선택하고 있다. JSP를 사용함으로써 개발자들은 자바의 장점을 사용할 수 있으며, 또한 표현부(presentation)와 구현부(implementation)를 손쉽게 구분할 수 있게 된다. 특히 자바빈 컴포넌트와 커스텀 태그의 지원은 컴포넌트의 관점에서 웹 페이지를 개발할 수 있는 프레임워크를 제공한다.

하지만, 개발자 중의 다수가 JSP의 프레임워크를 올바르게 사용하고 있지 않으며, 심지어 JSP를 개발할 때, ASP나 PHP의 코드를 자바로 변환하는 정도로 생각하는 경우도 있다. 이러한 것의 대표적인 경우가 JSP 페이지에서 직접 데이터베이스에 접근하는 것이다. 물론, 필요에 따라 JSP 페이지에서 직접 데이터베이스에 접근할 수 있지만, 대부분의 경우에 JSP 페이지에서 직접적으로 데이터베이스에 접근하는 것은 올바르지 못하며, 웹 어플리케이션의 전체 구조를 견고하지 못하게 만드는 요인이 될 수 있다.

이 글에서는 자바빈 컴포넌트에서 데이터베이스와 관련된 작업을 처리하도록 함으로써 이러한 문제를 해결하는 것에 대해서 알아볼 것이다. 이렇게 함으로써 JSP 페이지로부터 구현부와 관련된 부분을 제거할 수 있으며, 따라서 JSP 페이지는 표현부의 역할에 좀더 충실해질 수 있다. 다시 말해서, 표현부와 구현부를 좀더 명확하게 분리할 수 있는 것이다.

자바빈 컴포넌트

이 글에서 예를 들기 위해 사용할 자바빈 컴포넌트는 회원 정보를 나타내는 MemberBean 클래스이다. MemberBean 컴포넌트는 웹사이트에서 가입할 때 주로 사용되는 회원아이디, 패스워드, 회원 이름, 회원의 주소, 연락처 정보를 저장하고 있으며, 이들은 다음 표에 명시한 프로퍼티를 사용하여 표시된다.

MemberBean 컴포넌트의 프로퍼티 목록
프로퍼티 이름 타입 설명
memberID String 회원 아이디 저장
memberPassword String 회원의 로그인 암호
memberName String 회원의 이름
memberPhone String 회원의 전화번호
memberAddress String 회원의 주소
MemberBean 클래스의 소스 코드는 다음과 같은 골격을 갖게 된다.

package javacan.member.bean;

import java.sql.*;

public class MemberBean implements Serializable {
   
   private String memberID;
   private String memberPassword;
   private String memberName;
   private String memberPhone;
   private String memberAddress;
   
   public void setMemberID(String memberID) {
      this.memberID = memberID;
   }
   public String getMemberID() {
      return memberID;
   }
   public void setMemberPassword(String memberPassword) {
      this.memberPassword = memberPassword;
   }
   public String getMemberPassword() {
      return memberPassword;
   }
   
   public void setMemberName(String memberName) {
      this.memberName = memberName;
   }
   public String getMemberName() {
      return memberName;
   }
   public void setMemberPhone(String memberPhone) {
      this.memberPhone = memberPhone;
   }
   public String getMemberPhone() {
      return memberPhone;
   }
   public void setMemberAddress(String memberAddress) {
      this.memberAddress = memberAddress;
   }
   public String getMemberAddress() {
      return memberAddress;
   }
   
   // 나머지 부분이 온다.

}

위 코드는 자바빈 컴포넌트의 전형적인 형태를 취하고 있다. 여기에 차례대로 데이터베이스와 관련된 작업을 추가해나가보자.

데이터 삽입

데이터를 삽입이라고 제목을 붙이긴 했지만, 특별히 어려운 것은 아니다. 우리가 흔히 테이블에 데이터를 삽입할 때 사용하는 쿼리문을 자바빈 컴포넌트에서 실행한다고 생각하면 된다. MemberBean 컴포넌트는 저장하고 있는 데이터를 DB 테이블에 삽입해주는 메소드인 insert()를 정의하며, 이는 다음과 같다.

   public void insert() throws SQLException {
      Connection conn = null;
      PreparedStatement pstmt = null;
      
      try {
         conn = DriverManager.getConnection("...");
         pstmt = conn.prepareStatement("insert into Member values (?,?,?,?,?)");
         pstmt.setString(1, memberID);
         pstmt.setString(2, memberPassword);
         pstmt.setString(3, memberName);
         pstmt.setString(4, memberPhone);
         pstmt.setString(5, memberAddress);
         
         pstmt.executeUpdate();
      } finally {
         if (pstmt != null) try { pstmt.close(); } catch(SQLException ex1) {}
         if (conn != null) try { conn.close(); } catch(SQLException ex1) {}
      }
   }

여기서 DriverManager.getConnection() 메소드에 전달된 JDBC URL은 해당 DBMS에 알맞은 걸 사용할 것이다. 데이터베이스 커넥션 풀링을 사용함으로써 성능 향상을 꾀할 수도있지만, 이 글에서는 자바빈 컴포넌트가 데이터베이스 작업을 한다는 점에 초점을 맞추고 있으므로 간단한 코드를 사용한다.

이제 회원가입 폼을 보여주는 registerForm.html과 자바빈 컴포넌트를 사용하여 가입처리를 하는 register.jsp를 살펴보자. registerForm.html은 단순한 HTML로서 다음과 같다.

<html>
<head><title>회원가입폼</title></head>
<body>
<form action="register.jsp" method="post">
아이디: <input type="text" name="memberID" size="10"> <br>
암호: <input type="password" name="memberPassword" size="10"> <br>
이름: <input type="text" name="memberName" size="20"> <br>
전화: <input type="text" name="memberPhone" size="15"> <br>
주소: <input type="text" name="memberAddress" size="40"> <br>
<input type="submit" value="등록">
</form>
</body>
</html>

register.jsp 페이지는 registerForm.html을 통해 입력한 정보를 사용하여 가입신청을 처리하는 부분으로서 다음과 같다.

<jsp:useBean id="member" class="javacan.member.bean.MemberBean">
   <jsp:setProperty name="member" property="*" />
</jsp:useBean>
<html>
<head><title>회원가입완료</title></head>
<body>
<% member.insert() %>
<jsp:getProperty name="member" property="name" />님의 회원가입을
진심으로 축하합니다.
<p>
앞으로 많은 활동부탁드립니다.
</body>
</html>

위 코드를 보면 register.jsp는 가장 먼저 <jsp:useBean> 액션태그와 <jsp:setProperty> 액션태그를 사용하여 이전 페이지에서 사용자가 폼을 통해 입력한 정보를 MemberBean 컴포넌트저에 저장한다. 그런 후, MemberBean 컴포넌트의 insert() 메소드를 호출함으로써 사용자가 입력한 정보를 데이터베이스 저장하게 된다. 위 코드를 살펴보면 스크립트 부분이 단 한줄에 불과하다는 것을 알 수 있다. 이것이 바로 자바빈 컴포넌트에 역할을 집중시킬 때의 장점이라고 할 수 있다. 물론, insert() 메소드가 발생할 수 있는 예외를 알맞게 처리해야 하지만, 이는 register.jsp 페이지에 <%@ page errorPage=".." %> 를 추가함으로써 예외 처리 부분을 register.jsp 페이지로부터 분리해낼 수 있다.

데이터 조회, 변경, 삭제

데이터의 조회와 데이터 삭제 역시 데이터 삽입과 같은 방법으로 처리된다. 때에 따라서 조회와 삭제를 위한 자바빈 클래스를 별도로 작성할 수도 있다. 특히 조회의 경우는 한 사람의 회원을 조회할 수도 있고, 여러명의 회원을 조회할 수도 있다. 한 사람의 회원을 조회하는 경우에는 MemberBean 클래스에서 조회 코드를 위치시키는 것이 좋은 방법이 될 수 있지만, 한 사람이 아닌 여러명의 사용자를 동시에 조회하는 경우에는 자바빈 컴포넌트에서 처리하기 보다는 자바빈 컴포넌트가 나타내는 엔티티와 관련된 작업을 처리하는 매니저 클래스(EJB의 경우 세션빈에 해당한다)에서 처리하는 것이 좋을 수도 있다. 이 글에서는 MemberBean 클래스에 조회를 위한 메소드인 select()를 추가해보자.

select() 메소드는 한명의 회원을 검색하기 위해 사용되며, 파라미터로 회원의 ID를 전달받는다. MemberBean의 select() 메소드는 다음과 같다.

   public MemberBean select(String id) throws SQLException {
      Connection conn = null;
      PreparedStatement pstmt = null;
      ResultSet rs = null;
      
      try {
         conn = DriverManager.getConnection("...");
         pstmt = conn.prepareStatement(
             "select ID, PASSWD, NAME, PHONE, ADDR from Member where ID=?");
         pstmt.setString(1, id);
         rs = pstmt.executeQuery();
         if (rs.next()) {
            memberID = rs.getString(1);
            memberPassword = rs.getString(2);
            memberName = rs.getString(3);
            memberPhone = rs.getString(4);
            memberAddress = rs.getString(5);
            
            return this;
         } else {
            return null;
         }
      } finally {
         if (rs != null) try { rs.close(); } catch(SQLException ex1) {}
         if (pstmt != null) try { pstmt.close(); } catch(SQLException ex1) {}
         if (conn != null) try { conn.close(); } catch(SQLException ex1) {}
      }
   }

위에서 조회 결과가 없을 때는 null을 리턴하는 데, 때에 따라서는 조회 결과가 없는 경우에 예외를 발생하도록 하는 것이 좋을 수도 있다. 이러한 문제는 웹 어플리케이션의 전체적인 규칙에 따라서 결정되어야 할 것이다. 참고로, 필자의 경우 추가적으로 EmptyResultException과 같은 예외를 만들어 예외를 발생하는 방법을 주로 사용하고 있다.

조회를 위한 JSP 페이지는 좀더 간단하다. 먼저 조회를 하고자 하는 회원의 ID를 입력받는 폼이 필요할 것이다. 이 폼은 selectForm.html을 통해서 제공되며, selectForm.html의 소스 코드는 다음과 같다.

<html>
<head><title>조회폼</title></head>
<body>
<form action="select.jsp">
조회할 아이디: <input type="text" name="memberID" size="10">
<br>
<input type="submit" value="조회">
</form>
</body>
</html>

selectForm.html로부터 조회하고자하는 회원의 ID를 전달받아 MemberBean 컴포넌트를 사용하여 데이터를 데이터베이스로부터 읽어오는 select.jsp는 다음과 같다.

<jsp:useBean id="member" class="javacan.member.bean.MemberBean" />
<html>
<head><title>조회</title></head>
<body>
<%  if (member.select(request.getParameter("memberID")) == null ) {  %>
조회 결과가 없습니다.
<%  } else {  %>
아이디: <jsp:getProperty name="member" property="memberID" />
<br>
이름: <jsp:getProperty name="member" property="memberName" />
<br>
전화: <jsp:getProperty name="member" property="memberPhone" />
<br>
주소: <jsp:getProperty name="member" property="memberAddress" />
<%  }  %>
</body>
</head>

이제, 회원 정보를 변경하는 메소드인 update()를 추가해보자. 이 메소드는 현재 MemberBean 컴포넌트에 저장되어 있는 정보를 데이터베이스에 반영하는 역할을 한다. update() 메소드는 다음과 같다.

   public void update() throws SQLException {
      Connection conn = null;
      PreparedStatement pstmt = null;
      
      try {
         conn = DriverManager.getConnection("...");
         pstmt = conn.prepareStatement(
               "update Member set NAME=?, PHONE=?, ADDR=? where ID=?");
         pstmt.setString(1, memberName);
         pstmt.setString(2, memberPhone);
         pstmt.setString(3, memberAddress);
         pstmt.setString(4, memberID);
         
         pstmt.executeUpdate();
      } finally {
         if (pstmt != null) try { pstmt.close(); } catch(SQLException ex1) {}
         if (conn != null) try { conn.close(); } catch(SQLException ex1) {}
      }
   }

insert() 메소드와 거의 비슷하다는 것을 알 수 있다. 회원 정보를 변경하기 이해서는 두 개의 JSP 페이지가 필요하다. 한 개는 정보를 변경할 수 있는 폼을 제공해주는 JSP 페이지이고, 다른 하나는 폼에 입력한 정보를 실제로 데이터베이스 반영하는 JSP 페이지이다. 먼저 정보를 변경할 수 있는 폼을 제공해주는 updateForm.jsp를 살펴보자. 다음은 updateForm.jsp의 소스 코드이다.

<jsp:useBean id="member" class="javacan.member.bean.MemberBean">
<%  member.select(request.getParameter("memberID")); %>
<html>
<head><title>회원정보수정폼</title></head>
<body>
알맞게 정보를 변경하세요.
<form action="update.jsp" method="post">
<input type="hidden" name="memberID"
       value="<jsp:getProperty name="member" property="memberID" />" >
아이디: <jsp:getProperty name="member" property="memberID" /><br>
암호: <input type="password" name="memberPassword" size="10"
      value="<jsp:getProperty name="member" property="memberPassword" />" > <br>
이름: <input type="text" name="memberName" size="20"
     value="<jsp:getProperty name="member" property="memberName" />" > <br>
전화: <input type="text" name="memberPhone" size="15"
     value="<jsp:getProperty name="member" property="memberPhone" />"> <br>
주소: <input type="text" name="memberAddress" size="40"
     value="<jsp:getProperty name="member" property="memberAddress" />"> <br>
<input type="submit" value="변경">
</form>
</body>
</html>

updateForm.jsp는 변경할 회원의 ID를 파라미터를 통해서 입력받는다. 만약 파라미터가 아닌 쿠키나 세션을 사용해서 회원 아이디를 읽어온다면 그에 알맞게 변경하면 될 것이다. updateForm.jsp는 회원 ID를 읽어온 후, MemberBean.select() 메소드를 사용하여 해당 회원의 정보를 읽어온다. 그런 후, 화면에 값을 변경할 수 있는 화면을 보여준다.

update.jsp는 앞에서 살펴본 insert.jsp와 거의 유사하다. <jsp:useBean> 태그를 사용하여 사용자가 입력한 회원 정보를 입력받은 후, MemberBaan 컴포넌트의 update() 메소드를 호출하기만 하면 된다. 다음은 update.jsp의 소스 코드이다.

<jsp:useBean id="member" class="javacan.member.bean.MemberBean">
   <jsp:setProperty name="member" property="*" />
</jsp:useBean>
<html>
<head><title>회원정보변경</title></head>
<body>
<% member.update() %>
<jsp:getProperty name="member" property="name" />님의 회원 정보를 변경하였습니다.
</body>
</html>

회원 정보의 삭제 역시 지금까지 살펴본 것과 비슷한 방법을 사용하여 할 수 있을 것이다.

결론

JSP는 자바를 사용하여 웹 어플리케이션을 개발할 수 있는 효과적인 모델을 제공한다. 이 글에서는 데이터베이스에 데이터를 삽입하고, RDB로부터 데이터를 추출해내고, RDB에서 데이터를 삭제하는 구현부를 자바빈 컴포넌트에서 처리하는 방법을 알아보았다. 비록 JDBC를 이용한 데이터베이스 처리 코드를 JSP 페이지에 삽입할 수도 있지만, 데이터베이스 처리 코드를 자바빈에 넣는 것이 웹 어플리케이션에 있는 각 구성요소의 역활을 좀더 명확하게 구분지어준다. 또한, 자바빈 컴포넌트가 데이터베이스 작업을 처리하도록 함으로써 JSP 페이지가 더욱 더 표현부만을 처리하도록 할 수 있게 되었다.

개념을 좀더 확장하여 자바빈 컴포넌트와 관련된 엔티티를 관리해주는 매니저 클래스를 작성한다고 생각해보자. 이는 비지니스 로직과 관련된 부분을 매니저 클래스에서 처리한다는 것을 의미하며, 따라서 자바빈 컴포넌트와 JSP의 역할은 좀더 세분화될 것이며, 이에 따라 전체 웹 어플리케이션의 구성 역시 견고해질 것이다. 또한, 자바빈 컴포넌트와 매니저 클래스를 EJB의 엔티티빈과 세션빈으로 어렵지 않게 확장할 수 있을 것이다. 실제로 이에 대한 내용은 자바캔에 필자가 기고한 "Singleton 패턴을 적용한 매니저 클래스와 견고한 웹 어플리케이션의 개발"에서 언급한 적이 있으니, 그 기사를 참고하기 바란다.

관련링크:
Posted by 최범균 madvirus
TAG DB, JSP, 자바빈

댓글을 달아 주세요

  1. AiRukia 2009.06.12 00:54 신고  댓글주소  수정/삭제  댓글쓰기

    여러모로 많이 배우고 갑니다.좋은 글 올려주셔서 감사드립니다.
    오늘도 즐거운 하루 되세요.^^