반응형

일반적으로 Action 클래스에서는 다음과 같은 로직을 통해 중복 Form Submit 여부를 체크할 수 있다.

  • Action Class

    boolean valid = isTokenValid(request, true);
    if (valid) {
        // TODO: submit 할 때 수행할 로직을 넣을 것
        System.out.println("status: performed");
    } else {
        // TODO: init / reload 할 때 수행할 로직을 넣을 것
        System.out.println("status: initialized or reloaded");
    }
    saveToken(request);

  • JSP

    <input type="hidden" name="org.apache.struts.taglib.html.TOKEN" 
    value="<%= session.getAttribute(org.apache.struts.Globals.TRANSACTION_TOKEN_KEY) %>">

    UI(JSP, HTML)에서는 "org.apache.struts.taglib.html.TOKEN"을 Key로 하는 Hidden Field를 통해 할당된 Token 값을 서버로 전송하고, 해당 Action 클래스에서는 isTokenValid 메소드 호출을 통해 이 Token 값과 Session에 저장된 Token 값을 비교함으로써, Token의 유효성을 검사한다.

위에 소스에 대해 개괄적으로 설명하면 Action 을 처리하면서 saveToken(request); 부분에서
세션에 토큰키를 생성하여 저장하게 된다.(현재시간과 세션아이디를 가지고 MD5 알고리즘을 이용해 생성)

JSP페이지에서 이를 히든타입으로 form에 담게 되고 submit을 통해 Action으로 가면
현재 세션에 저장되어 있는 토큰키와 form에 담겨 request로 넘어온 토큰키와 비교를 하게 된다.
그 키값이 같으면 정상처리를 하게 되고 saveToken(request); 부분을 통해 신규 토큰키가 생성되고
세션에 다시 담기게 된다. 이후에 새로고침이나 뒤로가기를 통해 다시 넘어오는 토큰키는 이전에 처리된
토큰키를 그대로 담고 있기 때문에 새로 생성된 신규 토큰키와 다르므로 중복처리를 제어할수 있는것이다.

토큰에 관한 부분은 아래 링크에 상세히 설명되어 있고 jsp에 name은 명확히 동일하게 작성되어야 한다.
 
아래 링크에서 "56.3.1. Double Submit의 개념" 부분을 참고하도록 하자.
http://dev.anyframejava.org/docs/anyframe/4.6.0/reference/html/ch56.html
반응형

Tomcat 5.0 JNDI Datasoruce 설정


JNDI Datasoruce 설정 개요

  • Tomcat 5 JNDI DataSource를 통한 DB 커넥션 풀 사용
  • JNDI를 통한 커넥션 풀 사용은 J2EE 표준이고, 현존하는 거의 모든 웹 컨테이너가 지원
  • Jakarta의 DBCP 커넥션 풀과 Tomcat JNDI 설정을 통해 데이터베이스 커넥션 풀을 사용하는 방법 제시
  • 기본적으로 필요한 라이브러리
    • commons-dbcp.jar
    • commons-collections.jar
    • commons-pool.jar
    • DB에 대한 JDBC 라이브러리 (JAR)

JNDI Naming Resource 설정

  1. 위 라이브러리들을 $CATALINA_HOME/common/lib 에 복사한다. 그 이외 디렉토리에 두면 안된다.
  2. Connection 풀을 이용할 경우에는 ResultSet과 Connection 객체를 필히 직접 닫아 줘야만 한다.
  3. $CATALINA_HOME/conf/server.xml 혹은 각 웹 컨텍스트별 XML 파일의 <Context>의 자식 요소로 <Resource>를 추가한다.
  4. 웹 어플리케이션의 web.xml파일에 <resource-ref>를 추가하여 JNDI 리소스를 사용할 수 있도록 한다.
  5. 전역적인 JNDI 리소스를 이용하고자 하는 경우는 <GlobalNamingResources>

Server.xml

server.xml
<Resource name="jdbc/forumDb" auth="Container" type="javax.sql.DataSource"/>
<!-- Resource의 name 속성을 이용해서 각 어플리케이션에서 javax.sql.DataSource 객체를 얻어가게 된다. -->

<!-- 자세한 파라미터 설정은 위의 참조사이트에서 확인 -->
<ResourceParams name="jdbc/forumDb">
<parameter>
<name>factory</name>
<value>org.apache.commons.dbcp.BasicDataSourceFactory</value>
</parameter>

<parameter>
<name>maxActive</name>
<value>100</value>
</parameter>

<parameter>
<name>maxIdle</name>
<value>30</value>
</parameter>

<parameter>
<name>maxWait</name>
<value>10000</value>
</parameter>

<!-- DB 사용자명과 비밀번호 설정 -->
<parameter>
<name>username</name>
<value>dbuser</value>
</parameter>
<parameter>
<name>password</name>
<value>dbpasswd</value>
</parameter>

<!-- JDBC 드라이버 클래스 -->
<parameter>
<name>driverClassName</name>
<value>oracle.jdbc.driver.OracleDriver</value>
</parameter>

<!-- JDBC 접속 URL -->
<parameter>
<name>url</name>
<value>jdbc:oracle:thin:@dbhost:1521:ORA</value>
</parameter>
</ResourceParams>
전역 JNDI 리소스 이용
  • <Resource>와 <ResourceParams> 요소를 server.xml의 <GlobalNamingResources> 의 자식노드로 옮기면 특정 웹 어플리케이션이 아니라, 이 톰캣에 설치된 전체 웹 어플리케이션에서 사용 할 수 있게 된다. 하지만 각 웹 어플리케이션 "<Context>"에 다음과 같은 설정을 해야 한다.
server.xml의 <Context> 부분에 추가
<ResourceLink
name="jdbc/forumDb"
global="jdbc/forumDb"
type="javax.sql.DataSource"
/>
  • 아니면 server.xml에서 <Host> 요소의 자식으로 다음을 추가하면 각 컨텍스트별 설정 필요없이 전체 웹 어플리케이션 컨텍스트에서 GlobalNamingResources로 지정된 JNDI 리소스를 사용할 수 있다.
server.xml의 <Context> 부분에 추가
<DefaultContext>
<ResourceLink
name="jdbc/forumDb"
global="jdbc/forumDb"
type="javax.sql.DataSource"
/>
</DefaultContext>

web.xml

web.xml에 리소스에 대한 reference를 설정
<resource-ref>
<description>Forum DB Connection</description>
<!-- 다음이 바로 리소스의 이름 -->
<res-ref-name>jdbc/forumDb</res-ref-name>
<res-type>javax.sql.DataSource</res-type>
<res-auth>Container</res-auth>
</resource-ref>

Tomcat 5.5 JNDI Datasoruce 설정


DBCP(Database Connection Pool) 설정

  • 라이브러리 설치
    • Jakarta-Commons DBCP
    • Jakarta-Commons Collections
    • Jakarta-Commons Pool
  • DBCP 는 Jakarta-Commons Database Connection Pool을 사용하는데, 위에 있는 세개의 라이브러리는 $CATALINA_HOME/common/lib/naming-factory-dbcp.jar의 한개의 JAR에 포함되어 있다.
  • 이 하나의 라이브러리가 존재하는지 확인한다.

DB connection pool 누수 방지하기

  • DBCP Datasource 설정에 다음 Resource 속성을 설정한다.
  • 디폴트는 false이다.
removeAbandoned="true"
  • 취소된 Connection에 대한 timeout 시간을 설정한다.
  • 디폴트 timeout 시간읜 300초이다.
removeAbandonedTimeout="60"
  • Connection 리소스를 취소하는 코드의 Stack Trace를 로깅하도록 설정한다.
  • 디폴트는 false이다.
logAbandoned="true"

MySQL DBCP 예제

  • MySQL JDBC 드라이버 설치 
    • MySQL 3.23.47, MySQL 3.23.47 using InnoDB,, MySQL 3.23.58, MySQL 4.0.1alpha
    • Connector/J 3.0.11-stable (the official JDBC Driver)
    • mm.mysql 2.0.14 (an old 3rd party JDBC Driver)
    • 이 중에 하나의 드라이버를 결정하여 $CATALINA_HOME/common/lib에 복사한다.
  • Context 설정 (context.xml)
context.xml
<Context path="/DBTest" docBase="DBTest"
        debug="5" reloadable="true" crossContext="true">

    <!-- maxActive: Maximum number of dB connections in pool. Make sure you
         configure your mysqld max_connections large enough to handle
         all of your db connections. Set to -1 for no limit.
         -->

    <!-- maxIdle: Maximum number of idle dB connections to retain in pool.
         Set to -1 for no limit.  See also the DBCP documentation on this
         and the minEvictableIdleTimeMillis configuration parameter.
         -->

    <!-- maxWait: Maximum time to wait for a dB connection to become available
         in ms, in this example 10 seconds. An Exception is thrown if
         this timeout is exceeded.  Set to -1 to wait indefinitely.
         -->

    <!-- username and password: MySQL dB username and password for dB connections  -->

    <!-- driverClassName: Class name for the old mm.mysql JDBC driver is
         org.gjt.mm.mysql.Driver - we recommend using Connector/J though.
         Class name for the official MySQL Connector/J driver is com.mysql.jdbc.Driver.
         -->

    <!-- url: The JDBC connection url for connecting to your MySQL dB.
         The autoReconnect=true argument to the url makes sure that the
         mm.mysql JDBC Driver will automatically reconnect if mysqld closed the
         connection.  mysqld by default closes idle connections after 8 hours.
         -->

  <Resource name="jdbc/TestDB" auth="Container" type="javax.sql.DataSource"
               maxActive="100" maxIdle="30" maxWait="10000"
               username="javauser" password="javadude" driverClassName="com.mysql.jdbc.Driver"
               url="jdbc:mysql://localhost:3306/javatest?autoReconnect=true"/>

</Context>
  • web.xml 설정
    • WebRoot의 WEB-INF/web.xml 파일에 다음 <resource-ref> 부분을 추가한다.
web.xml
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
    version="2.4">
  <description>MySQL Test App</description>
  <resource-ref>
      <description>DB Connection</description>
      <res-ref-name>jdbc/TestDB</res-ref-name>
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
  </resource-ref>
</web-app>

Oracle 8i, 9i & 10g 예제

  •  Oracle의 경우에는 MySQL 예제에서 최소한의 수정을 통해 설정할 수 있다.
  • 오라클9i까지는 oracle.jdbc.driver.OracleDriver 드라이버를 지원하지만 이후에는 oracle.jdbc.OracleDriver 드라이버를 활용해야 한다.
  • 오라클 드라이버를 포함하는 JAR 파일을 $CATALINA_HOME/common/lib에 설치한다.
  • context.xml 설정
<Resource name="jdbc/myoracle" auth="Container"
              type="javax.sql.DataSource" driverClassName="oracle.jdbc.OracleDriver"
              url="jdbc:oracle:thin:@127.0.0.1:1521:mysid"
              username="scott" password="tiger" maxActive="20" maxIdle="10"
              maxWait="-1"/>
  • web.xml 설정
<resource-ref>
 <description>Oracle Datasource example</description>
 <res-ref-name>jdbc/myoracle</res-ref-name>
 <res-type>javax.sql.DataSource</res-type>
 <res-auth>Container</res-auth>
</resource-ref>
  • 자바 활용 코드 예제
Context initContext = new InitialContext();
Context envContext  = (Context)initContext.lookup("java:/comp/env");
DataSource ds = (DataSource)envContext.lookup("jdbc/myoracle");
Connection conn = ds.getConnection();
//etc.

Tomcat 6.0 JNDI Datasoruce 설정


Tomcat 5.5와 동일한 설정

  • 톰캣 5.5의 설정방법과 동일하게 6.0에서도 설정한다.

참고자료


 
반응형

1. HelloServlet

서블릿의 첫번째 예제는 "안녕! 2008년" 를 출력하는 예제입니다.
앞으로 나오는 모든 예제는 ROOT 웹 애플리케이션에서 실행합니다.
아래 파일을 ROOT 웹 애플리케이션의 WEB-INF/classes 폴더에 저장합니다.

/WEB-INF/classes/HelloServlet.java

package example;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class HelloServlet extends HttpServlet {
  public void doGet( HttpServletRequest req, HttpServletResponse res )
    throws ServletException,IOException {
    res.setContentType( "text/html;charset=euc-kr" );
    PrintWriter out = res.getWriter();
    out.println( "<HTML>" );
    out.println( "<BODY>" );
    out.println( "안녕! 2008년" );
    out.println( "</BODY>" );
    out.println( "</HTML>" );
    out.close();
  }
}

(1) web.xml 파일 편집

web.xml 파일을 열고 web-app 엘리멘트안에 아래를 추가합니다.

/WEB-INF/web.xml

<servlet>
    <servlet-name>HelloServlet</servlet-name>
    <servlet-class>example.HelloServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/servlet/HelloServlet</url-pattern>
</servlet-mapping>

(2) HelloServlet.java 컴파일

WEB-INF/classes 에 위 파일을 편집하여 복사하고 컴파일합니다.
(CLASSPATH에 {TOMCAT_HOME}/common/lib/servlet-api.jar 파일을 추가했다면)
javac -d . HelloServlet.java
(CLASSPATH에 servlet-api.jar 파일을 추가하지 않은 경우)
javac -d . -classpath D:\apps\Tomcat\common\lib\servlet-api.jar HelloServlet.java

(3) http://localhost:8998/servlet/HelloServlet 로 방문하여 테스트

톰캣을 재시작하고
http://localhost:8998/servlet/HelloServlet 을 방문하여 실행이 되는지 확인합니다.

(4) HelloServlet.java 서블릿 소스 설명

① HttpServlet 클래스를 상속받은 서블릿 클래스는 public class 로 선언해야 합니다.
② HTTP 의 GET 방식으로 웹 브라우저가 요청해오면 doGet() 메소드를 작성합니다.
(일반적으로 웹서버의 자원을 요청하는 것은 HTTP 프로토콜의 GET 방식의 요청입니다.)
③ doGet() 메소드는 HttpServletRequest 와 HttpServletResponse 타입의 아규먼트를 가집니다.
이 메소드는 예외가 발생할 수 있으므로 다음 문장으로 예외를 처리합니다.
throws ServletException, IOExcepiton
④ res.setContentType("text/html;charset=euc-kr");
은 웹브라우저에 응답으로 출력될 문서의 MIME 타입을 설정합니다.
;charset=euc-kr을 쓰지 않으면 한글이 깨지는 경우가 발생합니다.
setContentType()메소드는 HttpServletResponse 의 메소드입니다.
⑤ PrintWriter out = res.getWriter();
웹브라우저으로의 문자열에 대한 출력 스트림을 얻습니다.
PrintWriter의 plintln() 메소드안에 문자열을 넣으면 그대로 클라이언트의 웹브라우저에 출력된다고 생각하시면 됩니다.
⑥ http://localhost:8998/servlet/HelloServlet 는 web.xml에서 매핑한 내용대로 접근해야 합니다.
⑦ HelloServlet 서블릿이 작동 원리
클라이언트, 즉 웹브라우저가 서버의 HelloSerlvet 서블릿 자원을 요청합니다.
서블릿엔진인 톰캣은 클라이언트의 요청을 캡슐화한 HttpSerlvetRequest 인터페이스를 구현한 객체와
요청에 대한 응답을 캡슐화한 HttpSerlvetResponse 인터페이스를 구현한 객체를 아규먼트로 받는 protected void service(HttpServletRequest req,HttpServletResponse res) 메소드를 호출합니다.
이 메소드의 내용은 단지 웹브라우저가 헤더로 보낸 HTTP 메소드타입(GET,POST)에 따라 자동적으로 doGet(,) 또는 doPost(,)메소드를 호출하도록 하는 것이 전부입니다.
(따라서 doGet(,)이나 doPost(,)를 생각하지 않으려면 service(,)메소드를 직접 오버라이딩해도 됩니다.)
객체로 된 HelloServlet 서블릿은 클라이언트의 각각의 요청을 개발자가 구현한 doGet(,), doPost(,) 메소드가 병행적으로 처리하므로써 클라이언트의 요청에 응답하게 됩니다.

"안녕! 2008년" 을 출력하는 기본적인 예제를 실행해 봤습니다.
이제는 웹브라우저, 즉 클라이언트가 보내는 정보를 어떻게 서블릿에서 다룰 수 있는지 알아봅니다.
정확하게 클라이언트가 보내는 문자열 정보를 서블릿에서 catch하는 코드를 설명합니다.

2. 웹 브라우저가 서버에 문자열을 전송하는 방법

HTML FORM 태그를 이용하여 웹브라우저가 서버로 문자열 데이터를 전달하는 방법과 서블릿에서 그 정보를 받는 코드조각은 다음과 같습니다.

HTML 서블릿
<input type="text" name="addr"/> req.getParameter( "addr" );
<input type="radio" name="os" value="win98"/>
<input type="radio" name="os" value="win2000"/>
req.getParameter( "os" );
<input type="hidden" name="cur_page" value="1"/> req.getParameter( "cur_page" );
<input type="password" name="passwd"/> req.getParamter( "passwd" );
<input type="checkbox" name="hw" value="intel"/>
<input type="checkbox" name="hw" value="amd"/>
req.getParameterValues( "hw" );
<select name="os" multiple>
  <option value="win">windows</option>
  <option value="linux">linux</option>
  <option value="solaris">solaris</option>
</select>
req.getParameterValues( "os" );
multiple 속성이 없다면
req.getParameter( "os" )도 가능

HttpServletRequest.getParameter()

HttpServletRequest 의 getParameter() 메소드는 웹브라우저에서 사용자가 보내는 데이터를 서블릿에서 받기 위해 사용하는 가장 보편적인 메소드입니다. 라디오 버튼은 name 속성이 같게 두면 같은 name 속성의 라디오 버튼들은 그룹이 됩니다.
그룹화되어 있는 라디오 버튼은 사용자가 한개 항목만을 선택할 수 있습니다.
따라서 HttpServletRequest 의 getParameter() 메소드로 클라이언트가 보낸 데이터를 받습니다.
이 밖에 FORM 태그의 <input type="password" ../> 이나 <input type="hidden" ../> 도 역시 마찬가지로 HttpServletRequest 의 getParameter() 메소드를 이용해서 사용자가 전달한 파라미터 값을 받습니다.
클라이언트, 즉 웹 브라우저에서 서버로는 문자열만을 전송할 수 있습니다.
(물론 서버 사이드에서는 서버자원에서 서버자원으로 문자열 뿐만이 아닌 다른 객체를 만들어 전달 할 수 있습니다.)

HttpServletRequest.getParameterValues()

웹브라우저를 통해 사용자가 다중 선택을 하여 전송하는 데이터의 경우는 HttpServletRequet 의 getParamter() 메소드가 아닌 getParamterValues() 메소드가 쓰입니다.
이 메소드의 리턴값은 사용자가 선택한 값들만으로 구성된 String 배열입니다.

HttpServletRequest.getParamterNames()

만약 사용자가 어떤 파라미터명으로 데이터를 보내는지 알수 없다고 가정을 합니다.(그럴 경우는 거의 없지만) 이때 사용자가 보내는 파라미터명을 알 수 있는 메소드가 HttpServletRequest 의 getParamterNames()입니다.
getParameterNames() 메소드는 리턴값으로 Enumeration 타입을 반환합니다.
(Enumeration 의 hasMoreElements() , nextElement() 2개의 메소드가 기억나지 않는다면 Java Basic 에서 지금 바로 확인하세요.)

실습 1 - 2008년 투자 포트폴리오 설문조사

이 예제는 사이트의 회원으로부터 2008년의 투자 포트폴리오를 설문조사하는 예제입니다.
portfolio2008.html 은 사용자로부터 입력을 받는 페이지입니다.
Portfolio2008Servlet.java 는 portfolio2008.html에서 사용자가 입력한 정보를 데이터베이스에 인서트하는 JDBC 코드를 포함합니다.
portfolio2008.sql 은 관련 테이블 쿼리문입니다.

/example/portfolio2008.html

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
	"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitonal.dtd">	
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR" />
<title>2008년 투자 포트폴리오</title>
</head>
<body>
  <form action="../servlet/Portfolio2008Servlet" method="post">
   기관 : <input type="text" name="company"/><br />
   제안자 : <input type="text" name="name"/><br />
   제안일 : <input type="text" name="signdate"/><br />
   투자액 : <input type="text" name="money"/><br />
   투자성격 : <input type="radio" name="type" value="aggressive" />공격적
   <input type="radio" name="type" value="passive"/>위험회피<br />
   투자형태 :<br />
   <select name="investments" multiple="multiple">
   <option value="koreafund">국내펀드</option>
   <option value="overseasfund">해외펀드</option>
   <option value="direct">직접투자</option>
   <option value="bank">CMA 또는 은행</option>
   </select><br />
   국내펀드 1 <input type="text" name="koreafund1_nm"/>
   투자액 <input type="text" name="koreafund1_money"/>원<br />
   국내펀드 2 <input type="text" name="koreafund2_nm"/>
   투자액 <input type="text" name="koreafund2_money"/>원<br />
   국내펀드 3 <input type="text" name="koreafund3_nm"/>
   투자액 <input type="text" name="koreafund3_money"/>원<br /><br />
   해외펀드 1 <input type="text" name="overseasfund1_nm"/>
   투자액 <input type="text" name="overseasfund1_money"/>원<br />
   해외펀드 2 <input type="text" name="overseasfund2_nm"/>
   투자액 <input type="text" name="overseasfund2_money"/>원<br />
   해외펀드 3 <input type="text" name="overseasfund3_nm"/>
   투자액 <input type="text" name="overseasfund3_money"/>원<br /><br />
   직접투자 <input type="text" name="directp"/>원<br />
   CMA 또는 은행 <input type="text" name="bankp"/>원<br />
   <input type="submit" value="전송"/>
  </form>
</body>
</html>

/WEB-INF/classes/Portfolio2008Servlet.java

package example;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Portfolio2008Servlet extends HttpServlet {
  public void doPost( HttpServletRequest req, HttpServletResponse res )
  throws IOException,ServletException {
    res.setContentType( "text/html;charset=euc-kr" );
    PrintWriter out = res.getWriter();
    req.setCharacterEncoding( "euc-kr" );

    String company = req.getParameter("company");
    String name = req.getParameter("name");
    String signdate = req.getParameter("signdate");
    String money = req.getParameter("money");
    String type = req.getParameter("type");
    String[] investments = req.getParameterValues("investments");

    String koreafund1_nm = req.getParameter("koreafund1_nm");
    String koreafund2_nm = req.getParameter("koreafund2_nm");
    String koreafund3_nm = req.getParameter("koreafund3_nm");

    String koreafund1_money = req.getParameter("koreafund1_money");
    String koreafund2_money = req.getParameter("koreafund2_money");
    String koreafund3_money = req.getParameter("koreafund3_money");

    String overseasfund1_nm = req.getParameter("overseasfund1_nm");
    String overseasfund2_nm = req.getParameter("overseasfund2_nm");
    String overseasfund3_nm = req.getParameter("overseasfund3_nm");

    String overseasfund1_money = req.getParameter("overseasfund1_money");
    String overseasfund2_money = req.getParameter("overseasfund2_money");
    String overseasfund3_money = req.getParameter("overseasfund3_money");

    String directp = req.getParameter("directp");
    String bankp = req.getParameter("bankp");

    out.println( "<html><body>" );
    out.println( "<h1>portfolio.html 에서 보내온 데이터는 다음과 같습니다.</h1>" );
    out.println( "<ul>" );
    out.println( "<li>기관 : " + company + "</li>");
    out.println( "<li>제안자 : " + name + "</li>");
    out.println( "<li>제안일 : " + signdate + "</li>");
    out.println( "<li>투자액 : " + money + "만원 </li>");
    out.println( "<li>투자성격 : " + type + "</li>");
    out.println( "</ul>" );
    out.println("<h3>투자형태</h3>");
    out.println( "<ul>" );
    if( investments != null ) {
      for( int i=0; i < investments.length; i++ ) {
        out.print( "<li>" );
        out.print( investments[i] );
        out.print( "</li>" );	
      }
    }
    out.println( "</ul>" );
    out.println("<h3>국내펀드</h3>");
    out.println( "<ul>" );
    out.println( "<li>" + koreafund1_nm + ": " + koreafund1_money + "만원 </li>");
    out.println( "<li>" + koreafund2_nm + ": " + koreafund2_money + "만원 </li>");
    out.println( "<li>" + koreafund3_nm + ": " + koreafund3_money + "만원 </li>");
    out.println( "</ul>" );

    out.println("<h3>해외펀드</h3>");
    out.println( "<ul>" );
    out.println( "<li>" + overseasfund1_nm + ": " + overseasfund1_money + "만원 </li>");
    out.println( "<li>" + overseasfund2_nm + ": " + overseasfund2_money + "만원 </li>");
    out.println( "<li>" + overseasfund3_nm + ": " + overseasfund3_money + "만원 </li>");
    out.println( "</ul>" );

    out.println("<h3>직접투자</h3>");
    out.println( "<ul>" );
    out.println( "<li>" + directp + "만원</li>");
    out.println( "</ul>" );

    out.println("<h3>CMA 또는 은행</h3>");
    out.println( "<ul>" );
    out.println( "<li>" + bankp + "만원 </li>");
    out.println( "</ul>" );

    String path = req.getContextPath();
    out.println( "<a href=" + path + "/example/portfolio2008.html>뒤로</a>" );
    out.println( "</body></html>" );
  }
}

HttpServletRequest.getContextPath()

HTML문서와 서블릿간의 상대경로에 주의를 기울려야 합니다.
서블릿 코드에서 HTML문서나 JSP문서에 링크를 걸때는 req.getContextPath() 메소드 로 일단 Context base 경로를 구한 후에 이를 이용해 경로를 링크시키면 됩니다.

HttpServletRequest.setCharacterEncoding()

HttpServletRequest 의 setCharacterEncoding("euc-kr")은 웹브라우저, 즉 클라이언트가 보내는 한글 데이터를 한글 인코딩으로 받기 위한 것입니다.
이 부분이 없다면 클라이언트가 보낸 한글 데이터는 깨져 보일 겁니다.

HttpServletResponse.setContentType()

HttpServletResponse 의 setContentType() 메소드는 서블릿이 만드는 HTML문서의 타입과 문자셋을 지정하는 것입니다.
HttpServletRequest 의 setCharacterEncoding() 와 구별하여서 기억해야 합니다.

portfolio2008.sql

create table portfolio (
  portfolio_no   int(11) not null,
  company   varchar(30)	not null,
  name   varchar(10) 	not null,
  signdate   varchar(16)	not null,
  money   int(11) default '0' not null,
  type   varchar(12)	not null,
  koreafund   enum('Y','N') not null default 'Y',
  overseasfund   enum('Y','N') not null default 'Y',
  direct   enum('Y','N') not null default 'Y',
  bank   enum('Y','N') not null default 'Y',
  directp   int(11) default '0' not null,
  bankp   int(11) default '0' not null,
  primary key (portfolio_no),
  index portfolio_no_idx  (portfolio_no)
);

create table koreafundp (
  koerafundp_no   int(11)	not null,
  portfolio_no   int(11)	not null,
  koreafund_nm   varchar(40) not null,
  koreafund_money   int(11) default '0' not null,
  primary key (koerafundp_no)
);

create table overseasfundp (
  overseasfundp_no   int(11)	not null,
  portfolio_no   int(11)	not null,
  overseasfund_nm   varchar(40) not null,
  overseasfund_money   int(11) default '0' not null,
  primary key (overseasfundp_no)
);

위 예제를 실행하려면, portfolio2008.html 의 form action 속성이 ../servlet/Portfolio2008Servlet 로 되어 있으므로 이에 맞게 web.xml 파일을 열어서 web-app 엘리멘트 안에 아래를 추가합니다.

/WEB-INF/web.xml

<servlet>			
    <servlet-name>Portfolio2008Servlet</servlet-name>
    <servlet-class>example.Portfolio2008Servlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Portfolio2008Servlet</servlet-name>
    <url-pattern>/servlet/Portfolio2008Servlet</url-pattern>
</servlet-mapping>

Portfolio2008Servlet.java 를 컴파일하여 Portfolio2008Servlet.class 파일이 /WEB-INF/classes/example 디렉토리에 생기도록 하고 톰캣을 재시동한 후 http://localhost:8998/example/portfolio2008.html 를 방문하여 테스트합니다.

Portfolio2008Servlet.java 에서 JDBC 관련 소스를 입력해 보기 바랍니다.
다음은 간단한 서블릿을 이용한 회원가입 예제입니다.
이 예제를 먼저 실습해 보면 응용이 가능하리라 생각합니다.
sendTF.html에서 이름과 주소를 입력받고 GetTFData.java서블릿은 sendTF.html 에서 사용자가 전달한 값을 JDBC 를 이용해서 memtest 테이블에 인서트를 합니다.
실습에 필요한 테이블은
memtest.sql이고 이 예제는 오라클을 사용해야 합니다.

3. RequestDispatcher 사용 예제

javax.servlet.RequestDispathcer 클래스는 클라이언트의 요청을 서버상의 다른 자원(서블릿,JSP)으로 보내는 작업을 할 때 사용됩니다.
RequestDispathcer 는 include() 와 forward() 2개의 메소드가 있습니다.
include() 메소드는 요청을 다른 자원으로 보냈다가 다른 자원에서 실행이 끝나면 다시 요청을 가져오는 메소드입니다. 결론적으로 말하면 요청을 전달한 자원의 결과를 포함해서 클라이언트에게 보여주게 됩니다.
forward() 메소드는 이름 그대로 클라이언트의 요청을 서버상의 다른 자원에게 넘기는 메소드입니다.
forward() 메소드가 가장 많이 사용됩니다.

/WEB-INF/classes/ControllerServlet.java

package example;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class ControllerServlet extends HttpServlet {

  public void doPost( HttpServletRequest req, HttpServletResponse res ) 
    throws IOException, ServletException {
    String url = req.getParameter( "url" );
    if ( url.equals( "list" ) ) {
      url = "/example/list.jsp"; //구조에 맞게 설정하시요.
    }
    ServletContext sc = getServletContext();
    RequestDispatcher rd= sc.getRequestDispatcher( url );
    rd.forward( req, res );
  }

  public void doGet( HttpServletRequest req, HttpServletResponse res ) 
    throws IOException, ServletException {
    doPost( req, res );
  }
}

/example/list.jsp

<%@ page 
	language="java" 
	contentType="text/html; charset=EUC-KR"
    pageEncoding="EUC-KR"%>
<!DOCTYPE 
	html 
	PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" 
	"http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-KR">
<title>Insert title here</title>
</head>
<body>
	게시판 목록을 보이는 페이지...
</body>
</html>

ControllerServlet 에서 "/example/list.jsp" 에 대한 RequestDispatcher 를 얻은 다음 forward() 메소드를 이용해서 사용자의 요청을 전달하고 있습니다.
ControllerServlet 을 등록하기 위해서 web.xml 파일을 열어 web-app 엘리멘트 안에 아래를 추가합니다.
추가한 다음 톰캣을 재가동하고 http://localhost:8998/bbs/ControllerServlet?url=list 를 방문하여 /example/list.jsp 파일이 보이는지 확인합니다.

/WEB-INF/web.xml

<servlet>
    <servlet-name>Controller</servlet-name>
    <servlet-class>example.ControllerServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>Controller</servlet-name>
    <url-pattern>/ControllerServlet</url-pattern>
</servlet-mapping>

4. ServletConfig 의 getInitParameter() 사용 예제

ServletConfig 의 getInitParameter() 메소드는 해당 서블릿에서만 사용할 수 있는 초기화 파라미터를 web.xml 으로부터 가져올 때 사용하는 메소드입니다.
다음은 예제에 대한 설명입니다.
이전에 사용했던 ConnectionPool 관련 소스는 JDBC 설정내용을 자바의 Properties 파일(oracle.properties, mysql.properties)을 이용했습니다.
예제에서는 이 JDBC 설정 파일을 XML 파일로 변경합니다.
XML 파일을 읽기 위해서 서블릿을 이용합니다.
해당 서블릿의 init 메소드내에 ServletConfig 의 getInitParameter() 메소드를 이용해서 JDBC 설정 XML 파일에 대한 실제 시스템상의 경로를 얻은 다음 SAX 파서를 사용해서 XML 파일의 내용을 읽어 ConnectionPool 관련 객체를 생성한 다음 그 객체를 ServletContext 에 저장합니다.
다음은 위에서 예제로 사용했던 ControllerServlet.java 에 아래와 같이 init 메소드를 추가합니다.
그리고 다음과 같은 import 문장을 추가해야 합니다.

  • import net.java_school.db.dbpool.*;
  • import javax.xml.xpath.*;
  • import org.xml.sax.*;

/WEB-INF/classes/ControllerServlet.java

public void init() throws ServletException {
  
  ServletContext cxt = getServletConfig().getServletContext();
  
  String pool = getInitParameter( "pool" );

  String dbServer = null;
  String port = null;
  String dbName = null;
  String userID = null;
  String passwd = null;
  int maxConn = 0;
  int initConn = 0;
  int maxWait = 0;

  if ( pool != null ) {
    try {
      XPathFactory xpathFactory = XPathFactory.newInstance();
      XPath xpath = xpathFactory.newXPath();

      pool = cxt.getRealPath( pool );

      InputSource xmlSource = new InputSource(pool);

      dbServer = xpath.evaluate("/DBproperties/dbServer",xmlSource);
      port = xpath.evaluate("/DBproperties/port",xmlSource);
      dbName = xpath.evaluate("/DBproperties/dbName",xmlSource);
      userID = xpath.evaluate("/DBproperties/userID",xmlSource);
      passwd = xpath.evaluate("/DBproperties/passwd",xmlSource);
      maxConn = Integer.parseInt(xpath.evaluate("/DBproperties/maxConn",xmlSource));
      initConn = Integer.parseInt(xpath.evaluate("/DBproperties/initConn",xmlSource));
      maxWait = Integer.parseInt(xpath.evaluate("/DBproperties/maxWait",xmlSource));
    } catch ( Exception e ) {}
  }
  ConnectionManager dbmgr = new OracleConnectionManager( dbServer, dbName, port, userID, 
  passwd, maxConn, initConn, maxWait );
  
  // OracleConnectionManager 객체를 ServletContext 에 dbmgr 이란 이름으로 저장
  cxt.setAttribute("dbmgr",dbmgr);
}

web.xml 열고 아래를 추가합니다.

/WEB-INF/web.xml

<servlet>			
  <servlet-name>Controller</servlet-name>
  <servlet-class>example.ControllerServlet</servlet-class>

  <init-param>
    <param-name>pool</param-name>
    <param-value>/WEB-INF/oracle.xml</param-value>
  </init-param>

  <load-on-startup>1</load-on-startup>

</servlet>

oracle.xml 파일을 /WEB-INF 에 아래와 같은 내용으로 만듭니다.

/WEB-INF/oracle.xml

<?xml version="1.0"?>
<DBproperties>
  <dbServer>10.10.10.10</dbServer>
  <port>1521</port>
  <dbName>orcl</dbName>
  <userID>scott</userID>
  <passwd>tiger</passwd>
  <maxConn>20</maxConn>
  <initConn>5</initConn>
  <maxWait>5</maxWait>
</DBproperties>

메인메뉴 JDBC 에서 실습한 ConnectionPool 관련 소스 중 ConnectionManager.java 와 OracleConnectionManager.java 를 각각 다음과 같이 고칩니다.

ConnectionManager.java

package net.java_school.db.dbpool;

import java.sql.*;

public abstract class ConnectionManager {

  protected DBConnectionPoolManager connMgr = null;
  protected String poolName, dbServer, dbName, port, userID, passwd;
  int maxConn,initConn, maxWait;

  public ConnectionManager() {}
  
  public ConnectionManager( String pool, String dbServer, String dbName, String port, 
    String userID, String passwd, int maxConn, int initConn, int maxWait ) {
    poolName = pool;
    this.dbServer = dbServer;
    this.dbName = dbName;
    this.port = port;
    this.userID = userID;
    this.passwd = passwd;
    this.maxConn = maxConn;
    this.initConn = initConn;
    this.maxConn = maxConn;
  }

  public Connection getConnection() {
    return ( connMgr.getConnection( poolName ) );
  }

  public void freeConnection( Connection conn ) {
    connMgr.freeConnection( poolName, conn );
  }

  public int getDriverNumber() {
    return connMgr.getDriverNumber();
  }
}

OracleConnectionManager.java

package net.java_school.db.dbpool;

public class OracleConnectionManager extends ConnectionManager {
  
  public OracleConnectionManager() {}

  public OracleConnectionManager( String dbServer, String dbName, String port, 
    String userID, String passwd, int maxConn, int initConn, int maxWait ) {
    
    super( "oracle", dbServer, dbName, port, userID, passwd, 
	maxConn, initConn, maxWait );
    
    String JDBCDriver = "oracle.jdbc.driver.OracleDriver";

    // 오라클용 JDBC thin driver
    String JDBCDriverType = "jdbc:oracle:thin";

    String url = JDBCDriverType + ":@" + dbServer + ":" + port + ":" + dbName;

    connMgr = DBConnectionPoolManager.getInstance();
    connMgr.init( poolName, JDBCDriver, url, userID, passwd, 
	maxConn, initConn, maxWait );
  }
}

위에서 간단하게 테스트했던 /exmaple/list.jsp 파일을 아래와 같이 수정합니다.

/example/list.jsp

<%@ page contentType="text/html;charset=euc-kr" %>
<%@ page import="java.sql.*, net.java_school.db.dbpool.*" %>
<jsp:useBean id="dbmgr" class="net.java_school.db.dbpool.OracleConnectionManager"
scope="application" />
<%
  Connection conn = null;
  Statement stmt = null;
  ResultSet rs = null;
  String query = "select * from emp";
  try {

    //데이터베이스의 연결을 설정합니다.커넥션풀 이용
    conn = dbmgr.getConnection();

    //Statement를 가져온다.
    stmt = conn.createStatement();

    //SQL문을 실행합니다.
    rs = stmt.executeQuery( query );

    while ( rs.next() ) {
      String empno = rs.getString(1);
      String ename = rs.getString(2);
      String job = rs.getString(3);
      String mgr = rs.getString(4);
      String hiredate = rs.getString(5);
      String sal = rs.getString(6);
      String comm = rs.getString(7);
      String depno = rs.getString(8);

      //결과를 출력합니다.
      out.println( empno + " : " + ename + " : " + job + " : " + mgr + " : " + hiredate + 
      " : " + sal + " : " + comm + " : " + depno + "<br>" );
    }
  } catch ( SQLException e ) {
    out.println( "SQLException: " + e.getMessage() );
  } finally {
    try {
      if ( rs != null ) rs.close();
      if ( stmt != null) stmt.close();
      dbmgr.freeConnection( conn );
    } catch ( SQLException e ){}
  }
%>

톰캣을 재시작하고 http://localhost:8998/bbs/ControllerServlet?url=list 로 방문해서 테스트합니다.

5. 파일 업로드을 위한 MultipartRequest 팩키지

MultipartRequest 팩키지는 파일 업로드에 널리 이용되고 있는 팩키지입니다.
http://www.servlets.com/cos/index.html 에서 가장 최신 버전인 cos-05Nov2002.zip 를 다운로드 하여 압축을 풉니다.
lib 디렉토리에 있는 cos.jar 파일을 /WEB-INF/lib 디렉토리에 복사합니다.
MultipartRequest 클래스의 생성자는 아래 링크에서 확인할 수 있듯이 8가지나 됩니다.
http://www.servlets.com/cos/javadoc/com/oreilly/servlet/MultipartRequest.html
그 중 아래의 생성자는 한글 인코딩 문제를 해결할 수 있고, 또한 업로드되는 파일이 기존의 파일명과 중복될 때 파일명을 변경해서 업로드할 수 있습니다.
MultipartRequest ( HttpServletRequest req, String dir, int max, String encoding, FileRenamePolicy policy )

MultipartRequest 메소드

<input type="file" name="photo"/> 태그를 이용해서 logo.gif 를 업로드했다고 가정하에 설명합니다.

메소드 설명
getContentType( "photo" ); 업로드된 파일의 MIME 타입 리턴, 리턴값 "image/gif"
getFile( "photo" ); 업로드되어 서버에 저장된 파일의 File 객체 리턴
getFileNames(); 폼 요소 중 input 태그 속성이 file 로 된 파라미터의 이름을 Enumeration 타입으로 리턴
getFilesystemName( "photo" ); 업로드되어 서버 파일시스템에 존재하는 실제 파일명을 리턴
getOriginalFileName( "photo" ); 원래의 파일명을 리턴
HttpServletRequest 와 같은 인터페이스를 제공하기 위한 메소드
getParameter(String name); name 에 해당하는 파라미터의 값을 String 타입으로 리턴
getParameterNames(); 모든 파라미터의 이름을 String 으로 구성된 Enumeration 타입으로 리턴
getParameterValues(String name); name 에 해당하는 파라미터의 값들을 String[] 타입으로 리턴

MultipartRequest 를 이용한 파일 업로드 예제

/example/upload.html

<!--
파일명: upload.html
-->

<html>
<head>
    <meta http-equiv="content-type" content="text/html; charset=euc-kr">
</head>
<body>
<h2>MultipartRequest 를 이용한 파일 업로드 테스트</h2><
<form action="../servlet/UploadTest" method="post" enctype="multipart/form-data">
    이름 : <input type="text" name="name"/><br>
    파일1 : <input type="file" name="file1"/><br>
    파일2 : <input type="file" name="file2"/><br>
    <input type="submit" value="전송"/>
</form>
</body></html>

/WEB-INF/classes/UploadTest.java

package example;

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
import com.oreilly.servlet.MultipartRequest;
import com.oreilly.servlet.multipart.DefaultFileRenamePolicy;

public class UploadTest extends HttpServlet {
  public void doPost( HttpServletRequest req, HttpServletResponse res ) 
  throws IOException, ServletException {
    
    res.setContentType( "text/html;charset=euc-kr" );
    PrintWriter out = res.getWriter();
    //req.setCharacterEncoding( "euc-kr" );
    ServletContext cxt = getServletContext();
    String dir = cxt.getRealPath( "upload" );

    try {
      MultipartRequest multi = new MultipartRequest( req, dir, 
        5*1024*1024, "euc-kr", new DefaultFileRenamePolicy());

      out.println( "<html>" );
      out.println( "<body>" );
      out.println( "<h1>사용자가 전달한 파라미터들</h1>" );
      out.println( "<ol>" );
      Enumeration params = multi.getParameterNames();

      while( params.hasMoreElements() ) {
        String name = (String)params.nextElement();
        String value = multi.getParameter( name );
        out.println( "<li>" + name + "=" + value + "</li>" );
      }
      out.println( "</ol>" );

      out.println( "<h1>업로드된 파일</h1>" );

      Enumeration files = multi.getFileNames();

      while( files.hasMoreElements() ) {
        out.println( "<ul>" );  
        String name = (String)files.nextElement();
        String filename = multi.getFilesystemName( name );
        String orginalname =multi.getOriginalFileName( name );
        String type = multi.getContentType( name );
        File f = multi.getFile( name );
        out.println( "<li>파라미터 이름 : "  + name + "</li>" );
        out.println( "<li>파일 이름 : " + filename + "</li>" );
        out.println( "<li>원래 파일 이름 : " + orginalname + "</li>" );
        out.println( "<li>파일 타입 : " + type + "</li>" );

        if( f != null ) {
        out.println( "<li>크기: " + f.length() + "</li>" );
        }

        out.println( "</ul>" );
      }
    } catch( Exception e ) {
      out.println( "<ul>" );
      e.printStackTrace( out );
      out.println( "</ul>" );
    }
    out.println( "</body></html>" );
  }
}

테스트를 위해서는

  1. 웹 애플리케이션의 루트에 upload 라는 폴더를 만듭니다
  2. 컴파일을 위해 cos.jar 파일을 CLASSPATH 에 추가하고 컴파일합니다
  3. cos.jar 파일을 /WEB-INF/lib 에 복사합니다.
  4. /WEB-INF/web.xml 파일을 열어 UploadTest 서블릿을 등록합니다.(아래참조)
  5. 톰캣을 재가동하고 http://localhost:8998/bbs/example/upload.html 를 방문하여 테스트 합니다.
  6. 중복된 파일을 업로드 테스트하고 upload 폴더에 파일명을 확인합니다.

/WEB-INF/web.xml

<servlet>
    <servlet-name>UploadTest</servlet-name>
    <servlet-class>example.UploadTest</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>UploadTest</servlet-name>
    <url-pattern>/servlet/UploadTest</url-pattern>
</servlet-mapping>

java.lang.NoClassDefFoundError: javax/activation/DataSource

컴파일이 문제가 없었고 톰캣을 재가동한 후에 URL로 방문하여 테스트 하니 위와 같은 에러가 나왔다면 JavaBeans(TM) Activation Framework 라는 팩키지가 필요합니다.
http://java.sun.com 에 방문해서 jaf라는 이름으로 검색해서 팩키지를 다운로드를 한 다음 /WEB-INF/lib/ 폴더에 복사합니다.

jaf 다운로드
a. http://java.sun.com 에 방문해서 오른쪽 메뉴의 Popular Downloads: See All를 선택합니다.

b. 다음 페이지에서 JAVA SE 셀렉트박스에서 JavaBeans Activation Framework(JAF) 를 선택하여 다운로드 합니다.

c. 압축을 풀고 activation.jar 를 /WEB-INF/lib/ 디렉토리에 저장합니다.

쿠키

HTTP 전송방식의 특징상 각각의 웹 브라우저가 서버와 통신에서 세션을 유지하지 못하는 것을 보완하기 위한 기술입니다.
서버가 쿠키를 전송하면 웹 브라우저는 그 다음 요청마다 쿠키 값을 서버로 전달하여 사용자 정보를 유지할 수 있게 합니다.
서버 -> 웹 브라우저 (쿠키를 굽는다고 표현되는데 아래와 같은 정보가 클라이언트의 웹 브라우저를 통해서 파일로 저장됩니다.)
이때 전달되는 정보 형태는 아래와 같습니다.

Set-Cookie : name = value ; expires = date ; path = path ; domain = domain ; secure

웹 브라우저 -> 서버 (쿠키가 웹브라우저에 셋팅되면, 웹브라우저는 쿠기를 전달해준 서버로 요청시마다 아래와 같은 문자열을 서버로 보냅니다.)

Cookie ; name = value1 ; name2 = value2 ;

쿠키 이름과 값에는 []()="/?@:; 와 같은 문자는 올 수 없습니다.

(1) 쿠키 설정 절차

① Cookie 객체를 만든다. Cookie(String name, String value)
② 다음 메소드를 이용해 쿠키에 속성을 부여한다.

메소드 설명
setValue(String value) 생성된 쿠키의 값을 재설정할 때 사용한다.
setDomain(String pattern) 쿠키는 기본적으로 쿠키를 생성한 서버에만 전송된다.
같은 도메인을 사용하는 서버에 대해서 같은 쿠키를 보내기 위해서 setDomain()을 사용한다.
주의할 점은 쿠키를 생성한 서버와 관련이 없는 도메인을 setDomain()에 값으로 설정하면 쿠키가 구워지지 않는다는 것이다.
setMaxAge(int expiry) 쿠키의 유효기간을 초단위로 설정한다.
음수 입력시에는 브라우저가 닫으면 쿠키가 삭제된다.
setPath(String uri) 쿠키가 적용될 경로 정보를 설정한다.
경로가 설정되면 해당되는 경로로 방문하는 경우에만 웹브라우저가 쿠키를 웹서버에 전송한다.
setSecure(boolean flag) flag가 true이면 보안채널을 사용하는 서버의 경우에 한해 쿠키를 전송한다.
③ 웹브라우저에 생성된 쿠키를 전송 : res.addCookie(cookie);

(2) 구워진 쿠키 이용

① 서블릿에서 쿠키 이용

Cookie[] cookie = req.getCookies();

HttpServletRequest의 getCookies() 메소드를 사용해서 쿠키배열을 얻습니다.
만약 구워진 쿠키가 없다면 getCookies() 메소드는 null을 리턴합니다.
이제 쿠키 객체를 접근할 수 있게 되었습니다.
다음 메소드를 이용하면 쿠키에 대한 정보를 얻을 수 있습니다.
이중 getName()과 getValue()가 주로 쓰입니다.

Cookie 메소드 설명
getName() 쿠키의 이름을 구한다.
getValue() 쿠키의 값을 구한다.
getDomain() 쿠키의 도메인을 구한다.
getMaxAge() 쿠키의 유효시간을 구한다.

String id = null;
Cookie[] cookies = request.getCookies();
if ( cookies != null ) {
  for ( int i=0; i < cookies.length; i++ ) {
    String name = cookies[i].getName();
    if ( name.equals( "id" ) ) {
      id = cookies[i].getValue();
    }
  }
}

② 서블릿에서 쿠키 삭제
아래와 같이 삭제하고자 하는 쿠키와 같은 이름의 쿠키를 생성하고 setMaxAge(0) 을 호출합니다.

Cookie cookie = new Cookie( "id", "" );
cookie.setMaxAge( 0 );
res.addCookie( cookie );

(3) 쿠키 실습

다음은 간단한 쿠키예제입니다.
실습 후 cookieList.jsp 파일을 cookie 폴더 외에 다른 폴더에 복사하고 테스트해 보세요.

/cookie/setCookie.jsp

<%@ page contentType="text/html; charset=euc-kr" %>
<%
  String cookieName = "id";
  String cookieValue = "xman";

  Cookie cookie = new Cookie(cookieName,cookieValue);
  String path = request.getContextPath();
  path = path + "/cookie";
  cookie.setPath( path );
  response.addCookie(cookie);
%>
<a href="cookieList.jsp">쿠키 목록보기</a>			

/cookie/cookieList.jsp

<%@ page contentType="text/html; charset=euc-kr" %>
<html><body>
저장되어 있는 쿠키 목록입니다.<br />
<%
  Cookie[] cookies = request.getCookies();
  if ( cookies != null ) {
    for ( int i=0 ;i < cookies.length; i++ ) {
      out.println(cookies[i].getName());
      out.println("<br />");
      out.println(cookies[i].getValue());
      out.println("<br />");
    }
  }
%>
<a href="removeCookie.jsp">쿠키 제거하기</a> <a href="setCookie.jsp">쿠키 굽기</a>
</body></html>

/cookie/removeCookie.jsp

<%@ page contentType="text/html; charset=euc-kr" %>
<%
  Cookie cookie = new Cookie( "id", "" );
  String path = request.getContextPath();
  path = path + "/cookie";
  cookie.setPath( path );
  cookie.setMaxAge( 0 );
  response.addCookie( cookie );
%>
<a href="cookieList.jsp">쿠키 목록보기</a>

세션

세션은 쿠키 기반 기술로 쿠키의 보안상 약점을 극복하기 위한 기술입니다.
쿠키와 다른 점(즉, 보안상 개선된 점) : 웹브라우저는 서버가 정해준 세션ID 만을 쿠키값으로 저장합니다.
세션이 생성되면(즉, 세션ID 쿠키가 구워지면) 세션ID 쿠키만을 서버로 전송하게 되고,
서버에서는 세션ID로 해당 HttpSession 객체를 서블릿/JSP 컨테이너가 연결시켜 줍니다.
HttpSession 의 메소드
setAttribute( String name , Object value )
getAttribute( String name )
removeAttribute( String name )
invalidate();
사용법
세션 생성 : HttpSession session = req.getSession( true ); //세션이 없으면 생성합니다.
HttpSession session = req.getSession( false ); // 세션이 없다면 null 을 리턴
세션에 정보 저장 : session.setAttribue( "data", value ); //data 이름으로 value 객체 저장

File 클래스

자바에서는 파일을 표현하기 위해 File 클래스를 사용합니다.
디렉토리도 File 클래스로 표현됩니다.
주의할 것은 File 클래스는 파일을 읽거나 쓰는 메소드는 가지고 있지 않습니다.
파일을 읽거나 쓰기 위해서는 입출력 클래스를 사용합니다.
File 클래스로 할 수 있는 작업
① 디렉토리 내용을 알아본다.
② 파일의 속성을 알아보거나 설정합니다.
③ 파일의 이름을 변경하거나 삭제합니다.
File 클래스 생성
객체 생성 : File dir = new File( path );
주의) 여기서 path 에 해당하는 파일이나 디렉토리는 시스템의 풀패스가 되어야 한다는 것입니다.
File 클래스 중요 메소드 소개와 사용법
isDirectory() : dir.isDirectory(); // dir 이 디렉토리이면 true 리턴
isFile() : dir.isFile(); // dir 이 파일이면 true 리턴
list() : dir.list() : // dir 이 디렉토리일 때 디렉토리에 있는 파일명이 String[] 값으로 리턴
listFiles() : dir.listFiles(); // 디렉토리에 있는 파일의 파일 객체 배열 리턴
mkdir() : dir.mkdir(); // File 객체의 이름을 가진 디렉토리를 만든다
getName() : 파일명을 리턴
getPath() : 경로를 리턴
delete() : 파일을 지운다.
exists() : 파일이 존재하는지 여부를 알려준다.

이것으로 아주 간단하게 서블릿 문법을 살펴보았습니다.
서블릿에 대한 이해가 있어야 JSP 할때에 이해가 쉽습니다.
다음은 JSP 문법으로 넘어갑니다.

반응형

개요

  이 글의 목적

웹 사이트를 만드는데 있어서 기존의 개발 방법과 자바 환경에서의 개발 방법은 그 개념이 여러가지 면에서 많이 다르다. 물론, 자바를 이용해서도 종래의 개념처럼 코딩할 수 있지만, 이는 자바가 제공하는 많은 개념을 제대로 활용하지 못한 채 서버급 컴퓨터로 워드 프로세싱 정도의 작업만 하는 것과 다를 바 없다. 그러나, 아직은 자바 환경에서 서블릿과 JSP로 웹 사이트를 구축하는 노우하우가 많이 알려지지 않았고, 환경을 구축하는 방법도 쉬운 것만은 아니다. 그러나, 이미 많은 개발자들이 인식하고 있는 것처럼 앞으로 자바 환경을 중심으로 웹 사이트 개발의 주류가 형성될 것이고, 앞서가는 개발자라면 개념적으로 훌륭한 도구를 내 것으로 활용할 수 있어야 하는 만큼, 자바의 진보적인 개념을 제대로 이해하여 실제 개발할 수 있는 능력을 배양할 필요가 있다.

이 글은 필자가 작성한 실제 프로그램 코드 예를 통해 JSP로 웹 문서를 제작하는 구체적인 방법을 제시한다. 특히, beans의 사용법과 JSP의 include, forward 기능을 상세히 살펴볼 수 있으며, 계승과 예외 처리 등 객체지향 기법들이 JSP를 통한 웹 문서 개발에 어떤 식으로 적용되는지도 보여준다. JDBC를 활용하여 데이터베이스를 조작하는 방법과 데이터베이스 접속을 pool로 관리하는 방법도 다루고 있으므로, 가히 JSP를 통한 대부분의 테크닉을 집약했다고 할 수 있다. 회원 관리를 위한 로그인 시스템은 코드의 양은 그리 많지 않은데도 JSP의 독특한 기법들을 모두 활용할 수 있는 좋은 개발 예로서, 독자 여러분에게 많은 도움이 될 것이다.

  로그인 관리 프로그램이란?

근래에는 많은 웹 사이트가 회원제로 운영된다. 이를 위해 웹 사이트가 개발해야 할 일은 다음의 것들이 있다.

  • 회원의 정보를 저장하는 데이터베이스 구축

  • 아이디와 패스워드를 입력받고 이를 인증해 주는 과정

  • 클라이언트로부터 웹 문서 요청 시, 이 요청이 인증을 통과한 요청인지 아닌지를 알아내는 기능과 인증을 통과했다면 어떤 회원의 요청인지 구별하는 기능

  • 인증을 통과한 클라이언트로부터 일정한 시간동안 접속이 없다면 로그아웃하도록 하는 기능

  • 사이트 내에서 회원이 취하는 행위들에 대해 기록하는 기능

위와 같은 기능을 모두 포괄하되, 사용하기 쉽고, 웹 서버에 큰 부담을 주어서는 안 된다. 필자가 여기서 제시하는 프로그램은 위의 기능을 모두 충족하면서도 사용하기 간편하고, 웹 서버에도 거의 부담을 주지 않는다. 향후 J2EE 기반에서 EJB를 사용하면 더욱 견고하게 구현할 수 있지만, JSP만을 이용해서도 부족하지 않다. 기존의 웹 개발 방법으로 회원 관리 시스템을 구현해 본 개발자라면 자바의 강점을 충분히 느낄 수 있을 것이다.

준비 작업

  소프트웨어 설치

    데이터베이스 설치

데이터베이스를 설치한다. 필자는 postgreSQL에서 테스트하였으나, JDBC를 지원하는 데이터베이스라면 어떤 것도 괜찮다.

    JDK 설치

JDK는 1.2.2를 권장한다. Sun의 자바 홈페이지를 방문하면 구할 수 있다. 압축을 풀고, $JAVA_HOME/bin 디렉토리를 PATH에 추가하여 설치를 완료한다.

    Tomcat 설치

Tomcat의 최신 버전을 다운 받아 설치한다. 설치 방법은 WebDox의 Apache에서 Tomcat 사용하기에 자세히 설명되어 있으므로 참고한다. 주의할 점은 JDBC 드라이버가 $TOMCAT_HOME/bin/tomcat.sh 파일의 CLASSPATH에 잡혀야 하는 것이다.

  사전 학습

이 글은 JSP, Beans, JDBC의 실전 응용 예이므로 각각에 대한 기본적인 이해가 필요하다. WebDox에 관련 문서들이 있다.

    JSP

JSP 개념과 기본적인 사용 문법에 대한 이해를 위해서 WebDox의 JSP 맛보기를 읽어 본다.

    Beans

Beans가 없는 JSP는 생각할 수 없다. Beans를 사용하지 않는 JSP는 속빈 강정이고, 대포없는 전차이다. Beans에 대한 개념을 이해하기 위해 JSP에서 Beans 사용하기를 읽고 숙지한다.

    JDBC

본 예제는 사용자 정보와 접속 사용자의 아이디를 데이터베이스로 관리하므로 JDBC를 이용하여 데이터베이스를 조작하는 방법을 이해하고 있어야 한다. 역시 WebDox의 JDBC를 익히자를 읽어 본다.

프로그램 설치

  데이터베이스 설정 하기

테이블이 두 개 필요하다. 하나는 아이디와 비밀번호를 저장하고 있는 테이블이고 또 하나는 현재 접속한 사용자의 아이디를 저장하는 테이블이다. 전자는 id, password 이름을 갖는 필드가 존재하면 되고, 후자는 id 이름을 갖는 필드 하나만 갖는 테이블이여야 한다. 테이블 이름은 적절하게 만들도록 한다.

  WebDox's User Login Management(WULM: 움이라 읽는다)의 JSP 부분 다운 받기

wulmJsp.tar.gz를 다운받는다. 웹 서버의 루트 디렉토리에서 앞축을 푼다. userLog 디렉토리가 생기고 파일들이 그 안에 생길 것이다.

  WULM 클래스 부분 다운 받기

wulmClass.tar.gz를 다운 받는다. WEB-INF/classes 디렉토리 밑에서 압축을 푼다. userLog 디렉토리가 생기고 이 디렉토리 안에 WULM이 사용하는 클래스들이 있다. WEB-INF 디렉토리에는 CharacterSet.java가 생성된다.

  데이터베이스 이름, 테이블 이름 설정하기

WEB-INF/classes/userLog/Log.java 파일을 Emacs 등의 편집기로 열어 주석이 설명하는데로 데이터베이스 URL, 데이터베이스 사용자 이름, 데이터베이스 사용자 비밀번호와 앞에서 설정한 두 개 테이블 이름을 지정한다.

  컴파일하기

자바 소스를 모두 컴파일한다. 에러 없이 java 소스가 컴파일되어 class 파일들이 생기면 된다.

  테스트

loginTest.jsp를 브라우저를 통해 접속한다.

프로그램 설명

  WULM의 기능 개요

클라이언트가 웹 사이트에 방문하여 로그인하고 작업을 수행하다가 로그아웃하거나 더 이상의 문서 요청이 없게 되기까지의 과정을 생각해 보자.

회원제로 운영되는 사이트도 대개의 경우 비회원도 어느 정도는 사이트의 내용을 볼 수 있고 서비스를 받도록 하고 있다. 완전히 회원제로 운영되어 회원이 아닌 사람은 사이트의 어떤 내용도 들여다 볼 수 없다면, 회원으로 유도하기 쉽지 않기 때문이다. 따라서, 회원이 아이디와 비밀번호를 입력하는 로그인 화면은 꼭 로그인 화면으로의 링크를 누르지 않아도 회원만이 가능한 서비스인 경우는 자동적으로 로그인 과정을 거치도록 해 주는 편이 훨씬 지능적이다.

클라이언트가 비회원도 볼 수 있는 문서들을 보다가 회원만 가능한 문서로 접근 요청을 하게 되면, 우리의 회원 로그인 관리 시스템은 자동적으로 로그인 화면을 보여 주게 되고, 여기서 클라이언트는 아이디와 비밀번호를 입력해야 한다. 이 때, 당연히 회원의 아이디와 비밀번호가 유효한지를 확인해야 하고, 아이디와 비밀번호가 맞다면 이미 로그인했는지의 여부를 살펴야 한다.

복수 로그인을 가능하게 할 것인가는 사이트 운영 정책에 따라 결정되므로, 회원 로그인 관리 시스템은 복수 로그인이 되도록 설정했다면 복수 로그인을 지원해야 하고 그렇지 않다면 복수 로그인을 금하는 기능을 포함해야 할 것이다.

어쨌거나, 정상적으로 아이디와 비밀번호를 입력한 후에는 로그인 직전에 요청했던 문서로 자동적으로 돌아가 주어야 한다. 예를 들어, 특정 품목에 대해서는 회원만이 구입이 가능한 쇼핑몰이라면, 그 특정 품목을 눌렀을 때 회원 만이 가능한 서비스라는 메시지와 함께 로그인 화면을 사용자에게 보여 주고, 사용자가 입력한 아이디와 비밀번호가 유효하다면, 이제는 그 특정 품목을 살 수 있으므로 특정 품목 구입 화면으로 자동적으로 리턴해야 한다.

로그인을 거친 사용자(클라이언트)는 사이트에서 본인이 원하는 일을 하고 나서, 로그아웃을 할 것이다. 웹 사이트는 로그아웃을 명시적으로 할 수 있도록 로그아웃 메뉴를 갖추어야할 뿐만 아니라, 브라우저를 그냥 끄거나, 다른 사이트로 가 버리는 경우도 고려해야 한다. 이 경우들은 웹 사이트 입장에서 보면 더 이상의 문서 요청이 없는 것이고, 지정된 일정 시간 동안 추가적으로 웹 사이트에 접속하지 않는다면 자동으로 로그아웃이 되도록 해야 한다.

WULM은 위에서 나열한 플로우 상에 필요한 기능들을 모두 갖추고 있다. 이제 이 기능을 구현하기 위해 어떻게 코딩을 하였는지 살펴보자.

  WULM의 JSP 파일들

이 절에서는 WULM이 사용하는 jsp 파일들을 살펴보고 WULM의 전체적인 플로우를 이해해보자. Jsp 파일들은 여러가지 beans를 사용하는데 우선은 jsp 파일만을 통해 프로세스를 이해하고 다음 절에서 beans를 살펴본다.

    Include 부분

회원 관리가 필요한 문서, 즉, 회원만이 사용할 수 있는 페이지는 회원인지 아닌지를 체크하고 회원이 아니라면 로그인 과정을 거칠 수 있도록 해야 한다. 말하자면, 그 페이지에는 WULM을 포함해야 한다. WULM을 포함하는 방법은 간단히, 다음처럼 include를 사용하면 된다.



1   <%@ page import="java.util.Enumeration" 
2                         contentType="text/html; charset=EUC_KR" %>
3
4   <% String loginUrl = "/hello.jsp"; %>
5   <%@ include file="/userLog/log.jsp" %>
6
7   <html>
8
9   .....
10
11  </html>

여기서 <jsp:include>를 사용하지 않은 것에 유의하자. 인클루드 되는 log.jps 파일은 <jsp:forward> 기능을 사용하는데 필자가 테스트한 바로는 <jsp:include> 태그로 인클루드한 페이지가 <jsp:forward> 태그를 사용하면 에러가 났다. (<jsp:forward>로 forwading한 페이지는 <jsp:forward> 태그로 다시 다른 페이지로 forwarding할 수는 있다.)

첫번째 행에서 import 부분을 유의하자. 아래에서 인클루드하는 /login/log.jsp에서 Enumeration 클래스를 사용하므로 이 import 부분에 이 클래스를 명시해야 한다.

네번째 행의 loginUrl 인스턴스는 로그인 처리가 끝난 후 보여줄 페이지의 파일 이름이다. 주의할 점은, login 디렉토리를 기준으로한 상대 경로이거나, 웹 서버의 Document Root로부터의 절대경로여야 한다는 것이다. 이것은 로그인 과정을 처리하는 /login/loginProcess.jsp 페이지에서 loginUrl 값으로 forwarding하기 때문이다.

    /login/log.jsp

이제, log.jsp 파일이 무슨 일을 하는지 살펴보자. 소스는 다음과 같다.



1    <jsp:useBean id="myLogin" class="userLog.MyLogin" scope="session">
2        <jsp:forward page="/userLog/login.jsp">
3     	     <jsp:param name="loginUrl" value="<%= loginUrl %>"/>
4        </jsp:forward>
5    </jsp:useBean>
6
7    <%
8        if (!myLogin.isLoginStatus()) {
9    %>
10       <jsp:forward page="/userLog/login.jsp">
11           <jsp:param name="loginUrl" value="<%= loginUrl %>"/>
12       </jsp:forward>
13   <%
14       }
15   %>

1번부터 5번 행까지는 MyLogin 타입의 bean을 생성/선언한다. MyLogin은 개별 로그인 마다 생기는 session bean이다. 2번에서 4번 행까지는 이 MyLogin bean이 처음으로 생성될 때 수행되는 부분으로서, MyLogin bean이 아직 생성이 안 되었다면 로그인 절차를 밟지 않았음을 의미하므로 로그인 절차를 수행하도록 아이디와 비밀번호를 입력받는 /login/login.jsp 파일로 forwading한다.

7번부터 15번 까지는 이미 MyLogin bean이 생성되었다고 해도 사용자가 로그아웃 절차를 통해 로그아웃 한 경우를 처리해 준다. 이 경우는 이미 로그아웃을 했으므로 다시 로그인 절차를 밟아야 하기 때문에, MyLogin bean이 처음 생성되는 경우와 마찬가지로 login.jsp 파일로 forwarding한다.

3번, 11번 행은 로그인 절차를 밟은 후 보여주는 페이지를 bean의 속성으로 설정한다. log.jsp 파일은 <jsp:include> 태그를 이용하여 동적으로 포함되는 것이 아니라, <%@ include> 태그를 이용하여 정적으로 인클루드되므로, log.jsp 파일을 인클루드한 페이지에서 정의한 loginUrl 인스턴스를 바로 사용할 수 있다. 뿐만아니라, 상단에 <%@ page> 태그도 사용하지 않았다.

MyLogin bean에 대한 자세한 설명은 다음 절에서 설명한다.

    /login/login.jsp

log.jsp 파일은 로그인이 이미 되어 있다면 아무 일도 하지 않지만, 로그인이 아직 안 되어서 MyLogin bean이 생성이 안 되어 있다거나, 생성이 되어 있지만, 로그아웃 절차를 밟은 경우에는 login.jsp 파일로 forwading하는 역할을 수행한다. 이제, login.jsp 파일이 어떤 일을 하는지 살펴보자.



1    <%@ page import="CharacterSet,java.util.Enumeration" contentType="text/html; charset=EUC_KR" %>
2
3    <html>
4    <head>
5      <title>로그인</title>
6    </head>
7
8    <body>
9
10   <script language="javascript">
11   function checkLoginForm() {
12       if (document.loginForm.loginId.value == "" || document.loginForm.loginPassword.value == "") {
13           alert("아이디와 비밀번호를 입력하셔야 합니다.");
14           return false;
15       }
16       return true;	
17   }
18   </script>
19
20   <jsp:useBean id="myLogin" class="userLog.MyLogin" scope="session"/>
21
22   <form name="loginForm" method="post" action="/userLog/loginProcess.jsp" onSubmit="return checkLoginForm();">
23
24   <%
25       Enumeration p_pr = request.getParameterNames();
26       String p_name = "", p_value = "";
27       while (p_pr.hasMoreElements()) {
28           p_name = (String) p_pr.nextElement();
29           p_value = request.getParameter(p_name);
30   %>
31       <input type="hidden" name="<%= p_name %>" value="<%= p_value %>">
32   <%
33      }
34   %>
35
36   &nbsp;&nbsp;&nbsp;아이디 : <input type="text" name="loginId" maxlength="16" size="20"><br>
37   &nbsp;&nbsp;&nbsp;비밀번호 : <input type="password" name="loginPassword" maxlength="16" size="20"><br>
38   &nbsp;&nbsp;&nbsp;<input type="submit" value="로그인">
39
40   </form>
41   <br><br>
42
43   </body>
44   </html>

login.jsp는 사용자로부터 아이디와 비밀번호를 입력받는 페이지이다. 22번부터 40번 행까지는 HTML의 FORM을 사용해 사용자로부터 아이디와 비밀번호를 입력받도록 하고 이 내용을 loginProcess.jsp로 넘겨준다.

24번부터 34번 행까지는 log.jsp를 인클루드한 문서에 GET이나 POST로 전달된 파라미터 이름과 값들을 읽어서 hidden 타입으로 다시 설정하는 부분이다. 이를 통해 초기에 log.jsp를 인클루드한 페이지가 받은 POST나 GET 등을 통해 받은 파라미터 이름과 값들이 loginProcess.jsp로 전달되게 된다.

사용자가 이 페이지를 통해 아이디와 비밀번호를 입력받고 SUBMIT 버튼을 누르면 loginProcess.jsp 페이지가 호출된다. 다음으로는 loginProcess.jsp 파일을 살펴보자.

    login/loginProcess.jsp

loginProcess.jsp는 사용자로부터 입력받는 아이디와 패스워드를 확인하여 적절한 대응을 하여주는 페이지이다. 여기에는 Login 타입의 bean이 사용되는데 Login은 scope가 application인 bean으로서, 로그인한 사용자 리스트를 관리하는 역할을 한다. loginProcess.jsp 소스를 살펴보자.



1    <%@ page import="CharacterSet,java.util.Enumeration" contentType="text/html; charset=EUC_KR" isThreadSafe="false" %>
2
3    <% String loginUrl = CharacterSet.toKorean(request.getParameter("loginUrl")); %>
4    <jsp:useBean id="user" class="userLog.User" scope="page"/>
5    <jsp:setProperty name="user" property="*"/>
6    <% user.toKorean(); %>
7
8    <% 
9    boolean loginSuccess = false;
10
11   try {
12   %>
13       <jsp:useBean id="login" class="userLog.Login" scope="application"/>
14   <%
15        loginSuccess = login.login(user.getId(), user.getPassword());
16
17   } catch (userLog.UserAlreadyLoginException e) {
18   %>
19       <jsp:forward page="/userLog/userAlreadyLogin.jsp"/>
20   <%
21   } catch (userLog.PasswordNotCorrectException e) {
22   %>
23       <jsp:forward page="/userLog/passwordNotCorrect.jsp"/>
24   <%
25   } catch (userLog.NoSuchUserException e) {
26   %>
27       <jsp:forward page="/userLog/noSuchUser.jsp"/>
28   <%
29   }
30
31   if (loginSuccess) {
32   %>
33   <jsp:useBean id="myLogin" class="userLog.MyLogin" scope="session"/>
34   <%
35       myLogin.login(user.getId(), loginUrl);
36   %>
37   <html>
38   <head><title>성공적으로 로그인 되었습니다.</title>
39   </head>
40   <body>
41   <form name="afterLogin" action="<%= loginUrl %>" method=post>
42   <input type=hidden name="login" value="good">
43
44   <%
45       Enumeration pr = request.getParameterNames();
46
47       String name = "", value = "";
48       while (pr.hasMoreElements()) {
49           name = CharacterSet.toKorean((String) pr.nextElement());
50           value = CharacterSet.toKorean(request.getParameter(name));
51           if ("loginUrl".equals(name) || "loginId".equals(name) || "loginPassword".equals(name)) continue;
52   %>
53   <input type=hidden name="<%= name %>" value="<%= value %>">
54   <%
55       }
56   %>
57   </form>
58   <script language=javascript>
59   document.afterLogin.submit();
60   </script>
61   </body>
62   </html>
63   <%
64   } else {
64   %>
66   로그인 처리가 제대로 되지 않았습니다.
67   <%
68   }
69   %>

11번부터 29번 행까지는 Login bean을 통해 사용자의 로그인을 직접 처리한다. Login bean에는 login() 메쏘드가 정의되어 있는데, 이 메쏘드는 사용자 데이터베이스를 참조하여 인자로 전달 받은 아이디와 비밀번호를 검사한다. 또한, 이 아이디가 이미 로그인한 상태인지 아닌지도 살펴본다. login() 메쏘드는 결과에 따라 다음의 두 가지 예외를 던진다.

  • userLog.PasswordNotCorrectException : 비밀번호가 틀릴 때

  • userLog.NoSuchUserException : 그런 아이디가 존재하지 않을 때

15번 행의 login() 메쏘드에 login.jsp의 form에서 입력받은 아이디와 패스워드를 전달받기 위해 bean을 사용하였다. 4번부터 6번 행까지는 login.jsp의 form을 처리하는 User bean을 정의하고 값을 설정하는 부분이다. User bean은 다른 페이지에서는 사용할 이유가 없으므로 scope를 page로 지정했다. 3번과 6번 행을 주목해야 하는데, 현재 Tomcat은 GET이나 POST로 전달되는 값을 Cp1252로 인코딩하기 때문에 GET이나 POST로 전달되는 값을 beans나 request.getParameter() 메쏘드로 넘겨 받으면 한글이 깨진다. 따라서, GET 혹은 POST로 전달되는 값은 꼭 Cp1252를 EUC_KR로 변환해야 한다. 사용의 편의를 위해 필자는 CharacterSet이라는 클래스 내에 toKorean() 메쏘드를 static으로 정의하였다. 직접 CharacterSet 소스 코드를 살펴보기 바란다.

31번부터 63번 행까지는 입력한 예외가 발생하지 않고 성공적으로 로그인 한 경우에 MyLogin 타입의 session bean을 생성/선언하고 이 bean을 로그인 상태로 설정한다. 또, 이 session bean에 로그인 사용자의 아이디와 로그인한 url을 설정한다. 사용자의 아이디를 설정한 이유는 사용자가 다른 아이디로 로그인하는 경우를 고려한 것이고, url을 설정한 것은 사용자의 사이트 이용 이력을 데이터베이스화하는 것을 고려한 것이다.

37번 부터 62번 행까지는 번 행은 로그인 처리 후 사용자에게 적정한 페이지를 보여주는 작업이다. 이미 log.jsp를 인클루드할 때 loginUrl 인스턴스에 이 값을 설정하였다. 이 값은 이후 login.jsp를 거쳐 loginProcess.jsp까지 전달되어 41번 행에서 form의 action으로 설정된다. 그리고, 44번 행에서 52번 행까지가 우리가 계속 log.jsp, login.jsp을 거쳐 loginProcess.jsp로 넘긴 파라미터들의 이름과 값을 hidden 값으로 설정하는 부분이다.

1번 행을 주목하자. 한글을 제대로 사용하기 위해 EUC_KR로 문자셋을 설정하였고, isThreadSafe를 true로 설정하였다. 문자셋은 모든 jsp 문서마다 언제나 EUC_KR로 설정하여야 한글 사용에 문제가 없다. loginProcess.jsp는 Login bean을 통하여 로그인한 사용자의 아이디를 저장하고 있는 테이블를 참조하고 변경시킨다. 로그인은 동시에 여러 사람이 요청할 수 있으므로, 극단적인 경우에 같은 아이디로 두 사람 이상이 로그인 하게 되면 문제가 생길 수 있다. 따라서, loginProccess.jsp 문서는 한 순간에 오직 하나의 thread가 수행되어야 하고 이를 위해 isThreadSafe를 true로 설정하였다.

  WULM의 Beans

    Log Interface

WULM은 Login, MyLogin 두 개의 beans를 사용하는데, 모두 데이터베이스를 조작한다. 이 때, 데이터베이스 이름과 테이블 이름을 매번 지정하는 것이 번거로울 뿐만아니라, 향후 데이터베이스와 테이블의 이름이 바뀌게 되면 디버깅이 어렵다. 이를 위해, Log interface를 사용하여 한번에 관련 값들을 설정할 수 있도록 하였다. Log interface는 dbUrl, dbId, dbPassword, userTableName, currentUserTableName의 다섯 개 필드만을 정의하고 있고, 각각 데이터베이스 이름, 데이터베이스 사용자 이름, 데이터베이스 사용자 비밀번호, 로그인 아이디와 비밀번호를 저장하고 있는 테이블 이름, 현재 로그인한 아이디를 저장하는 테이블 이름을 명시한다.

    Login Bean

Login bean은 scope가 application으로서 로그인 가능 여부를 확인하고 가능하면 현재 로그인한 사용자 테이블에 아이디를 저장하는 일을 수행한다.

Login bean은 application bean이므로 Tomcat이 셧다운되기 전까지는 살아있다. 만일, Tomcat을 셧다운하면 모든 session bean이 소멸되므로 로그인한 사용자의 개별 bean은 없어지게 된다. 따라서, Tomcat을 셧다운하고 다시 시작한다는 것은 새로이 사용자 접속을 시작한다는 의미게 되며, 이를 제대로 수행하려면 기존의 사용자 로그인 정보는 삭제되어야 한다. Login bean은 생성자에서 이 작업을 수행한다. Login 생성자는 init() 메쏘드를 호출하고 init() 메쏘드는 로그인한 사용자의 아이디를 저장하고 있는 테이블의 모든 값을 삭제한다.

Login bean의 public 메쏘드는 login()이 중요하다. 다른 메쏘드들은 login 메쏘드가 사용하는 메쏘드들로서 private으로 정의되어 있다. login() 메쏘드는 인자로 아이디와 비밀번호를 넘겨 받고 맞는지 틀리는지를 확인하고, 이미 같은 아이디로 접속하였는지도 확인한다. 아이디가 존재하기 않거나, 비밀번호가 틀리거나, 이미 접속한 상태라면 적절한 예외를 던지며 이 예외를 이미 loginProcess.jsp를 설명하면서 언급하였다.

    MyLogin Bean

MyLogin bean은 개별 사용자가 로그인할 때마다 생성되는 session bean이다. loginProcess.jsp에서 로그인이 제대로 수행되었다면 MyLogin bean을 생성한다. MyLogin bean은 두 가지 경로를 통해 로그아웃을 수행한다.

우선 사용자가 직접 로그아웃을 한 경우가 있다. 사이트 내에 로그아웃 아이콘이 있고 사용자가 이것을 클릭함으로써 수행된다. 프로그램 코드를 통해 이미 생성된 bean을 없애는 방법은 없으므로, MyLogin bean의 loginStatus 필드를 false로 만들어서 로그아웃을 했음을 표시해 두고 deleteMeFromTable() 메쏘드를 통해 현재 로그인한 사용자의 아이디를 저장하고 있는 테이블에서 로그아웃하는 아이디를 삭제한다. 이 사용자가 bean이 타임아웃되기 전에 다시 로그인을 시도한다면 이 loginStatus 필드를 통해 로그아웃을 이미 했음을 알 수 있다. log.jsp 파일에서 loginStatus의 값을 넘겨주는 getLoginStatus() 메쏘드를 사용해 이를 적절히 수행한다.

다음으로, 사용자는 굳이 로그아웃을 통하지 않고 그냥 사이트를 떠나는 경우가 있다. 예를 들어, 브라우저를 그냥 종료시키거나, 다른 사이트로 이동한 후 다시 되돌아 오지 않는 경우들이 있다. 더 이상 웹 사이트에 접속하지 않는 사용자는 자동적으로 로그아웃 시켜야 하는데, 이를 위해 타임아웃 시간을 정해야 한다. Tomcat은 정해놓은 타임아웃 시간 동안 사용자의 접속이 더 이상 없다면 자동적으로 session bean을 소멸시키므로, 이를 이용하면 된다. MyLogin session bean이 소멸될 때 자동적으로 로그아웃 과정을 밟도록 하는 것이다. 클래스 인스턴스가 소멸될 때 클래스를 디자인한 사람이 특별히 어떤 일을 하고 싶다면 finalize() 메쏘드를 사용하면 된다. finalize 메쏘드는 클래스 인스턴스가 garbage collection 대상이 되면서 자동적으로 수행되는 메쏘드이다. WULM의 MyLogin bean은 finalize 메쏘드를 이용하여 session bean이 소멸될 때, 자동적으로 로그인한 사용자의 아이디를 저장하는 테이블에서 아이디를 삭제하도록 한다.

Tomcat에서 session bean의 타임아웃 시간은 $TOMCAT_HOME/conf/web.xml 파일의 <session-timeout> 태그 부분에서 설정할 수 있다.

    User Bean

User bean은 login.jsp 페이지에서 사용자가 입력한 아이디와 비밀번호를 처리하는 bean이다. JSP에서 Beans 사용하기를 참조하면 form의 데이터를 beans를 통해 제어하는 방법이 상세히 나와있다.

Form의 데이터를 처리하는데 유의할 점은 한글의 인코딩 문제이다. WULM이 사용하는 User bean의 소스를 보자.



package userLog;

import java.io.*;
import CharacterSet;

public class User {
    private String id;
    private String password;

    public void setLoginId(String str) {
	id = str;
    }

    public void setLoginPassword(String str) {
	password = str;
    }

    public String getId() {
	return id;
    }

    public String getPassword() {
	return password;
    }

    public void toKorean() {
	id = CharacterSet.toKorean(id);
    }
}

setXXX 메쏘드와 getXXX 메쏘드 외에 toKorean 메쏘드를 주목하자. toKorean 메쏘드는 필드인 id와 password를 Cp1252에서 EUC_KR로 바꾸어주는 일을 한다. 이는 GET이나 FORM을 통해 건네받은 값을 <jsp:setProperty>를 통해 설정한 후 바로 실행해야 한다. 일단, EUC_KR로 변환했다면 다시 이 메쏘드를 호출해서는 안된다. GET이나 POST를 통해 건네받는 데이터가 영문으로 구성된다면 이 작업은 필요가 없다.

마치며

프로그래밍은 실전 예를 통해 학습하면 많은 효과를 얻을 수 있다. 다른 사람의 소스를 보고 분석해 보는 것만으로도 실력이 쑥쑥 향상된다. JSP, Servlet등 자바 환경으로 웹 사이트를 만드는 것은 아직까지는 많이 알려지지 않아 많은 개발자들이 그 실제 예를 접할 기회가 별로 없다. 간혹 얻을 수 있는 예라고 하여도 JSP의 새로운 개념이나 자바의 진보적인 개념이 듬뿍 가미된 소스는 찾기 어렵다. 필자는, 독자 여러분에게 JSP가 다른 웹 사이트 개발 방법과 다른 점을 충분히 이해할 수 있도록 JSP의 개념과 자바의 특성이 가미된 WULM을 공개하였다. WULM은 소스를 바로 인스톨하여 자신의 웹 사이트에 적용시킬 수는 없고, 소스를 이해하고 이를 바탕으로 자신의 환경에 적절히 맞추어야 한다. 다음 버전의 소스와 글에서는 좀 더 customization이 쉽도록 개선하도록 하겠다.

+ Recent posts