반응형

JSP란?
Java Server Pages, 즉 JSP는 다이나믹 HTML를 생성하기 위한 자바진영의 기술입니다.

JSP는 마이크로소프트의 ASP에 대항하기 위한 자바진영의 기술로, 서블릿의 가지고 있는 디자인과 코드의 분리의 어려움을 개선한 기술입니다.
JSP 문서의 확장자는 반드시 .jsp 이어야 합니다.
다음 코드는 간단한 JSP파일의 예입니다.


<html>
<body>
<% out.println("Hello JSP Reader"); %>
</body>
</html>

보기에는 단순한 HTML 파일 같지만 사실 자바 코드를 포함하고 있습니다.
이제 이 파일을 클라이언트의 웹 브라우저에서 보기 위해 hello.jsp 로 우리가 만든 /bbs 디렉토리에 옮겨 놓으면 됩니다.


클라이언트가 hello.jsp를 요청하면, 서버는 .jsp 확장자를 인식하고 이것이 특별한 핸들링이 필요하다는 것을 판단하여 서블릿 컨테이너에게 이 파일을 보내게 됩니다. (이것은 웹서버와 PHP 파서와의 관계와 마찬가지입니다)


그러면 JSP엔진은 .jsp 파일을 받아서 이를 서블릿으로 변환합니다.
변환된 서블릿은 톰캣의 경우 <TOMCAT_HOME>/work 에서 확인할 수 있습니다.

 

위의 간단한 예제도 결국은 서블릿으로 변환됩니다.
hello.jsp 가 처음으로 불리워진다면 이 파일은 서블릿으로 변환되고 컴파일된 후에 상주 메모리에 로딩됩니다.


그런 후에 해당하는 출력내용을 요청한 클라이언트에게 보내게 됩니다.


이후 똑같은 요청이 들어오면, 서블릿 엔진은 원본 .jsp 소스가 변경되었는지를 체크합니다.

그리고 객체가 이미 로딩되었는지 확인합니다. 로딩이 되었다면 로딩된 객체가 서비스하고 로딩되지 않았다면 메모리에 로딩시킵니다.

소스가 변경되었다면 JSP엔진은 JSP소스를 다시 파싱하여 서블릿으로 변환합니다.


다음 그림은 JSP 소스의 파싱 과정을 나타낸다.

최초의 클라이어트가 접속하여 서비스를 요청할 때 컨테이너는 JSP 페이지가 서블릿으로 변환하고 서블릿을 컴파일하여 요청에 응답합니다.

일단 서블릿이 요청에 응답하면 메모리에 로딩되어 있는 상태로 다음 클라이언트의 요청을 기다리게 됩니다.

사용자 삽입 이미지

※ JSP도 서블릿 기반의 기술입니다.
따라서 JSP는 서블릿의 자원과 기능을 가집니다.

JSP Directives

이것은 JSP페이지의 전반적인 정보를 서블릿 엔진에게 제공합니다.
JSP 스펙에 의해 현재에 가능한 지시어는
page, include, taglib 입니다.

page Directive

용법 : <%@ page {attribute="value"} %>

 

페이지 지시어의 속성 설명

language="scriptLanguage"

페이지를 컴파일할 서버측 언어가 무엇인지 기술

extend="className"

페이지가 상속한 부모클래스를 정의

import="importList"

페이지가 import하는 자바팩키지 리스트 기술 (,로 구분)

session="true|false"

페이지에 session 데이터가 이용되는지의 여부를 결정

(디폴트값:true)

buffer="none|size in kb"

출력 스트림의 버퍼크기를 결정(디폴트값:8kb)

autoFlush="true|false"

출력버퍼가 자동적으로 비워지는가 또는 버퍼가 차면 익셉션을 발생할것인가 여부를 결정 (디폴트값:true)

isThreadSafe="true|false"

JSP엔진에게 이 페이지가 일시에 다중으로 서비스할 수 있는가의 여부를 알림

(디폴트값은 true, 만약 이 값이 false로 셋팅되었다면

SingleThreadModel 로 페이지가 작동합니다.)

info="text"

JSP페이지에 관한 정보를 나타낸다.

Servlet.getServletInfo()메소드를 이용해 접근가능

errorPage="error_uri"

JSP 익셉션을 다루는 에러 페이지의 상대경로를 나타냄

isErrorPage="true|false"

페이지가 에러핸들링하는 페이지인가를 기술(디폴트값:false)

contentType="ctinfo"        

클라이언트로 보내질 response의 MIME타입과 캐릭터셋


※ 디폴트 값이 적용되므로 개발자는 이 속성을 모두를 정의해 줄 필요가 없습니다.

java.util팩키지를 import하는 페이지 지시어 예


<%@ page import="java.util.*" %>

include Directive


inlcude 지시어는 JSP 변환때에 텍스트나 소스를 삽입하기 위해 사용합니다.


용법 : <%@ include file="relativeURLspec" %>

삽입되는 문서의 포맷을 html이거나 jsp이거나 상관없지만 웹 애플리케이션내에 존재해야 합니다.

예) <%@ include file="header.jsp" %> 상대경로 사용

※ include 지시어는 변환때 한번만 사용됩니다.
따라서 삽입되는 소스파일이 변경될때는 JSP/Servlet엔진이 재시작되기 전까지는 반영되지 않습니다.

taglib Directive


taglib 지시어는 JSP페이지가 커스텀 태그 라이브러리를 이용함을 기술합니다.
커스텀 태그 라이브러리는 각각의 커스텀 태그 집합을 구별하는 prefix와 uri로 유일하게 구별됩니다.

용법 : <%@ taglib uri="tagLibraryURI" prefix="tagPrefix" %>

태그라이브러리 지시어 속성


uri               커스텀태그 라이브러리를 고유하게 이름짓는 URI 참조
prefix            커스텀 태그 라이브러리를 구별하는데 쓰이는 Prefix 정의


 


Scripting


Scripting은 HTML페이지에 자바코드 조각을 바로 삽입하기 위해 사용합니다.
Scripting에는 Declarations, Exceptions, Scriplets 3가지가 있습니다.
이들 각각은 서블릿의 적절한 위치에서 변화되어 놓이게 됩니다.

Declarations


Declarations는 자바 변수와 메소드를 JSP페이지내에서 선언하기 위해선 사용됩니다.

JSP Declarations는 JSP페이지가 첫번째로 로딩될때 초기화되고 그 후에 같은 페이지내의
다른 Declarations, expressions, scriptlets에게 이용되어 진다.

용법 :
<%! declaration %>

변수 선언 예   : <%! String name = new String("BOB") %>
메소드 선언 예 : <%! public String getName() { return name; } %>

Declarations는 서블릿으로 변환될때 서블릿 클래스의 선언부분에 위치하게 됩니다.

 

Expressions


JSP Expressions는 컨테이너에 의해 결과값(문자열)으로 바뀌는 JSP 컴포넌트입니다.
JSP Expressions는 request타임에 .jsp 파일내에서 컨테이너에 의해 변환되어 삽입됩니다.
만약 결과값이 문자열로 변환되지 않는다면 번역시 에러가 발생합니다.
번역시 문자열이 검출되지 않는다면 ClassCastException 익셉션이 request타임에 발생합니다.
용법 :
<%=expression %>
) Hello <%= getName() %>

Scriptlets


Scriptlets는 모든 JSP 요소를 가져올 수 있는 JSP컴포넌트입니다.
이들은 request타임에 실행됩니다.

용법 :
<% scriptlets source %>

Scriptlets를 가지고 있는 페이지를 처음 요청하면 JSP는 서블릿코드로 변환되고 컴파일되고 상주메모리에 로딩됩니다.
<%...%>로 표현되는 Scriptlets는 서블릿의 service() 메소드내에 위치하게 됩니다.

JSP Error 핸들링

JSP 아키텍처는 오로지 에러만을 다룰 수 있는 JSP페이지로서 에러 핸들링의 해법을 제시합니다.
에러는 주로 런타임 에러가 대부분인데 이것은 JSP 바디안 이거나 또는 JSP 바디안 에서 호출하는 어떤 다른 객체에서 발생합니다.


request타임 에러는 호출한 JSP의 몸체에서 잡을 수 있고 또한 다룰 수 있는 에러가 던져졌을 때 발생합니다.

호출한 JSP의 몸체에서 다룰 수 없는 익셉션의 경우는 잡혀지지 않은 익셉션과 함께 클라이언트 request를 에러 페이지로 전송합니다.

JSP Error 페이지 만들기

JSP Error 페이지를 만드는 것은 간단하다.
기본적인 JSP페이지를 만들고 컨테이너에게 이 페이지가 에러 페이지임을 알리면 됩니다.
이것은 앞서 언급한 page Directives의 isErrorPage를 셋팅하면 됩니다.
다음은 에러페이지의 예입니다.

*** errorpage.jsp ***


<html>
<body>
<%@ page isErrorPage="true" %>
Bob there has been an error : <%= exception.getMessage() %> has been reported.
</body>
</html>

<설명>
<%@ page isErrorPage="true" %>
위 지시자는 이 페이지가 에러 페이지라는 것을 컨테이너에게 알리는 역활을 합니다.

<%= exception.getMessage() %>


는 에러페이지로 전달되어 온 익셉션의 에러 메시지를 출력하기 위한 부분입니다.
이때 exception이라는 내장객체를 사용합니다.

JSP Error 페이지 사용법


에러 페이지가 어떻게 작동하는지 알아보기 위해 잡히지 않는 익셉션을 발생시키는 간단한
JSP페이지를 만들어봅시다.

*** testerror.jsp ***

<%@ page errorPage="errorpage.jsp" %>
<%
   if(true){
      //Just throw an exception
      throw new Exception("An uncaght Exception");
   }
%>

에러페이지를 errorpage.jsp로 셋팅했다.

이렇듯 JSP페이지에게 에러페이지를 알리는 방법은 JSP페이지의 page directive에서
errorPage속성값을 정해주는 것입니다.
속성값에서 에러페이지의 위치는 페이지의 상대경로입니다.

이 예제을 실행해보기 위해서는 testerror.jsp 와 errorpage.jsp을
<TOMCAT_HOME>/webapps/bbs/에 위치시키고 testerror.jsp를 방문합니다.

Implicit Objects ( 내장 객체 )

내장 객체는 JSP문서내에서 이용되는 객체인데 특별히 레퍼런스를 얻기 위한 작업 없이 바로 사용할 수 있는 객체를 말합니다.

out

 

javax.servlet.jsp.JspWriter클래스의 인스턴스를 나타냅니다.
데이터를 응답 스트림으로 작성하는데 사용합니다.
이 객체의 일반적인 메소드는 out.println()입니다.
 사용예


*** out.jsp ***


<%@ page errorPage="errorpage.jsp" %>
<html><head><title>Use Out</title></head>
<body>
<%
   //Print a simple message using the implicit out object.
   out.println("<b> Hello Bob! </b>");
%>

</body></html>

request

 

javax.servlet.http.HttpServletRequest 인터페이스의 인스턴스
요청 파라미터나 헤더와 같은 사용자가 요청한 유용한 정보에 대해 접근할 수 있습니다.
request 객체는 모든 HTTP요청과 관련되어 있습니다.
이 객체는 request의 파라미터를 구하는데 일반적으로 사용되는데
getParamter()메소드를 사용함으로써 파라미터의 값을

리턴받습니다.

예)

*** request.jsp ***

<%@ page errorPage="errorpage.jsp" %>
<html>
<head>
   <title>UseRequest</title>
</head>
<body>
<%
   out.println("<b>Welcome: " + request.getParameter("user") + "</b>");
%>
</body>
</html>


실행하려면 다음 URL을 방문합니다. http://localhost/bbs/request.jsp?user=kim



response


javax.servlet.http.HttpServletResponse 인터페이스의 인스턴스입니다.

사용자에게 리턴할 현재 응답을 나타낸다.
하지만 대부분의 HTML 출력은 out객체를 이용합니다.


pageContext

 

jsp범위 내에서 이용 가능한 모든 자원 및 현재의 요청, 응답, ServletContext,

HttpSession, ServletConfig 같은 페이지 속성들에 접근할 수 있는 방법을 제공합니다.

pageContext 객체는 내장 객체를 접근할 수 있는 방법을 제공합니다.

session

session 객체는 javax.servlet.http.HttpSession 객체를 나타냅니다.
세션 데이타를 읽고 저장하는 데 사용됩니다.
 
session 객체의 사용예
*** session.jsp ***


<html> <lhead> <title>Session Example </title> </head>
<body>
<%
   //get a referece to the current count from the session
   Integer count = (Integer)session.getAttribute("COUNT");

   if(count==null){
      //If the count was not found, create one
      count = new Integer(1);
      //and add it to the HttpSession
      session.setAttribute("COUNT",count);
   } else {
      //Otherwise increment the value
      count = new Integer(count.intValue() +1);
      session.setAttribute("COUNT",count);
   }
   out.println("<b>This page has been accessed: " + count + " times.</b>");
%>
</body> </html>

이 예제를 사용하기 위해서는 session.jsp 파일을
<TOMCAT_HOME>/webapps/mnd에 복사하고 http://localhost/bbs/session.jsp 를 방문합니다.
리로드 버튼을 눌러 결과를 확인합니다.

application


application 객체는 javax.servlet.ServletContext 나타낸다.

이 객체는 주로 웹 컴포넌트가 공유할 수 있도록 ServletContext에 저장되어 있는 객체를 접근하기 위해 사용됩니다.


config


이 객체는 ServletConfig의 레퍼런스를 가지고 있습니다.
ServletConfig는 JSP/Servlet엔진의 관한 설정 정보를 담고 있습니다.
초기화 파라미터들에 접근하기위해 사용합니다.


page


이 객체는 현재 JSP페이지의 레퍼런스를 가지고 있습니다.
jsp페이지의 페이지 구현 클래스 인스턴스를 참조하며 Object형으로 선언되어 있습니다.
page변수는 스크립팅 엘리먼트들 안에서 가끔 사용되며, 단순히 jsp페이지와 구현 서블릿 사이의
연결 역할을 합니다.
 
exception


JSP페이지에서 발생한 잡히지 않은 익셉션에 접근을 제공하는 객체입니다.
이 객체는 page 지시어 중 isErrorPage가 true로 설정한 페이지내에서만 사용이 가능합니다.

, 페이지디렉티브를 이용해서 에러 페이지로 지정한 jsp에서만 유용한 객체입니다.


표준 액션

표준 액션이란 미리 제공된 커스텀 태그라 생각하면 됩니다.
6가지 표준액션이 제공됩니다.

<jsp:useBean>

이 표준액션은 자바빈을 생성하거나 생성된 자바빈을 찾기 위한 액션입니다.
<jsp:useBean>은 매우 유연합니다.


JSP문서내에서 <jsp:useBean>부분에 이르면 우선 같은 scope와 ID를 사용하는 객체를 찾습니다.
만약 찾는것에 실패하면 scope와 ID와 관련이 있는 객체를 생성하기 위해 시도합니다.

신택스


<jsp:useBean id="name" scope="page|request|session|application" typeSpec>
body
</jsp:useBean>

typeSpec :: =class="className" |
       class="className" type="typeName" |
       type="typeName" class="className" |
       beanName="beanName" type="typeName" |
       type="typeName" beanName="beanName" |
       type="typeName"

<jsp:useBean>의 속성 설명
id : 특정 scope 내에서 객체의 인스턴스와 관련되어 있는 키
scope : 참조되는 객체의 라이프 page | request | session | application

class : 객체의 구현을 정의하는 클래스
beanName : 자바빈의 레퍼런스
type : 정의된 변수의 타입(정의되지 않으면 class의 속성값과 같습니다)

<jsp:setProperty>

이 액션은 자바빈의 속성값을 셋팅하는 데 쓰입니다.

신택스 : <jsp:setProperty name="beanName" propexpr />
name 속성은 빈의 이름을 나타낸다.

propexpr

property="*" |
property="propertyName" |
property="propertyName" param="parameterName" |
property="propertyName" value="propertyValue"

<jsp:setProperty> 액션의 속성 설명

name  

<jsp:useBean>에서나 다른 액션에서 정의된 인스턴스의 이름


property  

셋팅을 원하는 곳의 속성
만약 propertyName에다가 * 라고 셋팅하면 ServletRequest로부터 차례대로 전달받은 파라미터로 모든 속성값을 세팅하게
됩니다.
만일 해당 파라미터가 비어있습니다면 수정되지 않는다.


param    

셋팅하기를 원하는 파라미터의 이름

value      

빈의 속성에 대응하는 값

<jsp:getProperty>

빈의 속성값을 가져와서 스트림으로 변환하고 이것을 output 스트림에 두는 액션
신택스    <jsp:getProperty name="name" property="propertyName" />

<jsp:getProperty> : 표준 액션 속성 설명
name   : 빈 인스턴스의 이름
property  : 얻고자 하는 빈 인스턴스의 속성값

<jsp:param>

이 액션은 표준 액션, <jsp:include>,<jsp:forward>,<jsp:plugin>에 파람값을 전달할때 이용합니다.

신택스 : <jsp:param name="name" value="value" />

<jsp:param> 의 속성 설명
name : 파리미터의 이름
value : 파리미터의 값

<jsp:include>


이 액션은 JSP페이지에 정적 또는 다이나믹 웹 컴포넌트를 추가할때 사용합니다.

신택스
<jsp:include page="urlSpec" flush="true">
   <jsp:param ... />
</jsp:include>

<jsp:include> 의 속성 설명
page :  인클루드 되는 소스의 상대경로
flush :  버퍼가 비워지는 여부

※ include 지시어와 include 표준 액션과의 차이점
지시어는 한번만 번역타임에 해석되지만 표준액션의 경우는 매 request타임마다 해석됩니다.
따라서 지시어를 사용한 인클루드는 인클루드되는 파일의 변화는 톰캣이 재시동되지 않으면 반영되지 않습니다.


<jsp:forward>

이 액션은 JSP엔진이 실행중에 request를 다른 자원(정적 문서,서블릿, JSP)에게 넘기는 것을 가능하게 합니다.
이 액션에서도 <jsp:param>가 쓰일 수 있는데 이는 포워딩할 대상자원에게 전달할 파라미터를 지정하기 위해 쓰입니다.

신택스   
<jsp:forward page="relativeURL">
   <jsp:param .../>
</jsp:forward>

단일 속성 page는 포워딩할 대상자원을 상대주소로 나타낸 값입니다.

<jsp:plugin>

이 액션은 다운로드나 애플릿,자바빈의 실행을 일으키는 HTML 코드를 생성하는데 사용됩니다.
이 액션은 한번 해석되어 <object> 나 <embed>로 바뀝니다.
속성은 바뀌는 코드에 표현을 위한 설정데이터로 제공됩니다.

신택스
<jsp:plugin type="pluginType" code="classFile" codebase="relativeURLpath">
   <jsp:params>
   </jsp:params>
</jsp:plugin>

<jsp:plugin>의 속성 설명
type : 인클루드할 플러그인 타입 예를 들면 applet
code : 플러그인이 실행할 클래스의 이름
codebase : 클래스의 절대 또는 상대경로



서블릿컨텍스트와 웹 애플리케이션의 관계

 

서블릿컨텍스트와 웹 애플리케이션과의 관계에 대해 알기 전에 먼저 서블릿컨텍스트가 무엇인지 부터 알아야 합니다.
서블릿컨텍스트(ServletContext)는 javax.servlet 팩키지에 속하는 객체입니다.

이것은 웹 애플리케이션의 서브 사이드 컴포넌트와 서블릿 컨테이너와의 통신을 담당하는 메소드 집합을 정의합니다.

서블릿컨텍스트의 가장 일반적인 사용중 하나는, 웹 애플리케이션의 서버 사이드 컴포넌트들 모두에게 이용될 수 있는 객체를 위한 저장소로 이용되는 것입니다.
서블릿컨텍스트에 저장된 객체는 웹 애플리케이션의 일생동안 존재합니다.
4개의 서블릿컨텍스트의 메소드는 공유메모리기능를 제공하기 위해 쓰입니다.

다음은 각각의 메소드에 관한 설명입니다.

setAttribute(java.lang.String name,java.lang.Object object)


첫번째 파라미터값인 이름으로 객체를 바인딩하고 객체를 서블릿컨텍스트에 저장합니다.
만약 특정 이름이 이미 사용중이라면 기존의 이름으로 바인딩된 속성을 지우고 새로운 객체를 이 이름으로 바인딩합니다.

getAttribute(java.lang.String name)


첫번째 파라미터값을 이름으로 참조하는 객체를 반환합니다.
만약 주어진 이름으로 바인된 속성이 없다면 널을 반환합니다.

getAttributeNames()
서블릿컨텍스트에 저장된 속성이름을 Enumeration 타입으로 반환합니다.

웹 애플리케이션과 서블릿컨텍스트의 관계


우리는 이미 <TOMCAT_HOME>/conf/server.xml 파일에 새로운 Context를 추가했습니다. (톰캣 admin 툴을 이용해서).
이 작업을 하면 웹 애플리케이션을 생성한 것입니다.
웹 애플리케이션을 추가함과 동시에 새로운 서블릿컨텍스트를 또한 만든 것입니다.
이것이 웹 애플리케이션과 서블릿컨텍스트의 관계입니다.
모든 웹 애플리케이션마다 단 하나의 서블릿컨텍스트가 있습니다.
이는 서블릿 스펙에 의해 요구되어지고 모든 서블릿엔진에 의해 수행됩니다.

웹 애플리케이션이 웹 애플리케이션 컴포넌트에 어떻게 영향을 미치는지의 예

실제로 서블릿컨텍스트와 웹 애플리케이션과의 관계를 이해하기 위해,
우리는 이미 만든 /bbs 웹 애플리케이션을 외에 /bbs2 라는 웹 애플리케이션을 추가합니다.(톰캣 admin 툴 이용)
그리고 각각의 웹 애플리케이션에 아래와 같은 2개의 웹컴포넌트를 각각 배치합니다.


첫번째 웹 컴포넌트 서블릿

*** ContextText.java ***

 

package example;

import javax.servlet.*;

import javax.servlet.http.*;

import java.io.*;

import java.util.*;

 

public class ContextTest extends HttpServlet{

             private static final String CONTENT_TYPE="text/html;charset=euc-kr";

             public void init() throws ServletException {

             }

             public void doGet(HttpServletRequest request,HttpServletResponse response)

                           throws ServletException,IOException{

 

                           doPost(request,response);

             }

             public void doPost(HttpServletRequest request,HttpServletResponse response)

                           throws ServletException,IOException{

 

                           // ServletContext 레퍼런스를 얻는다.

                           ServletContext context=getServletContext();

                           // ServletContext로부터 count 속성값을 얻기를 시도합니다.

                           Integer count=(Integer)context.getAttribute("count");

                           // 만약 count 속성이 없다면 만든다.

                           // count 속성 초기값은 0 입니다.

                           if(count==null){

                                        count=new Integer(0);

                                        context.setAttribute("count",new Integer(0));

                           }

 

                           response.setContentType(CONTENT_TYPE);

                           PrintWriter out=response.getWriter();

                           out.println("<html>");

                           out.println("<head><title>ContextTest</title></head>");

                           out.println("<body>");

                           // count 속성값의 현재값을 출력합니다.

                           out.println("<p>현재 COUNT 는 : "+count+".</p>");

                           out.println("</body></html>");

                           // doPost메소드가 불릴때마다 count 속성값을 1씩 증가시킨다.

                           count=new Integer(count.intValue()+1);

                           // ServletContext에 증가한 count 속성을 저장합니다.

                           context.setAttribute("count",count);

             }

             public void destory(){

             }

}

 


코드 설명


1. getServletContext()메소드를 이용해서 ServletContext의 레퍼런스를 얻습니다.


ServletContext context = getServletContext();

2. 서블릿컨텍스트의 레퍼런스를 얻었다면 서블릿컨텍스트로부터 count 속성값을 얻고자 시도합니다.

이때 getAttribute()메소드를 사용합니다.

Integer count = (Integer)context.getAttribute("count");

3. 서블릿컨텍스트에 count라는 속성이 있는지 조사하고 만일 그런 속성이 존재하지 않는다면 만들고 서블릿컨텍스트에 추가합니다.

이때 setAttribute()메소드를 사용합니다.

if( count == null) {
   count = new Integer(0);
   
context.setAttribute("count",new Integer(0));
}

4. 속성이 발견되었다면 이 값을 출력스트림에 씁니다.


out.println("<p>The current COUNT is : " + count + ".</p>");

5. 서블릿컨텍스트의 속성값을 증가시키고 이 값을 서블릿컨텍스트에 추가합니다.

count = new Integer(count.intValue() + 1);

context.setAttribute("count", count);

서블릿을 2개의 웹 애플리케이션에 배치합니다.
위 서블릿 코드를 bbs/WEB-INF/classes와 bbs2/WEB-INF/classes 에 위치시킨후

javac -d . ContextTest.java


라는 명령어로 컴파일하고


http://localhost/bbs/servlet/example.ContextTest

http://localhost/bbs2/servlet/example.ContextTest


를 방문해 확인합니다.

2번째 웹컴포넌트 JSP페이지


파일위치 : /bbs/example/ , /bbs2/example/


***
ContextTest.jsp ***


<%@ page contentType="text/html;charset=euc-kr" %>

<HTML><head><title>ContextTest</title></head><body>

<h1>

<%

             // count 속성을 얻기를 시도합니다.

             Integer count=(Integer)application.getAttribute("count");

             // 만약 count 속성이 null이면 하나 만들어서 서블릿컨텍스트에 저장합니다.

             if(count==null){

                           count=new Integer(0);

                           application.setAttribute("count",count);

             }

%>

 

현재 COUNT 는 : <%=count%>

<%

             // count 속성을 1 증가시켜 서블릿컨텍스트에 새로 저장합니다.

             count=new Integer(count.intValue()+1);

             application.setAttribute("count",count);

%>

</h1>

</body></html>


이 파일을 bbs/example 와 bbs2/example 에 복사합니다.
각각의 분리된 웹 애플리케이션이 어떻게 이들 컴포넌트에 영향을 미치는지 확인합니다.
실험을 시작하기 위해 다음 URL를 방문합니다.

http://localhost/bbs/example/ContextTest.jsp

http://localhost/bbs2/example/ContextTest.jsp



Refresh 버튼을 여러번 누르고 count가 증가되는지 확인합니다.
새로운 브라우저를 열고 서블릿을 방문합니다.

http://localhost/bbs/servlet/example.ContextTest

http://localhost/bbs2/servlet/example.ContextTest



출력결과는 ContextTest.jsp 와 같을 것입니다.
count의 값에 주목합니다.


ContextTest.jsp에서보다 1만큼 증가된 값입니다.
이것은 서블릿과 JSP가 같은 서블릿컨텍스트를 공유하고 있습니다는 의미입니다.
서블릿 컨텍스트의 공유메모리 기능입니다. (공동 저장소 기능)
각각 JSP와 서블릿을 방문함으로써 count 객체가 어떻게 공유되는지 알 수 있습니다.

이제 /bbs2/ContextTest.jsp JSP페이지를 새로운 창으로 방문합니다.
http://localhost/bbs2/example/ContextTest.jsp count의 값이 0인 결과화면을 볼 수 있습니다.
이것이 의미하는 것은 /bbs 웹 애플리케이션이 사용하는 count와 지금의 count는 다르다는 것입니다.
이것이 웹 애플리케이션과 서블릿컨텍스트의 관계입니다.
/bbs2 와 관련이 있는 서블릿 컨텍스트는 이 웹 애플리케이션에 유일합니다.
그리고 다른 웹 애플리케이션 /bbs 안에 있는 웹 컨포넌트에 의해 영향을 받지 않는다는 것입니다.


다시 확인을 위해 /bbs2 의 서블릿을 방문합니다.
http://localhost/bbs2/servlet/example.ContextTest
/bbs2 에 배치된 JSP와 같은 count 레퍼런스를 사용함을 알 수 있습니다

 

JSP 문법에서 꼭 확인해야 할 사항들


1. <%@ page session=true>


Q. page 지시자에서 session 속성은 구체적으로 무엇을 의미하는가?

page 지시자에 session 속성이 false로 되어 있으면 웹 컨테이너에서는 해당 페이지가 session 객체를 생성하지도 못하고 또한 기존의 session 객체에 대한 레퍼런스도 얻을 수도 없습니다.

false로 되어 있는 상태에서 session 객체에 접근하고자 하면 에러가 발생하게 됩니다.

 

2.<jsp:useBean id="name" scope="page|request|session|application"  class=ClassName />


Q. 표준액션의 <jsp:useBean>에서 scope 속성은 무엇을 의미하는가?

scope 속성은 jsp:useBean 속성 중에서 가장 중요한 부분입니다.

일단 이 속성은 자바 빈즈를 객체화시킨 후 어느 범위까지 사용을 할 것인지를 결정하는 것입니다.

Scope 속성으로 지정한 영역의 수명에 따라서 빈 객체는 하나의 페이지가 아니라 여러 페이지에서 소멸하지 않고 참조되기고 합니다.

(예를 들어 scope 속성이 session 으로 설정되었다면 빈 객체는 세션이 종료할 때까지 소멸되지 않고 유지됩니다.)

빈 객체가 생성된 이후 어떤 페이지에서도 참조될 수 없을 때 자동으로 소멸됩니다.

Scope 속성은 기본 값은 page입니다.

scope 속성에는 4개의 값을 지정해 줄 수 있는데 각각의 값에 대해서 정리하면 다음과 같습니다.

 

page : 자바 빈즈가 현재의 JSP 페이지 내에서만 사용 가능하도록 설정할 때

기본 값이므로 특별히 지정하지 않으면 이 옵션이 적용됩니다.

빈 객체의 수명은 한 페이지 내에서만 유지됩니다.

Page 영역으로 생성된 객체는 요청된 페이지 내에서만 유효합니다.

같은 페이지를 요청해도 새로운 빈 객체가 생성됩니다.

페이지의 실행 종료와 함께 빈 객체는 소멸됩니다.

매 페이지마다 새로 생성되므로 객체가 생성자의 초기화 부분을 항상 실행합니다.

Page영역은 한 페이지에서만 유효하므로 <jsp:include> 태그나 <jsp:forward> 태그로 포함된 페이지에서

<jsp:useBean> 태그는 이미 만들어진 빈 객체를 참조하는 것이 아니라 같은 id 속성값을 가진 새로운 객체를 생성하게 됩니다.

또한 <jsp:include> 태그에서 생성한 빈 객체는 원 페이지에서 참조될 수 없습니다.

Page 영역의 빈 객체는 jsp 페이지가 불릴 때마다 먼저 생성된 빈 객체의 상태가 필요하지 않고 새로 생성되어도 되는 경우에 적합하다.

 

request : JSP 페이지는 jsp:forward, jsp:include 태그를 이용해서 다른 JSP 페이지와 함께 사용할 수 있는데 이 값으로 범위를 지정하면 현재의 JSP 페이지와 연결되어 있는 모든 JSP 페이지까지 영향을 미칩니다.

이럴 경우 이 자바 빈즈의 id는 다른 페이지에 동일한 이름의 id가 존재해서는 안됩니다.

반대로 page 속성으로 지정되어 있을 경우에는 현재 페이지에 jsp:forward와 jsp:include가 있을 지라도 해당 페이지까지 영향을 미치지는 않습니다. 즉, 해당 페이지가 원 페이지에서 참조하는 자바 빈즈를 참조하지 못합니다.

또한 request 영역으로 생성된 빈 객체를 request 객체에 저장하여 request 객체에서 찾아서 참조가 가능합니다.

<jsp:include> 태그와 <jsp:forward>태그를 사용하는 경우 같은 request 객체를 사용하게 됩니다.

 

session : 이 값으로 범위를 지정해 놓으면 세션에 유효할 때까지 자바 빈즈의 객체가 유효합니다.

세션이 유지되는 동안 같은 세션에서 호출되는 모든 페이지에서 빈 객체는 소멸되지 않고 유지됩니다.

Session 영역으로 생성된 빈은 session 객체에 저장됩니다.

각 사용자에 대하여 독립적으로 생성되는 session 객체는 세션이 종료될 때까지 호출되는 모든 페이지에서 id 속성으로 구분되는 객체들을 유지하고 사용할 수 있게 해준다.

한가지 주의할 점은 page 지시자에서 session 속성을 false로 해 놓았을 경우에는 이 자바 빈즈 객체는 사용할 수 없게 됩니다.

page,request  scope 속성으로 생성된 빈은 브라우저의 요청에 대하여 결과를 생성하여 돌려준 다음에는 소멸됩니다.

 

application : 이 값은 가장 광범위한 범위를 갖는 값입니다. 이 값으로 범위를 지정하면 모든 JSP 페이지에서 사용할 수 있으며 자바 빈즈 객체는 JSP 엔진을 정지시키거나 다시 시작시킬 때까지 계속 살아 있게 됩니다.

즉, JSP엔진에 의해 소멸되지 않고 유지됩니다.

application scope 속성으로 생성된 빈은 application 내장객체에 저장되어 같은 웹 애플리케이션을 사용하는 모든 사용자에게 사용되어 질 수 있게 됩니다. 이 옵션은 거의 사용되지 않습니다.

 

3. <%@ include file=”상대경로파일명” %> 과 <jsp:include page=”상대경로파일명” /> 의 정확한 차이는?

 

Include 디렉티브의 경우 포함되는 페이지와 현재 jsp 페이지가 합쳐져서 하나의 jsp 페이지(이는 곧 합쳐진 jsp페이지가 서블릿으로 변환됨을 의미)가 되는 반면에 <jsp:include>액션 태그로 포함되는 페이지와 포함하는 페이지가 합쳐지지 않고 그대로 존재하게 됩니다.

<출처:
http://cafe.naver.com/webprogramguide>

반응형

바로 JSP로 만든 게시판으로 들어가지는 않겠습니다.


왜냐하면 JSP는 서블릿 기반 기술이기 때문입니다. 서블릿에 대한 기본적인 이해를 한 다음  JSP를 하는 것이 낫다고  생각하기

때문입니다.


JSP가 서블릿 기반 기술이라는 것은 확장자가 jsp인 파일은 톰캣과 같은 서블릿 엔진에 의해 결국은 서블릿으로 변환된 다음에

서비스되기 때문입니다.


서블릿은 CGI 프로그래밍에 대한 자바측 기술로 등장하게 됩니다.

JSP는 마이크로소프트 진영의 ASP가 인기를 끌자 ASP에 대응하기 위한 자바측 기술로 나타납니다.


서블릿 문법


서블릿 아키텍처는
javax.servletjavax.servlet.http 두개의 팩키지로 구성됩니다.

javax.servlet 팩키지는
모든 서블릿이 상속하거나 구현하는 일반적인 인터페이스와 클래스로 구성되어 있습니다.

javax.servlet.http 팩키지는 HTTP 프로토콜에 맞춘 서블릿 클래스로 구성됩니나.

javax.servlet.Servlet 인터페이스는 서블릿 아키텍처의 핵심으로 모든 서블릿의 기초 클래스인 Servlet 인터페이스는

5개의 메소드를 정의합니다.


그 중 가장 중요한 3개의 메소드는 다음과 같다.

init() : 서블릿을 초기화
service() : 클라이언트 요청에 대한 서비스
destory() : 자원반납
이들 메소드는 서블릿 라이프사이클 메소드를 구성합니다.

상속을 통해서든 아니면 직접적인 구현을 통해서 모든 서블릿은 반드시 Servlet 인터페이스를 구현해야 합니다.

서블릿 프레임워크

사용자 삽입 이미지

GenericServlet 과 HttpServlet 클래스서블릿 아키텍처에서 보이는 2개의 중요한 클래스는 GenericServletHttpServlet 클래스입니다.

HttpServlet 클래스는 GenericServlet 클래스를 상속합니다.

GenericServletServlet를 구현한 클래스입니다.

HttpServletGenericServlet는 서블릿을 개발할때 이들 중 하나를 사용합니다.

GenericServlet클래스를 상속하면 service()메소드를 구현해야 합니다.
GenericServlet.service()메소드는 프레임워크를 따르기 위해서 반드시 구현하도록 추상 메소드로 정의됩니다.

GenericServlet.service() 프로토타입

public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;

 

추상 메소드이므로 프로그래머가 반드시 구현해야 합니다.

service()메소드로 전달되는 2개의 파라미터는 ServletRequest ServletResponse 객체입니다.

ServletRequest 는 서블릿으로 전달되어진 모든 정보를 가지고 있습니다.
ServletResponse 는 클라이언트에게 보내고자 하는 데이터가 위치하는 곳입이다.

HttpServlet 클래스를 확장했을때는, service()메소드를 일반적으로 구현할 필요가 없습니다.
HTTPServlet 클래스는 이미 service()메소드를 구현하고 있기 때문입니다.

HttpServlet.service() 프로토타입


Protected void service(HttpServletRequest req, HttpServletResponse res) throws ServletException,IOException;

HttpServlet.service()메소드가 호출되면
이 메소드는 request 안에서 메소드 타입값을 읽어내고 이 값에 따라서 호출할 메소드를 결정합니다.

이 메소드는 우리가 오버라이딩 해야하는 메소드입니다.

메소드 타입이 GET 이면 우리가 오버라이팅 할 메소드는
doGet(),  POST 이면 doPost() 입니다.

service()메소드는 5가지 메소드타입을 가지고 있지만 doGet()과 doPost()만 관심을 가지면 됩니다.

HttpServlet 클래스에 전달되는 파라미터, HttpServletRequest와 HttpServletResponse는
Servlet 클래스에 전달되는 파라미터였던 ServletRequest, ServletResponse 각각을 상속한 클래스입니다.

서블릿의 라이프 사이클
자바서블릿의 라이프사이클은 매우 논리적인 순서를 따르고 있습니다.
라이프사이클 메소드를 선언하고 있는 인터페이스가 javax.servlet.Servlet 인터페이스 입니다.

서블릿 라이프사이클 메소드는 init(), service(), destory() 입니다.

1. init() 메소드를 이용하여 서블릿이 로딩되고 초기화 됩니다.

2. 서블릿이 로딩되면 클라이언트이 요청에 응답을 하는 서비스를 시작하는데 이때 servie() 메소드가 이용됩니다.

3. 서블릿이 shutdown 될때 이용되는 메소드가 destory() 메소드입니다

init()


init() 메소드는 서블릿이 그 일생을 시작하게 하는 메소드입니다.
이 메소드는 서블릿이 인스턴스가 된 후 바로 호출되고, 단 한번만 호출됩니다.
init() 메소드는 request 를 다룰 때 사용하는 자원을 만들고 이 자원을 초기화할때 사용합니다.
init() 메소드의 모습은 다음과 같습니다.

public void init(ServletConfig config) throw ServletException;

init() 메소드는 파라미터로 ServletConfig 객체를 전달받습니다.
init() 메소드는 또한 ServletException을 던질 수 있도록 선언되어 있습니다.

만약 서블릿이 request를 핸들링할 수 있는 자원을 초기화할 수 없게 되었을 때 init() 메소드는 ServletException 을 해당 에러 메시지와 함께 던집니다.

service()


service()메소드는 request/response 패턴을 사용하는 클라이언트로부터 받은 모든 request에 대해 서비스합니다.

service()메소드의 모습은 다음과 같습니다.
public void service( ServletRequest req, ServletResponse res) throws ServletException, IOException;

service()메소드는 2개의 파라미터를 가집니다.
하나는 ServletRequest 객체인데, 클라이언트가 제공한 정보를 담고 있습니다.
다른 하나는 ServletResponse 객체인데, 클라이언트에 보내는 정보를 담고 있습니다.

service() 메소드를 가장 일반적인 구현은 HttpServlet 클래스에서 이루어진다.
HttpServlet 클래스는 GenericSevlet를 상속하므로서 Servlet을 구현합니다.
 
destory()


이 메소드는 서블릿 라이프의 끝을 의미합니다.
웹 애플리케이션이 shutdown 될때, 서블릿의 destory() 메소드가 호출됩니다.
그러면 init() 메소드에 의해 만들어진 모든 자원이 반납됩니다.

destory() 메소드의 모습은 다음과 같습니다.

public void destroy(); 

간단한 서블릿 예제와 설명이제 서블릿이 무엇이고 어떻게 작동하는에 관한 기본적인 이해를 가지게 되었습니다.
간단한 서블릿에 예제를 만들어 보겠습니다.

***
SimpleServlet.java ***


package example;

 

import javax.servlet.*;

import javax.servlet.http.*;

import java.io.*;

import java.util.*;

 

public class SimpleServlet extends HttpServlet{

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

                           doPost(req,res);

             }

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

 

                           res.setContentType("text/html;charset=euc-kr");

                           PrintWriter out=res.getWriter();

 

                           out.println("<html>");

                           out.println("<head><title>Simple Servlet</title></head>");

                           out.println("<body>");

 

                           //요청한 클라이언트의 IP를 출력합니다.

                           out.println("Your address is " + req.getRemoteAddr() + "\n");

                           out.println("</body></html>");

                           out.close();

             }

}

 

※ 여기서 주의하여 볼 것은 SimpleServlet가 init() 과 destory()메소드를 구현하고 있지 않는다는 것입니다.
이들 메소드는 GenericServlet 이 기본적으로 두개의 메소드를 구현하고 있기에 구현하지 않아도 되는 것이죠

SimpleServlet을 컴파일, 실행

0. http://www.okjsp.pe.kr 에서 Frequently Asked Question 의 서블릿 접근이 안되는 경우에서 web.xml 파일을

{TOMCAT_HOME}/conf 에 복사합니다. (그전에 기존의 web.xml 파일은 백업을 하는 것이 좋겠습니다)
1. servlet.jar를 CLASSPATH에 추가시킵니다.
2. SimpleServlet 컴파일합니다. 
javac –d . SimpleServlet.java
3. http://localhost:8080/bbs/servlet/example.SimpleServlet 를 방문해서 결과를 확인합니다.

<결과 화면>

사용자 삽입 이미지

※ 전 글에서 아파치와 연동을 위한 작업을 했기 때문에 localhost 다음에 포트 번호가 빠져도 실행이 됩니다.

SimpleServlet에 접근하기 위한 URL에 /servlet 이 포함되어 있어야 합니다.
이렇게 하므로써 컨테이너에게 서블릿을 찾고 있음을 알리게 됩니다.

SimpleServlet 코드 설명


SimpleServlet 에서 doGet()과 doPost()
이 두 메소드는 SimpleServlet 에서 오버라이드한 메소드입니다.
모든 비즈니스 로직이 이곳에 존재합니다.
예제에서 doGet() 메소드는 단순히 doPost() 메소드를 호출합니다.

doPost() 의 첫번째 실행라인

res.setContentType("text/html;charset=euc-kr");

이는 response에 대한 컨텐츠타입을 셋팅하는 작업입니다.
이것은 단 한번만 사용이 가능합니다.
또한 OutputStream으로서 PrintWriter를 코딩하기에 앞서 위 코딩 작업이 이루어져야 합니다.

다음은 PrintWriter 을 획득해야 합니다.

왜냐하면 클라이언트의 웹 브라우저에 IP를 나타내기 위해서 입니다.
PrintWriter 의 획득은 ServletResponse 의 getWriter() 을 호출함으로써 이루어집니다.

PrintWriter out = res.getWriter();

PrintWrtier는 클라이언트 응답에 보내지는 스트림에 글을 쓸 수 있게 합니다.
그러면 클라이언트의 브라우저에서 이 글을 볼 수 있게 됩니다.

클라이언트에 텍스트를 보낼 수 있는 객체에 대한 레퍼런스(out)를 얻었으므로,
이 객체를 사용하여 클라이언트에게 메시지를 전달할 수 있는 것입니다.

이 메시지는 HTML를 포함하고 있습니다.
이는 메시지가 클라이언트의 웹브라우저에 보이게 하기 위함입니다.
다음 몇 라인은 이것이 어떻게 행해지는지 보여준다.

out.println("<html>");
out.println("<head><title>Simple Servlet</title></head>");
out.println("<body>");

//요청한 클라이언트의 IP를 출력합니다.
out.println("Your address is " + req.getRemoteAddr() + "\n");

위에서 보듯이 SimpleServlet는 클라이언트에게 HTML을 보내기 위해 PrintWriter의 println()메소드를 사용하고 있습니다.

//요청한 클라이언트의 IP를 출력합니다.

out.println("Your address is " + req.getRemoteAddr() + "\n");


req.getRemoteAddr() 부분은 클라이언트에서 보내온 정보를 이용합니다.
즉, HttpServletRequest 의 getRemoteAddr() 메소드를 호출하여 클라이언트의 주소를 반환받고 있습니다.
이렇게, HttpServeltRequest 는 클라이언트가 보내거나, 클라이언트에 관한 정보를 담고 있습니다.

HttpServletRequest와 HttpServletResponse 객체에 관한 자세한 정보는 썬의 웹사이트에서 찾을 수 있습니다.


http://java.sun.com/products/servlet/

다음은 기본적으로 한번은 읽어 봐야 할 서블릿 클래스와 그의 메소드를 정리해 놓은 것입니다.

하나 하나 다 설명은 없지만 서블릿 API를 통해서 한번쯤은 읽고 넘어간다면 웬만한 JSP에 관한 책은 모두 이 범위 안에 있기에 이해가 쉬울 것입니다.

 

서블릿 클래스 요약

 

Servlet

init()

service()

destroy()

getServletConfig()

getServletInfo()

 

ServletConfig

getInitParameter()

getInitParameterNames()

getServletContext()

 

GenericServlet

log()

 

HttpServlet

doGet()

doPost()

 

ServletContext

setAttribute()

getAttribute()

getRequestDispatcher()

getRealPath()

getResource()

 

RequestDispatcher

forward()

include()

 

ServletRequest

getInputStream()

getParameter()

getParameterValues()

 

HttpServletRequest

getCookie()

getSession()

getSession(boolean created)

 

ServletResponse

getOutputStream()

getWriter()

 

HttpServletResponse

addCookie()

sendRedirect()

 

Servlet API Spec 은 다음 사이트에서 다운로드 받을 수 있습니다.

http://java.sun.com/products/servlet

 

서블릿 연습하기

 

Hello Servlet ! : 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("<HEAD><TITLE>Hello Servlet</TITLE></HEAD>");

                           out.println("<BODY>");

                           out.println("Hello Servlet!");

                           out.println("</BODY>");

                           out.println("</HTML>");

                           out.close();

             }

}

 

<설명>

1.HttpServlet 클래스를 상속받은 서블릿 클래스는 public class 로 선언해야 합니다.

 

2.HTTP 의 GET 방식으로 웹 브라우저가 요청해오면 doGet() 메소드를 작성합니다.

(일반적으로 웹서버의 자원을 요청하는 것은 GET 방식의 요청입니다.)

 

3.doGet() 메소드는 HttpServletRequest 와 HttpServletResponse 타입의 아규먼트를 가집니다.

이 메소드는 예외가 발생할 수 있으므로 다음 문장으로 예외를 처리합니다.

throws ServletException, IOExcepiton

 

4. res.setContentType(“text/html;charset=euc-kr”);

은 웹브라우저에 응답으로 출력될 문서의 MIME 타입을 기술합니다.

;charset=euc-kr을 쓰지 않으면 한글이 깨지는 경우가 발생합니다.

setContentType()메소드는 HttpServletResponse 의 메소드입니다.

 

5. PrintWriter out = res.getWriter();

웹브라우저에 대한 출력 스트림을 얻습니다.

out의 plintln() 메소드안에 문자열을 넣으면 그대로 흘러 흘러서 클라이언트의 웹브라우저에 출력된다고 생각하시면 됩니다.

 

6. 주소창에서 /bbs/servlet/ 는 /bbs/WEB-INF/classes 로 접근하기 위한 일종의 약속입니다.

 

7. HelloServlet 서블릿이 어떻게 작동하는가?

클라이언트, 즉 웹브라우저가 서버의 HelloSerlvet 서블릿 자원을 요청합니다.

서블릿엔진인 톰캣은 클라이언트의 요청을 캡슐화한 HttpSerlvetRequest 인터페이스를 구현한 객체와

요청에 대한 응답을 캡슐화한 HttpSerlvetResponse 인터페이스를 구현한 객체를 service(,) 메소드의 아규먼트로 넘깁니다.

HttpServlet은 GenericServlet 의 추상 sevice(,)메소드를 구현한 클래스입니다.

구현내용은 단지 웹브라우저가 헤더로 보낸 HTTP 메소드(GET,POST)에 따라 자동적으로 doGet(,) 또는 doPost(,)메소드를 호출하도록 하는 것이 전부입니다.

(따라서 doGet(,)이나 doPost(,)를 생각하지 않으려면 service(,)메소드를 오버라이딩하면 됩니다.)

객체로 된 HelloServlet 서블릿은 클라이언트의 각각의 요청을 개발자가 구현한 doGet(,), doPost(,) 혹은 service(,) 메소드가 병행적으로 처리하므로써 클라이언트의 요청에 응답하게 됩니다.

 

"Hello Servlet" 을 출력하는 기본적인 예제를 실행해 봤습니다.

이제는 웹브라우저, 즉 클라이언트가 보내는 정보를 어떻게 서블릿에서 다룰 수 있는지 알아봅니다.

정확하게 클라이언트가 보내는 문자열 정보를 서블릿에서 catch하는 코드를 설명합니다.

 

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

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=”windows”>windows</option>

<option value=”linux”>linux</option>

<option value=”solaris”>solaris</option>

</select>

req.getParameterValues(“os”);

(참고: multiple 속성이 없다면

req.getParameter(“os”)도 가능)

 

위의 내용을 하나 하나 예로 들어 설명합니다.

 

GetData.html / GetData.java

 

HTML FORM 태그의 텍스트필드를 이용한 문자열 전송

 

파일 위치 : /bbs/example/GetData.html , /bbs/WEB-INF/classes/GetData.java

*** GetData.html ***

 

<html><body>

<center><h1>GET 테스트</h1></center>

<form method=POST action=../servlet/example.GetData>

이름 : <input type=text name=name size=20><br>

주소 : <input type=text name=addr size=20><br>

<input type=submit value="전송"><input type=reset value="취소">

</form>

</body></html>

 

*** GetData.java ***

 

package example;

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;

 

public class GetData 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 name = req.getParameter("name");

                  String addr = req.getParameter("addr");

                  out.println("<html><body>");

                  out.println("<center><h2>GET TEST</h2></center>");

                  out.println("<li>이름 : "+name);

                  out.println("<li>주소 : "+addr+"<br>");

                  String path = req.getContextPath();

                  out.println("<a href="+path+"/example/GetData.html>뒤로</a>");

                  out.println("</body></html>");

                  out.close();

         }

}

 

<설명>

 

1. HTML문서와 서블릿간의 상대경로에 주의합니다.

HTML이나 JSP에서 서블릿을 링크할때는 /servlet/을 컨텍스트 루트처럼 생각하고 상대경로를 걸면 해결됩니다.

하지만 서블릿 코드에서 HTML문서나 JSP문서에 링크를 걸때는 req.getContextPath() 메소드를 통해 풀패스를 얻어서 경로를 걸어야 확실합니다.

 

2. 서블릿 소스에 package 가 선언되어 있다면 그 서블릿은 팩키지명.서블릿클래스명 으로 접근합니다.

 

3. HttpServletRequest 의 setCharacterEncoding(“euc-kr”)은 웹브라우저,즉 클라이언트가 보내는 한글 데이터를 한글 인코딩으로 받기 위한 것입니다.

이 코드조각이 없다면 클라이언트가 보낸 한글 데이터는 깨질 것입니다.

반면 HttpServletResponse.setContentType() 메소드는 서블릿이 만드는 HTML문서의 타입과 문자셋을 지정하는 것입니다.

혼동하지 말기 바랍니다.

 

4. HttpServletRequest 의 getParameter() 메소드는 가장 보편적으로 웹브라우저에서 사용자가 보내는 데이터를 받기 위한 메소드입니다.

 

6. 상대경로 문제

JSP 나 HTML 문서에서 서블릿을 링크시킬때는 /bbs/servlet/까지를 Context Base 라고 생각하고 일반적인 상대경로 개념으로 접근합니다.

서블릿에서 JSP 나 HTML문서를 링크시킬때는

req.getContextPath() 로 일단 Context base 경로를 구한후에 이를 이용해 경로를 링크시키면 됩니다.

 

톰캣4.1.30 버전에서 테스트 할 때는 GET 방식으로 클라이언트가 보낸 한글 이름과 주소가 정상적으로 출력이 되었으나 5.0.28 에서 테스트 할 때에는 한글이 깨져서 나옵니다.

근본적인 해결책은 아직 못찾아보았고, 대신 GET방식이 아닌 POST 방식으로만 서버에 정보를 보내도록 프로그래밍하면 되겠습니다.

GET 방식으로도 html form 에 입력한 한글을 정상적으로 출력하도록 하는 근본적인 해결책은 찾으시면 제게도 알려주시면 감사하겠습니다.

 

MultiCheck.html / MultiCheck.java

 

HTML FORM 태그의 체크박스를 이용한 문자열 전송

 

파일 위치 : /bbs/example/MultiCheck.html , /bbs/WEB-INF/classes/MultiCheck.java

 

*** MultiCheck.html ***

 

<html><body>

<center><h2>체크박스 다중 선택 테스트</h2></center>

<form method=POST action="../servlet/example.MultiCheck"><br>

다음중 사용중인 소프트웨어를 선택해 주세요<br>

<input type=checkbox name=sw value="jdk1.4">jdk1.4<br>

<input type=checkbox name=sw value="한글">한글<br>

<input type=checkbox name=sw value="MS오피스">MS오피스<br>

<input type=submit value="전송">

<input type=reset value="취소">

</form>

</body></html>

 

*** MultiCheck.java ***

 

package example;

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;

 

public class MultiCheck 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[] values = req.getParameterValues("sw");

                  out.println("<html><body>");

                  out.println("선택한 사용중의 소프트웨어는 아래와 같습니다.");

                  if(values!=null){

                           for(int i=0;i<values.length;i++){

                                   out.print("<li>");

                                   out.print(values[i]);

                                   out.println("<br>");

                           }

                  }

         }

}

 

<설명>

 

1. 웹브라우저를 통해 사용자가 다중선택을 하여 전송하는 데이터의 경우는 HttpServletRequet 의

getParamter() 메소드가 아닌 getParamterValues() 메소드가 쓰입니다.

이 메소드의 리턴값은 사용자가 선택한 값들만으로 구성된 String 배열입니다.

 

SelectItems.html / SelectItems.java

 

HTML FORM 태그의 Select 태그를 이용한 문자열 전송

 

파일 위치 : /bbs/chapter3/SelectItems.html , /bbs/WEB-INF/classes/SelectItems.java

 

*** SelectItems.html ***

 

<HTML><BODY>

<center><h2>SELECT 테스트</h2></center>

<form method=POST action=../servlet/example.SelectItems>

개발환경 운영 체제는?<br>

<select name=os size=3 multiple>

         <option value="윈도우">윈도우</option>

         <option value="리눅스">리눅스</option>

         <option value="솔라리스">솔라리스</option>

         <option value="기타">기타</option>

</select>

<br><br>

사용중인 초고속통신망은?<br>

<select name="초고속통신서비스" multiple>

         <option value="매가패스">매가패스</option>

         <option value="하나포스">하나포스</option>

         <option value="두루넷">두루넷</option>

         <option value="기타">기타</option>

</select>

<input type=submit value="전송">

<input type=reset value="취소">

</form></BODY></HTML>

 

*** SelectItems.java ***

 

package example;

import java.io.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class SelectItems 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");

                  Enumeration e = req.getParameterNames();

                  while(e.hasMoreElements()){

                           String name = (String)e.nextElement();

                           String values[] = req.getParameterValues(name);

                           if(values!=null){

                                   out.println("<li>");

                                   out.println(name + "중 아래 항목을 선택하셨습니다.<br>");

                                   for(int i=0;i<values.length;i++){

                                            out.println(values[i]);

                                            out.println("|");

                                   }

                           }

                           out.println("<br>");

                  }

                  String path = req.getContextPath();

                  out.println("<a href=" + path + "/example/SelectItems.html>뒤로</a>");

         }

}

 

<설명>

 

만약 사용자가 어떤 파라미터명으로 데이터를 보내는지 알수 없다고 가정을 합니다(그럴 경우는 거의 없지만)

이때 사용자가 보내는 파라미터명을 알 수 있는 메소드가 HttpServletRequest getParamterNames()입니다.

getParameterNames() 메소드는 리턴값으로 Enumeration 타입을 반환합니다.

(Enumeration 은 hasMoreElements() , nextElement() 2개의 메소드를 이용할 줄 알면 됩니다.)

 

MultiItems.html / MultiItems.java

 

HTML FORM 태그의 라디오버튼을  이용한 문자열 전송

 

파일 위치 : /bbs/chapter3/MultiItems.html, /bbs/WEB-INF/classes/MultiItems.java

 

*** MultiItems.html ***

 

<HTML><BODY>

<center><h2>라디오 버튼 테스트</h2></center>

<form method=POST action=../servlet/example.MultiItems>

다음 사항을 선택해 주세요<br>

사용중인 운영체제는?<br>

<input type=radio name=os value="windowsXP">windowsXP<br>

<input type=radio name=os value="windowsMe">windowMe<br>

<input type=radio name=os value="window98">windows98<br>

<input type=radio name=os value="redhat90">redhat90<br>

사용중인 컴퓨터 하드웨어는?<br>

<input type=radio name=hw value="intel">intel<br>

<input type=radio name=hw value="amd">amd<br>

<input type=radio name=hw value="기타">기타<br>

사용중인 초고속통신회사는?<br>

<input type=radio name="telecom" value="매가패스">매가패스<br>

<input type=radio name="telecom" value="하나포스">하나포스<br>

<input type=radio name="telecom" value="두루넷">두루넷<br>

<input type=submit value="전송">

<input type=reset value="취소">

</form></BODY></HTML>

 

*** MultiItems.java ***

 

package example;

import java.io.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

 

public class MultiItems 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");

                  Enumeration e = req.getParameterNames();

                  while(e.hasMoreElements()){

                           String name=(String)e.nextElement();

                           String value=req.getParameter(name);

                           out.println("<li>");

                           out.println(name + "|");

                           out.println(value);

                           out.println("<br>");

                  }

         }

}

 

 

RequestDispatcher 사용 예제

 

파일 위치 : /bbs/WEB-INF/classes/IncludingServlet.java , /bbs/WEB-INF/classes/In.java

 

*** IncludingServlet.java ***

 

package example;

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class IncludingServlet extends HttpServlet {

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

                           res.setContentType("text/html;charset=euc-kr");

                           PrintWriter out = res.getWriter();

                           out.println("<html><body>");

                           out.println("<center><h2>Including Servlet</h2></center>");

                           out.println("다음 내용은 다른 서블릿의 내용을 inlcude한 것입니다<hr>");

                           ServletContext sc = getServletContext();

                           RequestDispatcher rd= sc.getRequestDispatcher("/servlet/example.In");

                           rd.include(req,res);

                           out.println("<hr>이곳은 다시 IncludingServlet 입니다.");

                           out.println("</body></html>");

             }

}

 

*** In.java ***

 

package example;

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;

public class In 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("안녕하세요");

                           out.println("IN 서블릿입니다.");

             }

}

 

ServletConfig 의 getInitParameter() 사용 예제

 

파일 위치 : /bbs/WEB-INF/classes/InitParam.java

 

*** InitParam.java ***

 

package example;

import java.io.*;

import javax.servlet.*;

import javax.servlet.http.*;

 

public class InitParam extends HttpServlet {

             String file;

             long  counter;

             public void init() throws ServletException {

                           ServletContext cxt = getServletContext();

                           file = getInitParameter("file");

                           if(file != null) {

                                        try {

                                                     file = cxt.getRealPath(file);

                                                     DataInputStream in = new

                                                                  DataInputStream(new FileInputStream(file));

                                                     counter = in.readLong();

                                                     in.close();

                                        } catch (Exception e) { }

                           }

             }

             public void service(HttpServletRequest req,

             HttpServletResponse res) throws IOException, ServletException {

                           res.setContentType("text/html;charset=euc-kr");

                           PrintWriter out = res.getWriter();

                           out.println("<html><head><title>방문자 수</title></head>");

                           out.println("<body> <center><h2>방문자 수</h2>");

                           out.println("</center><hr>");

                           out.print("file = " + file + "<br>");

                           counter++;

                           out.print(counter);

                           out.println(" 번째 손님입니다.");

             }

public void destroy() {

                           if(file != null) {

                                        try {

                                                     DataOutputStream out = new

                                                                  DataOutputStream(new FileOutputStream(file));

                                                     out.writeLong(counter);

                                                     out.close();

                                        } catch(Exception e) {

                                                     System.out.println(e);

                                        }

                           }           

             }

}

 

위 파일을 실행하기에 앞서 /bbs/WEB-INF/web.xml 파일을 아래를 추가합니다.

<servlet>

        <servlet-name>example.InitParam</servlet-name>

        <servlet-class>example.InitParam</servlet-class>

        <init-param>

               <param-name>file</param-name>

               <param-value>counter.dat</param-value>

        </init-param>

</servlet>

 

(전체 내용에 대해서는 제가 여기까지 설정한 /bbs/WEB-INF/web.xml 파일을 올립니다 첨부파일을 참고하세요)

 

그런 다음 counter.dat 란 파일을 /bbs 에 생성합니다.

다음으로 톰캣을 재시동합니다.

어느정도 InitParam 서블릿을 실행한 다음 InitParam 서블릿을 강제로 내리기 위해서 톰캣을 재시동합니다.

그런 다음 다시 InitParam 서블릿을 요청하여 방문횟수가 저장되었는지 확인합니다.

 

http://localhost/bbs/servlet/InitParam

 

 

서버에서 클라이언트로 이미지 전송시키는데 이용하는 메소드 소개

res.setContentType(“image/gif”); // jpg 이미지는 (“image/jpeg”)

ServletOutputStream out = res.getOutputStream(); // 바이너리 데이터 전송이므로

 

서버에서 클라이언트로 MS오피스 타입의 자료 전송하는데 이용하는 메소드 소개

엑셀의 경우 : res.setContentType(“application/vnd.ms-excel;charset=euc-kr”);

워드의 경우 : res.setContentType(“application/vnd.msword;charset=euc-kr”);

 

파일 업로드을 위한 MultipartRequest 팩키지 사용 방법

MultipartRequest 팩키지는 썬에서 만든 표준 라이브러리가 아니지만 현재 파일 업로드에 널리 이용되고 있는 팩키지입니다.

(파일로 첨부합니다)

 

MultipartRequest 클래스의 생성자 소개

MultipartRequest(HttpServletRequest req, String dir)

dir 디렉토리에 파일을 업로드할 MultipartRequest 객체 생성

MultipartRequest(HttpServletRequest req, String dir, int max)

아규먼트의 max는 업로드 가능한 파일의 최대크기를 나타낸다.

MultipartRequest(HttpServletRequest req, String dir, int max, boolean overwrite, boolean save)

아규먼트의 overwrite는 파일시스템에 동일한 파일이 있으면 덮어쓰는가 여부를 나타낸다.

아규먼트의 save는 파일시스템에 저장하는지 여부를 나타낸다.

 

MultipartRequest 메소드 소개

<input type=file name=pic01> ç 파일찾기로 images.gif 를 업로드했다고 가정하면,

서버에서 MultipartRequest를 사용했다면 아래 메소드를 이용할 수 있습니다.

 

getContentType(“pic01”) : 업로드된 파일의 MIME 타입 리턴 (image/gif)

getFile(“pic01”) : 업로드되어 서버에 저장된 File 객체 리턴

getFileNames() : 업로드된 모든 파일들의 이름을 리턴 (여기서는 Enumeration 에 pic01 저장)

getFilesystemName(“pic01”) : 파일의 파일시스템 이름을 리턴 (images.gif)

 

HttpServletRequest 와 같은 인터페이스 제공하기 위한 메소드를 제공합니다.

getParameter()

getParameterNames()

getParameterValues()

 

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

파일위치 : /bbs/example/upload.html , /bbs/WEB-INF/classes/UploadTest.java

 

*** upload.html ***

 

<HTML><BODY>

<center><h2>파일 업로드</h2></center>

<form action=../servlet/example.UploadTest method=POST ENCTYPE=multipart/form-data>

이름 : <input type=text name=submitter><br>

업로드할 파일 : <input type=file name=file1><br>

업로드할 파일 : <input type=file name=file2><br>

<input type=submit value="전송">

</form>

</BODY></HTML>

 

*** UploadTest.java ***

 

package example;

import java.io.*;

import java.util.*;

import javax.servlet.*;

import javax.servlet.http.*;

import com.oreilly.servlet.MultipartRequest;

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("tmp");

                  try{

                           MultipartRequest multi = new MultipartRequest(req,dir,5*1024*1024);

                           out.println("<html>");

                           out.println("<head><title>파일 업로드</title></head>");

                           out.println("<body>");

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

                           out.println("<h3>Params</h3>");

                           out.println("<pre>");

                           Enumeration params = multi.getParameterNames();

                           while(params.hasMoreElements()){

                                   String name=(String)params.nextElement();

                                   String value=multi.getParameter(name);

                                   out.println(name+"="+value);

                           }

                           out.println("</pre>");

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

                           out.println("</pre>");

                           Enumeration files=multi.getFileNames();

                           while(files.hasMoreElements()){

                                   String name=(String)files.nextElement();

                                   String filename=multi.getFilesystemName(name);

                                   String type=multi.getContentType(name);

                                   File f=multi.getFile(name);

                                   out.println("파라메터 이름 : "  + name + "<br>");

                                   out.println("파일 이름 : " + filename + "<br>");

                                   out.println("파일 타입 : " + type + "<br>");

                                   if(f!=null){

                                            out.println("크기: " + f.length() + "<br>");

                                            out.println("<br>");

                                   }

                           }

                           out.println("</pre>");

                  }catch(Exception e){

                           out.println("<pre>");

                           e.printStackTrace(out);

                           out.println("</pre>");

                  }

                  out.println("</body></html>");

         }

}

 

<설명>

 

1. 테스트는 /bbs/tmp 라는 디렉토리를 만듭니다.

2. 컴파일을 위해 cos.jar 파일을 CLASSPATH에 추가하고 컴파일합니다.

3. cos.jar 파일을 /bbs/WEB-INF/lib 아래 복사 후 톰캣 재가동합니다.

 

2 단계에서 컴파일이 안된다고요?

cos.jar 파일안의 MultiparFormRequest 팩키지에 대한 클래스 패스가 잡히지 않아서입니다.

cos.jar 파일의 클래스 패스를 잡아줍니다.

클래스 패스를 잡기 위해서는 cos.jar 파일을 적당한 위치에 복사한다음 그 경로를 CLASSPATH 에 추가하면 됩니다.

jar 파일이므로 cos.jar 라는 이름까지 추가하셔야 합니다.

제 경우는 F:\javaschool_lib 라는 폴더를 만들고 그 아래에 cos.jar 파일을 복사했습니다.

그리고 F:\javaschool_lib\cos.jar 라는 경로를 CLASSPATH에 추가한다음 컴파일을 했습니다.

 

모두 잘 한것 같은데 파일 업로드를 시도하니 HTTP Status 500 - 에러가 나온다고요?

에러 메시지를 잘 보시면

 

java.lang.NoClassDefFoundError: javax/activation/DataSource

 

위와 같은 메시지가 있습니다. 여기서 힌트를 얻어서 해결책을 찾아보면 ...

JavaBeans(TM) Activation Framework 라는 팩키지가 필요한데 이것에 대한 팩키지는 j2sdk에 들어있지 않습니다.

따라서 http://java.sun.com 에 방문해서 jaf라는 이름으로 검색해서 팩키지를 다운로드 받아야 합니다.

그리고 이 파일을 {JAVA_HOME}/jre/lib/ext 폴더에 복사합니다.

그런 다음 톰캣을 재가동한 후 다운로드를 다시 시도합니다.

꼭 필요한 것 같으나 j2sdk에 포함되지 않은 팩키지가 이것 말고 javamail 팩키지 입니다.

메일과 관련된 프로그램을 위해서도 이 팩키지를 다운로드 받고 압축을 풀고 mai.jar 파일을

{JAVA_HOME}/jre/lib/ext 폴더에 복사하도록 합니다.

 

에러가 없다면 /bbs/tmp 폴더로 가서 확인합니다.

파일명이 바뀌었습니다.

이 팩키지는 MultipartRequest 원본이 아니고 최종명씨가 저자인 "웹 프로그래머를 위한 서블릿/JSP" 란 책에 첨부된 팩키지입니다.

아마도 파일명이 절대로 같게 하지 않게 소스를 변경해서 컴파일한 후 배포한 것으로 생각합니다.

(파일명 뒤에 시스템의 시간이 붙는 것 같습니다)

cos.jar 팩키지에는 소스가 없습니다.

따라서 이것이 마음에 들지 않다면 MultipartRequest 팩키지를 이용하기 위해서는 다른 곳에서 구해야 합니다.

(저는 개인적으로 이게 더 좋습니다.)

 

쿠키

 

HTTP 전송방식의 특징상 각각의 웹 브라우저가 서버와 통신에서 세션을 유지하지 못하는 것을 보완하기 위한 기술입니다.

서버가 쿠키를 전송하면 웹 브라우저는 그 다음 요청마다 쿠키 값이 서버로 전달하여 사용자 정보를 유지할 수 있게 됩니다.

(쇼핑몰의 장바구니, 회원로그인이 필요한 웹 사이트에 쓰임)

 

서버 à 웹 브라우저 (쿠키를 굽는다고 표현되는데 아래와 같은 정보가 클라이언트의 웹 브라우저로 날아가서 셋팅합니다)

 

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

 

웹 브라우저 à 서버 (쿠키가 웹브라우저에 셋팅되면 그 다음부터의 요청시마다 웹 브라우저는 아래와 같은 문자열을 쿠키를 준 서버로 보내게 됩니다. 단 쿠키를 구어준 서버가 아니면 이런 정보를 보내지 않습니다)

 

Cookie ; name = value1 ; name2 = value2 ; …

 

쿠키값이 이름과 값으로 얼마든지 만들 수 있는 것은 아닙니다.

 

쿠키 설정 절차

 

1. Cookie 객체를 만든다. Cookie(String name, String value) 이때 value에 해당하는 값을 인코딩

2. 쿠키에 속성을 부여 : setValue(),setDomain(),setMaxAge(),setPath(),setSecure()

3. 쿠키를 전송 : res.addCookie(cookie);

 

서블릿에서 쿠키 이용

...

Cookie[] cookie = req.getCookie();

String name = cookie[i].getName();

If(name.equals(“id”){

        

}

...

 

String value = cookie[I].getValue();

 

서블릿에서 쿠키 삭제

 

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

...

Cookie name = new Cookie(“name”,””);

name.setMaxAge(0);

res.addCookie(name);

...

 

세션

 

세션은 쿠키 기반 기술로 쿠키의 보안상 약점을 극복하기 위한 기술입니다.

쿠키와 다른 점(즉, 보안상 개선된 점) : 웹브라우저는 서버가 정해준 세션ID만을 쿠키값으로 저장합니다.

세션이 생성되면(즉, 세션ID 쿠키가 구워지면) 세션ID 쿠키만을 서버로 전송하게 되고,

서버에서는 세션ID로 해당 HttpSession 객체를 호출하게 되어(이 작업을 서블릿 컨테이너가 해야 함)

웹브라우저와 세션과의 통신이 이루지게 됩니다.

 

HttpSession 의 메소드 소개

 

setAttribute(String name , Object value)

getAttribute(String name)

removeAttribute(String name)

invalidate();

 

사용법

세션 생성 : HttpSession session = req.getSession(true); //true 일때 세션이 없으면  생성합니다.

HttpSession session = req.getSession(false); // 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 문법으로 넘어갑니다.

<출처: http://cafe.naver.com/webprogramguide/88 >

반응형

Tomcat 설치와 JDBC 연동이 확인됐다면 JSP 프로그래밍을 위한 준비가 되었다고 생각합니다.


다음으로는 톰캣과 웹 애플리케이션에 대해서 간단히 설명을 하겠습니다.

톰캣은 순수 자바로 만든 자바 웹 애플리케이션의 컨테이너라고 표현합니다.

컨테이너란 그릇이란 뜻인데 담는 내용물이 자바 웹 애플리케이션입니다.


자바 웹 애플리케이션을 담는 것뿐만 아니라 웹상에서 클라이언트의 요구에 따라 자바 웹 애플리케이션이 서블릿, JSP 스펙에 따라서

동작하도록 하는 것을 자바 웹 애플리케이션 컨테이너가 하는 일입니다.


JSP 서블릿을 서비스하기 위해서는 컨테이너를 선택하여 서버상에 동작하도록 해야 합니다.

우리는 Tomcat 을 선택했고 지난 글에서 설치를 하였습니다.


자바 웹 애플리케이션에 대해서 좀더 자세하게 살펴봅시다


자바 웹 애플리케이션란?


"자바 웹 애플리케이션은 서블릿, html 페이지, 클래스, 그리고 그 외 자원의 집합인데
이들 자원은 번들화될 수 있고, 이 번들은 다양한 벤더의 다양한 컨테이너에서 작동할 수 있다."

간단하게 정리하면, 웹 애플리케이션은 다음 리스트의 어떠한 결합도 유지할 수 있는 것을 자바 웹 애플리케이션이라고 할 수 있습니다.

* Servlets
* Java Server Pages (JSPs)
* utility classes, JavaBeans
* 정적 엘리먼트 HTML, images 등
* 클라이언트 사이드 classes
* 자바 웹 애플리케이션을 설명하는 메타 정보

또 한가지 중요한 것은, 웹 애플리케이션의 주요 특징 중 하나가 ServletContext 과 웹 애플리케이션과의 관계라는 것입니다.
각각의 웹 애플리케이션은 오직 하나의 ServletContext를 가집니다.
이 관계는 서블릿 컨테이너에 의해 유지되고, ServletContext 안의 객체에 두개의 웹 애플리케이션
이 접근할때 충돌이 일어나지 않게 하는 것을 보장합니다.
참고로 ServletContext는 웹 애플리케이션의 컴포넌트간 공동 저장소로서 주로 이용됩니다.
공동 저장소로서 사용되는 예제는 뒤에서 살보겠습니다.


자바 웹 애플리케이션은 위와 같은 파일들의 모임인데 이 파일들이 위치하는 디렉토리도 일정하게 정해져 있으므로 알맞은 위치에서

두어야 서비스 할 수 있습니다.


자바 웹 애플리케이션의 디렉토리 구조

%% 이클립스에 설정하요 사용시 디렉토리 구조가 다를수 있다. %% 

만약 {Tomcat}/webapps 디렉토리에 웹 애플케이션을 새로 설치한다면 {Tomcat}/webapps 아래에 다음과 같은 디렉토리 구조를 가지게 됩니다.

(우리가 새로 작업할 디렉토리명을 mnd라고 정했다면..)


/mnd
웹 애플리케이션 root 디렉토리입니다 , 모든 JSP, HTML 파일을 여기에 저장합니다.

서브 디렉토리 역시 만들 수 있습니다.


/mnd/WEB-INF
root 디렉토리에 있지 않은 모든 자원을 여기에 위치시킵니다.
여기에는 웹 애플리케이션 배치 정의자 (web.xml)가 위치합니다.
클리이언트가 직접적으로 접근할 수 있는 파일은 이곳에 없습니다.

/mnd/WEB-INF/classes
Servlets 과 사용자 유틸리티 클래스, 자바빈을 이곳에 저장합니다.

/mnd/WEB-INF/lib
웹 애플리케이션이 사용하는 자바 아카이브 파일을 이곳에 저장합니다.
예를들면 JDBC 드라이버나 태그 라이브러리를 구성하는 jar 파일을 이곳에 저장합니다.

웹 애플리케이션 디렉토리에서 주목해야 할 것은
컴파일된 클래스 파일이 위치하는 곳이 두 군데 라는 것입니다.
하나는 /mnd/WEB-INF/classes 이고 다른 하나는 /mnd/WEB-INF/lib 입니다.
만일 똑같은 객체가 이들 두곳에 위치합니다면 어떻게 될까요?
클래스 로더는 먼저 /mnd/WEB-INF/classes 에서 로딩하고 그 다음에 /mnd/WEB-INF/lib 를 로딩하게 됩니다.
따라서 /mnd/WEB-INF/classes 에 있는 클래스가 우선권을 가지고 로딩되고 /lib 에 있는 클래스는 무시된다고 합니다.

하지만 중복되지 않게 하는 것이 더 중요하겠지요.


다음은 /mnd/WEB-INF/ 내에 있는 파일인 web.xml 파일을 좀 더 정리합니다.


Deployment Descriptor (배치자) 설명


웹 애플리케이션의 심장은 Deployment Descriptor (배치자)라고 불리는 web.xml 이라는 이름의 XML파일입니다.
Deployment Descriptor 는 /mnd/WEB-INF/ 디렉토리안에 둡니다.

배치자는 웹 애플리케이션의 모든 설정 정보를 정의합니다.
배치자 파일이 가지고 있는 정보는 다음과 같습니다


* servlet 정의
* servlet 초기 파라미터
* session 설정 파라미터
* servlet/JSP 매핑
* MIME 타입 매핑
* 보안 설정 파라미터
* welcome 파일 리스트
* 에러 페이지 리스트
* 자원과 환경 변수 정의

web.xml 의 예

<web-app>
   <display-name>The Mnd App</display-name>
   <session-timeout>30</session-timeout>
   <servlet>
      <servlet-name>TestServlet</servlet-name>
      <servlet-class>kr.go.mnd.TestServlet</servlet-class>
      <load-on-startup>1</load-on-startup>
      <init-param>
         <param-name>name</param-name>
         <param-name>value</param-name>
      </init-param>
   </servlet>
</web-app>


<display-name>은 단순히 웹 애플리케이션의 이름을 나타냅니다.
이것은 아무런 기능도 하지 않습니다.

<session-timeout> 은 애플리케이션의 HttpSession 객체의 라이프타임을 컨트롤합니다.
위의 <session-timeout> 값은 JSP/servlet 컨테이너에게 HttpSession 객체가 30분 동안 아무런
움직임이 없다면 소멸될 것이라는 것을 알려줍니다.

<servlet>은 servlet과 해당 servlet 의 프로퍼티를 나타냅니다.


web.xml 파일은 간단한 웹 애플리케이션의 경우 생략이 가능합니다.


팩킹 (Packing)

위 디렉토리 구조로 개발이 완료되었다면 웹 애플리케이션을 번들화 하여 다른 웹 애플리케이션 컨테이너에 배포를 할 수 있습니다.


배포를 하기 위해서는 웹 애플리케이션의 루트 디렉토리에서 다음과 같은 명령을 내려 war 확장자를 가진 war 파일을 만듭니다.


jar cvf mnd.war .


그러면 mnd.war 파일이 만들어지는데 이 파일을 다른 톰캣 컨테이너에 배포를 하는 방법은 다른 톰캣 컨테이너의 webapps 디렉토리에 두면 자동으로 서비스를 할 수 있게 톰캣이 알아서 자동으로 우리의 mnd 애플리케이션을 배치합니다.


다른 방법으로는 톰캣의 manager 툴을 이용하는 방법이 있습니다.


이 툴은 톰캣을 설치하면 같이 설치가 되는 툴이며 이것 역시 웹 애플리케이션입니다.

<출처: http://cafe.naver.com/webprogramguide/88 >

반응형

목차

가. JDBC 프로그래밍 환경 셋업
나. JDBC 프로그래밍 방법

가. JDBC 프로그래밍 환경 셋업

(1) 데이터베이스에 맞는 JDBC 드라이버를 구합니다.
Oracle용 JDBC Thin 드라이버는 오라클을 설치하면 [ORACLE_HOME]/jdbc 디렉토리에 자동으로 설치가 됩니다.

보통은 이 파일을 그대로 쓰시면 됩니다.

더 자세한 사항은 http://www.oracle.com/technology/global/kr/index.html 에서 찾으시기 바랍니다.

(2)[ORACLE_HOME]/jdbc 디렉토리에 있는 다음 파일을 CLASSPATH에 추가합니다.

classes12.jar , nls_charset12.jar 파일의 경로를 CLASSPATH에 추가합니다(현재는 . 만 되어 있습니다.)

예).;C:\oracle\product\10.1.0\db_1\jdbc\lib\classes12.jar;C:\oracle\product\10.1.0\db_1\jdbc\lib\nls_charset12.jar

다음에 명령 프롬프트를 새로 띄우고 set classpath 입력합니다.

CLASSPATH가 제대로 잡혔는지 확인합니다.


(3) GetEmp.java 파일로 JDBC 연동 테스트

파일명: GetEmp.java

import java.sql.*;


public class GetEmp {
    public static void main(String[] args) {
        // 위 부분은 설치과정에서 자신이 정한 정보에 맞게 바꾼다.
        String DB_URL = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
        String DB_USER = "scott";
        String DB_PASSWORD = "tiger";
        Connection conn;
        Statement stmt;
        ResultSet rs;
        String query = "select * from emp";

        try {
            // 드라이버를 로딩한다.
            Class.forName("oracle.jdbc.driver.OracleDriver");
            // 데이터베이스의 연결을 설정한다.
            conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
            // 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);

                // 결과를 출력한다.
                System.out.println(
                        empno + " : " + ename + " : " + job + " : " + mgr
                        + " : " + hiredate + " : " + sal + " : " + comm + " : "
                        + depno);
            }
            // ResultSet를 닫는다.
            rs.close();
            // Statement를 닫는다.
            stmt.close();
            // Connection를 닫는다.
            conn.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }// main()의 끝
}// 클래스의 끝

 


컴파일하고 실행시킨 결과 다음의 결과가 나오면 JDBC 프로그래밍 준비 완료가 된 겁니다.

주의할 것은 String DB_URL="jdbc:oracle:thin:@127.0.0.1:1521:orcl";  이 부분입니다.

여기서 orcl 은 SID 명인데 설치시에 입력한 것으로 지정합니다.

> javac GetEmp.java 엔터

> java Getemp 엔터


------------- 실행 결과 -------------
7369 : SMITH : CLERK : 7902 : 1980-12-17 00:00:00.0 : 800 : null : 20
7499 : ALLEN : SALESMAN : 7698 : 1981-02-20 00:00:00.0 : 1600 : 300 : 30
7521 : WARD : SALESMAN : 7698 : 1981-02-22 00:00:00.0 : 1250 : 500 : 30
7934 : MILLER : CLERK : 7782 : 1982-01-23 00:00:00.0 : 1300 : null : 10
..


나. JDBC 프로그래밍 방법

(1) JDBC 드라이버 로딩 :

Class.forName("oracle.jdbc.driver.OracleDriver");


(2) Connection 맺기 :

String url = "jdbc:oracle:thin:@127.0.0.1:1521:orcl";
Connection con = DriverManager.getConnection(url,"scott", "tiger");


(3) SQL 실행


(4) [SQL문이 select문이었다면 ResultSet을 이용한 실행결과 처리]


(5) 자원 반환

위 순서가 JDBC 프로그래밍 방법입니다.

<출처 : http://cafe.naver.com/webprogramguide/86 >

반응형

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이 쉽도록 개선하도록 하겠다.

반응형
JVM GC와 메모리 튜닝




자바스터디 네트워크 [www.javastudy.co.kr]

조대협 [bcho_N_O_SPAM@j2eestudy.co.kr]




모든 Java Application은 JVM(Java Virtual Machine)위에서 동작한다.
이 JVM이 동작하는데 있어서, 메모리의 구조와 특히 GC는 Application의 응답시간과 성능에 밀접한 관계를 미친다. 이번 강좌에서는 JVM 의 메모리 구조와 GC 알고리즘 (JDK 1.4.X에 포함된 새로운 알고리즘 포함) 그리고, JVM의 메모리 튜닝을 통한 Application의 성능향상방법에 대해서 알아보도록 하자.


1.GC란 무엇인가?


GC는 Garbage Collection의 약자로 Java 언어의 중요한 특징중의 하나이다.
GC는 Java Application에서 사용하지 않는 메모리를 자동으로 수거하는 기능을 말한다.
예 전의 전통적인 언어 C등의 경우 malloc, free등을 이용해서 메모리를 할당하고, 일일이 그 메모리를 수거해줘야했다. 그러나 Java 언어에서는 GC 기술을 사용함에 따라서 개발자로 하여금 메모리 관리에서 부터 좀더 자유롭게 해주었다.


2.GC의 동작 방법은 어떻게 되는가?


1) JVM 메모리 영역

GC의 동작 방법을 이해하기 위해서는 Java의 메모리 구조를 먼저 이해할 필요가 있다.
일반적으로 Application에서 사용되는 객체는 오래 유지 되는 객체보다, 생성되고 얼마안있어서 사용되지 않는 경우가 많다. <그림 1 참조>


<그림 1. 메모리 foot print>


그래서 Java에서는 크게 두가지 영역으로 메모리를 나누는데 Young 영역과 Old 영역이 그것이다.
Young 영역은 생긴지 얼마 안된 객체들을 저장하는 장소이고, Old영역은 생성된지 오래된 객체를 저장하는 장소이다. 각 영역의 성격이 다른 만큼 GC의 방법도 다르다.
먼저 Java의 메모리 구조를 살펴보자.


<그림 2. Java 메모리 구조>


Java의 메모리 영역은 앞에서 이야기한 두 영역 (Young 영역,Old 영역)과 Perm 영역 이렇게 3가지로 영역으로 구성된다.


<표 1. Java 메모리 영역>



2) GC 알고리즘

그러면 이 메모리 영역을 JVM이 어떻게 관리하는지에 대해서 알아보자.
JVM 은 New/Young 영역과, Old영역 이 두영역에 대해서만 GC를 수행한다. Perm영역은 앞에서 설명했듯이 Code가 올라가는 부분이기 때문에, GC가 일어날 필요가 없다. Perm영역은 Code가 모두 Load되고 나면 거의 일정한 수치를 유지한다.


○ Minor GC
먼저 New/Young영역의 GC방법을 살펴보자 New/Young 영역의 GC를 Minor GC라고 부르는데, New/Young영역은 Eden과 Survivor라는 두가지 영역으로 또 나뉘어 진다. Eden영역은 Java 객체가 생성되자 마자 저장이 되는곳이다. 이렇게 생성된 객체는 Minor GC가 발생할때 Survivor 영역으로 이동된다.

Survivor 영역은 Survivor 1과 Suvivor2 영역 두 영역으로 나뉘어 지는데, Minor GC가 발생하면 Eden과 Survivor1에 Alive되어 있는 객체를 Suvivor2로 복사한다. 그리고 Alive되어 있지 않는 객체는 자연히 Suvivor1에 남아있게 되고, Survivor1과 Eden영역을 Clear한다. (결과적으로 Alive된 객체만 Survivor2로 이동한것이다.)
다음번 Minor GC가 발생하면 같은 원리로 Eden과 Survivor2영역에서 Alive되어 있는 객체를 Survivor1에 복사한다. 계속 이런 방법을 반복적으로 수행하면서 Minor GC를 수행한다.

이렇게 Minor GC를 수행하다가, Survivor영역에서 오래된 객체는 Old영역으로 옮기게 된다.

이 런 방식의 GC 알고리즘을 Copy & Scavenge라고 한다. 이 방법은 매우 속도가 빠르며 작은 크기의 메모리를 Collecting하는데 매우 효과적이다. Minor GC의 경우에는 자주 일어나기 때문에, GC에 소요되는 시간이 짧은 알고리즘이 적합하다.

이 내용을 그림을 보면서 살펴보도록 하자.


<그림 3-1. 1st Minor GC>


Eden에서 Alive된 객체를 Suvivor1으로 이동한다. Eden 영역을 Clear한다.


<그림 3-2. 2nd Minor GC>


Eden영역에 Alive된 객체와 Suvivor1영역에 Alive된 객체를 Survivor 2에 copy한다.
Eden영역과 Suvivor2영역을 clear한다.


<그림 3-3. 3rd Minor GC>


객체가 생성된 시간이 오래지나면 Eden과 Suvivor영역에 있는 오래된 객체들을 Old 영역으로 이동한다.


○ Full GC

Old 영역의 Garbage Collection을 Full GC라고 부르며, Full GC에 사용되는 알고리즘은 Mark & Compact라는 알고리즘을 이용한다. Mark & Compact 알고리즘은 전체 객체들의 reference를 쭉 따라가다면서 reference가 연결되지 않는 객체를 Mark한다. 이 작업이 끝나면 사용되지 않는 객체를 모두 Mark가 되고, 이 mark된 객체를 삭제한다.<그림 4 참고> (실제로는 compact라고 해서, mark된 객체로 생기는 부분을 unmark된 즉 사용하는 객체로 메꾸어 버리는 방법이다.)

Full GC는 매우 속도가 느리며, Full GC가 일어나는 도중에는 순간적으로 Java Application이 멈춰 버리기 때문에, Full GC가 일어나는 정도와 Full GC에 소요되는 시간은 Application의 성능과 안정성에 아주 큰 영향을 준다.


<그림 4. Full GC>




3. GC가 왜 중요한가?


Garbage Collection중에서 Minor GC의 경우 보통 0.5초 이내에 끝나기 때문에 큰문제가 되지 않는다. 그러나 Full GC의 경우 보통 수초가 소요가 되고, Full GC동안에는 Java Application이 멈춰버리기 때문에 문제가 될 수 있다.
예를 들어 게임 서버와 같은 Real Time Server를 구현을 했을때, Full GC가 일어나서 5초동안 시스템이 멈춘다고 생각해보자.
또 일반 WAS에서도 5~10초동안 멈추면, 멈추는동안의 사용자의 Request가 Queue에 저장되었다가 Full GC가 끝난후에 그 요청이 한꺼번에 들어오게 되면 과부하에 의한 여러 장애를 만들 수 있다..
그래서 원할한 서비스를 위해서는 GC를 어떻게 일어나게 하느냐가 시스템의 안정성과 성능에 큰 변수로 작용할 수 있다.


4. 다양한 GC 알고리즘


앞 에서 설명한 기본적인 GC방법 (Scavenge 와 Mark and compact)이외에 JVM에서는 좀더 다양한 GC 방법을 제공하고 그 동작방법이나 사용방법도 틀리다. 이번에는 다양한 GC 알고리즘에 대해서 알아보자. 현재 (JDK 1.4)까지 나와 있는 JVM의 GC방법은 크게 아래 4가지를 지원하고 있다.

- Default Collector
- Parallel GC for young generation (from JDK 1.4 )
- Concurrent GC for old generation (from JDK 1.4)
- Incremental GC (Train GC)

1) Default Collector
이 GC 방법은 앞에서 설명한 전통적인 GC방법으로 Minor GC에 Scavenge를, Full GC에 Mark & compact 알고리즘을 사용하는 방법이다. 이 알고리즘에는 이미 앞에서 설명했기 때문에 별도의 설명을 하지는 않는다.

JDK 1.4에서부터 새로 적용되는 GC방법은 Parallel GC와 Concurrent GC 두가지 방법이 있다. Parallel GC는 Minor GC를 좀더 빨리하게 하는 방법이고 (Throughput 위주) Concurrent GC는 Full GC시에 시스템의 멈춤(Pause)현상을 최소화하는 GC방법이다.

2) Parallel GC
JDK1.3까지 GC는 하나의 Thread에서 이루어진다. Java가 Multi Thread환경을 지원함에도 불구하고, 1 CPU에서는 동시에 하나의 Thread만을 수행할 수 밖에 없기때문에, 예전에는 하나의 CPU에서만 GC를 수행했지만, 근래에 들어서 하나의 CPU에서 동시에 여러개의 Thread를 실행할 수 있는 Hyper Threading기술이나, 여러개의 CPU를 동시에 장착한 HW의 보급으로 하나의 HW Box에서 동시에 여러개의 Thread를 수행할 수 있게 되었다.

JDK 1.4부터 지원되는 Parallel GC는 Minor GC를 동시에 여러개의 Thread를 이용해서 GC를 수행하는 방법으로 하나의 Thread를 이용하는것보다 훨씬 빨리 GC를 수행할 수 있다.


<그림 7. Parallel GC 개념도>


< 그림 7> 을 보자 왼쪽의 Default GC방법은 GC가 일어날때 Thread들이 작업을 멈추고, GC를 수행하는 thread만 gc를 수행한다. (그림에서 파란영역), Parallel GC에서는 여러 thread들이 gc를 수행이 가능하기 때문에, gc에 소요되는 시간이 낮아진다.

Parallel GC가 언제나 유익한것은 아니다. 앞에서도 말했듯이 1CPU에서는 동시에 여러개의 thread를 실행할 수 없기 때문에 오히혀 Parallel GC가 Default GC에 비해서 느리다. 2 CPU에서도 Multi thread에 대한 지원이나 계산등을 위해서 CPU Power가 사용되기 때문에, 최소한 4CPU의 256M 정도의 메모리를 가지고 있는 HW에서 Parallel GC가 유용하게 사용된다.

Parallel GC는 크게 두가지 종류의 옵션을 가지고 있는데,Low-pause 방식과 Throughput 방식의 GC방식이 있다.

Solaris 기준에서 Low-pause Parallel GC는 ?XX:+UseParNewGC 옵션을 사용한다. 이 모델은 Old 영역을 GC할때 다음에 설명할 Concurrent GC방법과 함께 사용할 수 있다. 이 방법은 GC가 일어날때 빨리 GC하는것이 아니라 GC가 발생할때 Application이 멈춰지는 현상(pause)를 최소화하는데 역점을 뒀다.

Throughput 방식의 Parallel GC는 ?XX:+UseParallelGC (Solaris 기준) 옵션을 이용하며 Old 영역을 GC할때는 Default GC (Mark and compact)방법만을 사용하도록 되어 있다.Minor GC가 발생했을때, 되도록이면 빨리 수행하도록 throughput에 역점을 두었다.

그외에도 ParallelGC를 수행할때 동시에 몇개의 Thread를 이용하여 Minor영역을 Parallel GC할지를 결정할 수 있는데, -XX:ParallelGCThreads= 옵션을 이용하여 Parallel GC에 사용되는 Thread의 수를 지정할 수 있다.

3) Concurrent GC

앞에서도 설명했듯이, Full GC즉 Old 영역을 GC하는 경우에는 그 시간이 길고 Application이 순간적으로 멈춰버리기 때문에, 시스템 운용에 문제가 된다.

그래서 JDK1.4부터 제공하는 Concurrent GC는 기존의 이런 Full GC의 단점을 보완하기 위해서 Full GC에 의해서 Application이 멈추어 지는 현상을 최소화 하기 위한 GC방법이다.
Full GC에 소요되는 작업을 Application을 멈추고 진행하는것이 아니라, 일부는 Application이 돌아가는 단계에서 수행하고, 최소한의 작업만을 Application이 멈췄을때 수행하는 방법으로 Application이 멈추는 시간을 최소화한다.


<그림 8. Concurrent GC 개념도>


그 림 8에서와 같이 Application이 수행중일때(붉은 라인) Full GC를 위한 작업을 수행한다. (Sweep,mark) Application을 멈추고 수행하는 작업은 일부분 (initial-mark, remark 작업)만을 수행하기 때문에, 기존 Default GC의 Mark & Sweep Collector에 비해서 Application이 멈추는 시간이 현저하게 줄어든다.

Solaris JVM에서는 -XX:+UseConcMarkSweepGC Parameter를 이용해 세팅한다.

4) Incremental GC (Train GC)

Incremental GC또는 Train GC라고도 불리는 GC방법은 JDK 1.3에서부터 지원된 GC방법이다. 앞에서 설명한 Concurrent GC와 비슷하게, 의도 자체는 Full GC에 의해서 Application이 멈추는 시간을 줄이고자 하는데 있다.

Incremental GC의 작동방법은 간단하다. Minor GC가 일어날때 마다 Old영역을 조금씩 GC를 해서 Full GC가 발생하는 횟수나 시간을 줄이는 방법이다.


<그림 9. Incremental GC 개념도>


그 림 9에서 보듯이. 왼쪽의 Default GC는 FullGC가 일어난후에나 Old 영역이 Clear된다. 그러나, 오른쪽의 Incremental GC를 보면 Minor GC가 일어난후에, Old 영역이 일부 Collect된것을 볼 수 있다.

Incremental GC를 사용하는 방법은 JVM 옵션에 ?Xinc 옵션을 사용하면 된다.
Incremental GC는 많은 자원을 소모하고, Minor GC를 자주일으키고, 그리고 Incremental GC를 사용한다고 Full GC가 없어지거나 그 횟수가 획기적으로 줄어드는 것은 아니다. 오히려 느려지는 경우가 많다. 필히 테스트 후에 사용하도록 하자.

※ Default GC이외의 알고리즘은 Application의 형태나 HW Spec(CPU수, Hyper threading 지원 여부), 그리고 JVM 버전(JDK 1.4.1이냐 1.4.2냐)에 따라서 차이가 매우 크다. 이론상으로는 실제로 성능이 좋아보일 수 있으나, 운영환경에서는 여러 요인으로 인해서 기대했던것만큼의 성능이 안나올 수 있기 때문에, 실환경에서 미리 충분한 테스트를 거쳐서 검증한후에 사용해야 한다.


5. GC 로그는 어떻게 수집과 분석


JVM에서는 GC 상황에 대한 로그를 남기기 위해서 옵션을 제공하고 있다.
Java 옵션에 ?verbosegc 라는 옵션을 주면되고 HP Unix의 경우 ?verbosegc ?Xverbosegc 옵션을 주면 좀더 자세한 GC정보를 얻을 수 있다. GC 정보는 stdout으로 출력이 되기 때문에 “>” redirection등을 이용해서 file에 저장해놓고 분석할 수 있다.

Example ) java ?verbosegc MyApplication

그럼 실제로 나온 GC로그를 어떻게 보는지를 알아보자.


<그림 5. 일반적인 GC 로그, Windows, Solaris>


<그림 5>는 GC로그 결과를 모아논 내용이다. (실제로는 Application의 stdout으로 출력되는 내용과 섞여서 출력된다.)
Minor GC는 ”[GC “로 표기되고, Full GC는 “[Full GC”로 표기된다.
그 다음값은 Heap size before GC인데,GC 전에 Heap 사용량 ( New/Young 영역 + Old 영역 + Perm 영역)의 크기를 나타낸다.

Heap size after GC는 GC가 발생한후에 Heap의 사용량이다. Minor GC가 발생했을때는 Eden과 Survivor 영역으 GC가 됨으로 Heap size after GC는 Old영역의 용량과 유사하다.(Minor GC에서 GC되지 않은 하나의 Survivor영역내의 Object들의 크기도 포함해야한다.)

Total Heap Size는 현재 JVM이 사용하는 Heap Memory양이다. 이 크기는 Java에서 ?ms와 ?mx 옵션으로 조정이 가능한데. 예를 들어 ?ms512m ?mx1024m로 해놓으면 Java Heap은 메모리 사용량에 따라서 512~1024m사이의 크기에서 적절하게 늘었다 줄었다한다. (이 늘어나는 기준과 줄어드는 기준은 (-XX:MaxHeapFreeRatio와 ?XX:MinHeapFreeRation를 이용해서 조정할 수 있으나 JVM vendor에 따라서 차이가 나기때문에 각 vendor별 JVM 메뉴얼을 참고하기 바란다.) Parameter에 대한 이야기는 추후에 좀더 자세히하도록 하자.

그 다음값은 GC에 소요된 시간이다.

< 그림 5>의 GC로그를 보면 Minor GC가 일어날때마다 약 20,000K 정도의 Collection이 일어난다. Minor GC는 Eden과 Suvivor영역 하나를 GC하는 것이기 때문에 New/Young 영역을 20,000Kbyte 정도로 생각할 수 있다.

Full GC때를 보면 약44,000Kbyte에서 1,749Kbyte로 GC가 되었음을 볼 수 있다. Old영역에 큰 데이타가 많지 않은 경우이다. Data를 많이 사용하는 Application의 경우 전체 Heap이 512이라고 가정할때, Full GC후에도 480M정도로 유지되는 경우가 있다. 이런 경우에는 실제로 Application에서 Memory를 많이 사용하고 있다고 판단할 수 있기 때문에 전체 Heap Size를 늘려줄 필요가 있다.

이렇게 수집된 GC로그는 다소 보기가 어렵기 때문에, 좀더 쉽게 분석할 수 있게 하기 위해서 GC로그를 awk 스크립트를 이용해서 정제하면 분석이 용이하다.


<표 2. gc.awk 스크립트>


이 스크립트를 작성한후에 Unix의 awk 명령을 이용해서

% awk ?f gc.awk GC로그파일명

을 쳐주면 아래<표 3>와 같이 정리된 형태로 GC 로그만 추출하여 보여준다.


<표 3. gc.awk 스크립트에 의해서 정재된 로그>


Minor와 Major는 각각 Minor GC와 Full GC가 일어날때 소요된 시간을 나타내며, Alive는 GC후에 남아있는 메모리양, 그리고 Freed는 GC에 의해서 collect된 메모리 양이다.

이 로그파일은 excel등을 이용하여 그래프등으로 변환해서 보면 좀더 다각적인 분석이 가능해진다.

※ JDK 1.4에서부터는 ?XX:+PrintGCDetails 옵션이 추가되어서 좀더 자세한 GC정보를 수집할 수 있다.


※ HP JVM의 GC Log 수집

HP JVM은 전체 heap 뿐 아니라 ?Xverbosegc 옵션을 통해서 Perm,Eden,Old등의 모든 영역에 대한 GC정보를 좀더 정확하게 수집할 수 있다.

Example ) java ?verbosegc ?Xverbosegc MyApplication ß (HP JVM Only)

HP JVM의 GC정보는 18개의 필드를 제공하는데 그 내용을 정리해보면 <표 4.>와 같다.

<GC : %1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12 %13 %14 %15 %16 %17 %18>


<표 4. HP JVM GC 로그 필드별 의미>


이 로그를 직접 보면서 분석하기는 쉽지가 않다. 그래서, HP에서는 좀더 Visual한 환경에서 분석이 가능하도록 HPJtune이라는 툴을 제공한다. 다음 URL에서 다운로드 받을 수 있다.

http://www.hp.com/products1/unix/java/java2/hpjtune/index.html


<그림 6. HP Jtune을 이용해서 GC후 Old영역의 변화 추이를 모니터링하는 화면>




6. GC 관련 Parameter


GC 관련 설정값을 보기전에 앞서서 ?X와 ?XX 옵션에 대해서 먼저 언급하자. 이 옵션들은 표준 옵션이 아니라, 벤더별 JVM에서 따로 제공하는 옵션이기 때문에, 예고 없이 변경되거나 없어질 수 있기 때문에, 사용전에 미리 JVM 벤더 홈페이지를 통해서 검증한다음에 사용해야한다.

1) 전체 Heap Size 조정 옵션

전체 Heap size는 ?ms와 ?mx로 Heap 사이즈의 영역을 조정할 수 있다. 예를 들어 ?ms512m ?mx 1024m로 설정하면 JVM은 전체 Heap size를 application의 상황에 따라서 512m~1024m byte 사이에서 사용하게 된다. 그림2의 Total heap size

메모리가 모자를때는 heap을 늘리고, 남을때는 heap을 줄이는 heap growing과 shirinking 작업을 수행하는데, 메모리 변화량이 큰 애플리케이션이 아니라면 이 min heap size와 max heap size는 동일하게 설정하는 것이 좋다. 일반적으로 1GB까지의 Heap을 설정하는데에는 문제가 없으나, 1GB가 넘는 대용량 메모리를 설정하고자 할 경우에는 별도의 JVM 옵션이 필요한 경우가 있기때문에 미리 자료를 참고할 필요가 있다.

※ IBM AIX JVM의 경우
%export LDR_CNTRL=MAXDATA=0x10000000
%java -Xms1500m -Xmx1500m MyApplication

2) Perm size 조정 옵션

Perm Size는 앞에서도 설명했듯이, Java Application 자체(Java class etc..)가 로딩되는 영역이다. J2EE application의 경우에는 application 자체의 크기가 큰 편에 속하기 때문에, Default로 설정된 Perm Size로는 application class가 loading되기에 모자른 경우가 대부분이기 때문에, WAS start초기나, 가동 초기에 Out Of Memory 에러를 유발하는 경우가 많다.

PermSize는 -XX:MaxPermSize=128m 식으로 지정할 수 있다.
일반적으로 WAS에서 PermSize는 64~256m 사이가 적절하다.

3) New 영역과 Old 영역의 조정New 영역은 ?XX:NewRatio=2 에 의해서 조정이 된다.
NewRatio Old/New Size의 값이다. 전체 Heap Size가 768일때, NewRatio=2이면 New영역이 256m, Old 영역이 512m 로 설정이 된다.
JVM 1.4.X에서는 ?XX:NewSize=128m 옵션을 이용해서 직접 New 영역의 크기를 지정하는 것이 가능하다.

4) Survivor 영역 조정 옵션
-XX:SurvivorRatio=64 (eden/survivor 의 비율) :64이면 eden 이 128m일때, survivor영역은 2m가 된다.

5) -server와 ?client 옵션
JVM에는 일반적으로 server와 client 두가지 옵션을 제공한다.
결 론만 말하면 server 옵션은 WAS와 같은 Server환경에 최적화된 옵션이고, client옵션은 워드프로세서와 같은 client application에 최적화된 옵션이다. 그냥 언뜻 보기에는 단순한 옵션 하나로보일 수 있지만, 내부에서 돌아가는 hotspot compiler에 대한 최적화 방법과 메모리 구조자체가 아예 틀리다.

○ -server 옵션

server 용 application에 최적화된 옵션이다. Server application은 boot up 시간 보다는 user에 대한 response time이 중요하고, 많은 사용자가 동시에 사용하기 때문에 session등의 user data를 다루는게 일반적이다. 그래서 server 옵션으로 제공되는 hotspot compiler는 java application을 최적화 해서 빠른 response time을 내는데 집중되어 있다.

또한 메모리 모델 역시, 서버의 경우에는 특정 사용자가 서버 운영시간동안 계속 서버를 사용하는게 아니기 때문에 (Login하고, 사용한 후에는 Logout되기 때문에..) 사용자에 관련된 객체들이 오래 지속되는 경우가 드물다. 그래서 상대적으로 Old영역이 작고 New 영역이 크게 배정된다. <그림 7. 참조 >

○ -client 옵션

client application은 워드프로세서 처럼 혼자 사용하는 application이다. 그래서 client application은 response time보다는 빨리 기동되는데에 최적화가 되어 있다. 또한대부분의 client application을 구성하는 object는GUI Component와 같이 application이 종료될때까지 남아있는 object의 비중이 높기 때문에 상대적으로 Old 영역의 비율이 높다.


<그림 7. ?server와 ?client 옵션에 따른 JVM Old와 New영역>


이 두옵션은 가장 간단한 옵션이지만, JVM의 최적화에 아주 큰부분을 차지하고 있는 옵션이기 때문에, 반드시 Application의 성격에 맞춰서 적용하기 바란다.
(※ 참고로, SUN JVM은 default가 client, HPJVM는 default가 server로 세팅되어 있다.)

○ GC 방식에 대한 옵션

GC 방식에 대한 옵션은 앞에서도 설명했지만, 일반적인 GC방식이외에, Concurrent GC,Parallel GC,Inceremental GC와 같이 추가적인 GC Algorithm이 존재한다. 옵션과 내용은 앞장에서 설명한 “다양한 GC알고리즘” 을 참고하기 바란다.


7.JVM GC 튜닝


그러면 이제부터 지금까지 설명한 내용을 기반으로 실제로 JVM 튜닝을 어떻게 하는지 알아보도록 하자.

STEP 1. Application의 종류와 튜닝목표값을 결정한다.

JVM 튜닝을 하기위해서 가장 중요한것은 JVM 튜닝의 목표를 설정하는것이다. 메모리를 적게 쓰는것이 목표인지, GC 횟수를 줄이는것이 목표인지, GC에 소요되는시간이 목표인지, Application의 성능(Throughput or response time) 향상인지를 먼저 정의한후에. 그 목표치에 근접하도록 JVM Parameter를 조정하는것이 필요하다.

STEP 2. Heap size와 Perm size를 설정한다.

-ms 와 ?mx 옵션을 이용해서 Heap Size를 정한다. 일반적으로 server application인 경우에는 ms와 mx 사이즈를 같게 하는것이 Memory의 growing과 shrinking에 의한 불필요한 로드를 막을 수 있어서 권장할만하다.

ms와mx사이즈를 다르게 하는 경우는 Application의 시간대별 memory 사용량이 급격하게 변화가 있는 Application에 효과적이다.
PermSize 는 JVM vendor에 따라 다소 차이가 있으나 일반적으로 16m정도이다. Client application의 경우에는 문제가 없을 수 있지만, J2EE Server Application의 경우 64~128m 사이로 사용이 된다.

Heap Size와 Perm Size는 아래 과정을 통해서 적정 수치를 얻어가야한다.

STEP 3. 테스트 & 로그 분석.

JVM Option에 GC 로그를 수집하기 위한 ?verbosegc 옵션을 적용한다. (HP의 경우 ?Xverbosegc 옵션을 적용한다.)

LoadRunner 나 MS Strest(무료로 MS社의 홈페이지에서 다운로드 받을 수 있다.)와 같은 Strest Test툴을 통해서 Application에 Strest를 줘서. 그 log를 수집한다. 튜닝에서 있어서 가장 중요한것은 목표산정이지만, 그만큼이나 중요한것은 실제 Tuning한 Parameter가 Application에 어떤 영향을 주는지를 테스트하는 방법이 매우 중요하다. 그런 의미에서 적절한 Strest Tool의 선정과, Strest Test 시나리오는 정확한 Tuning을 위해서 매우 중요한 요인이다.

○ Perm size 조정
아래 그림8.은 HP JVM에서 ?Xverbosegc 옵션으로 수집한 GC log를 HP Jtune을 통해서 graph로 나타낸 그래프이다. 그림을 보면 Application이 startup되었을때 Perm 영역이 40m에서. 시간이 지난후에도 50m 이하로 유지되는것을 볼 수 있다. 특별하게 동적 classloading등이 수십m byte가 일어나지 않는등의 큰 변화요인이 없을때, 이 application의 적정 Perm 영역은 64m로 판단할 수 있다.


<그림 8. GC 결과중 Perm 영역 그래프>


○ GC Time 수행 시간 분석

다음은 GC에 걸린 시간을 분석해보자. 앞에 강좌 내용에서도 설명햇듯이. GC Tuning에서 중요한 부분중 하나가 GC에 소요되는 시간 특히 Full GC 시간이다.

지금부터 볼 Log는 모社의 물류 시스템의 WAS 시스템 GC Log이다. HP JVM을 사용하며, -server ?ms512m ?mx512m 옵션으로 기동되는 시스템이다.

그림 9를 보면 Peak 시간 (첫번째 동그라미) 14시간동안에 Full GC(동그란점)가 7번일어난것을 볼 수 있다. 각각에 걸린 시간은2.5~6sec 사이이다.
여기서 STEP 1.에서 설정한 AP Tuning의 목표치를 참고해야하는데.

Full GC가 길게 일어나서 Full GC에 수행되는 시간을 줄이고자 한다면 Old 영역을 줄이면 Full GC가 일어나는 횟수는 늘어나고, 반대로 Full GC가 일어나는 시간을 줄어들것이다.

반대로 Full GC가 일어나는 횟수가 많다면, Old 영역을 늘려주면 Full GC가 일어나는 횟수는 상대적으로 줄어들것이고 반대로 Full GC 수행시간이 늘어날 것이다.

특 히 Server Application의 경우Full GC가 일어날때는 JVM자체가 멈춰버리기 때문에, 그림 9의 instance는 14시간동안 총 7번 시스템이 멈추고, 그때마다 2.5~6sec가량 시스템이 response를 못하는 상태가 된것이다. 그래서 멈춘 시간이 고객이 납득할만한 시간인지를 판단해야 하고, 거기에 적절한 Tuning을 해야한다.

Server Application에서 Full GC를 적게일어나게하고, Full GC 시간을 양쪽다 줄이기 위해서는 Old영역을 적게한후에, 여러개의 Instance를 동시에 뛰어서 Load Balancing을 해주면, Load가 분산되기 때문에 Full GC가 일어나는 횟수가 줄어들테고, Old 영역을 줄였기 때문에, Full GC에 드는 시간도 줄어들것이다. 또한 각각의 FullGC가 일어나는동안 하나의 서버 instance가 멈춰져 있어도, Load Balancing이 되는 다른 서버가 response를 하고 있기때문에, Full GC로 인한 Application이 멈추는것에 의한 영향을 최소화할 수 있다.


<그림 9. GC 소요시간>


데이타에 따라서 GC Tuning을 진행한후에는 다시 Strest Test를 진행해서 응답시간과 TPS(Throughput Per Second)를 체크해서 어떤 변화를 주었는지를 반드시 체크해봐야한다.


<그림 10. GC후의 Old 영역>


그림 10은 GC후에 Old 영역의 메모리 변화량을 나타낸다.

금 요일 업무시간에 메모리 사용량이 올라가다가. 주말에가서 완만한 곡선을 그리는것을 볼 수 있다. 월요일 근무시간에 메모리 사용량이 매우 많고, 화요일에도 어느정도 메모리 사용량이 있는것을 볼 수 있다. 월요일에 메모리 사용량이 많은것을 볼때, 이 시스템의 사용자들이 월요일에 시스템 사용량이 많을 수 있다고 생각할 수 있고, 또는 다른 주의 로그를 분석해봤을때 이 주만 월요일 사용량이 많았다면, 특별한 요인이나 Application 변경등이 있었는지를 고려해봐야할것이다.

이 그래프만을 봤을때 Full GC가 일어난후에도 월요일 근무시간을 보면 Old 영역이 180M를 유지하고 있는것을 볼 수 있다. 이 시스템의 Full GC후의 Old영역은 80M~180M를 유지하는것을 볼 수 있다. 그래서 이 시스템은 최소 180M이상의 Old 영역을 필요로하는것으로 판단할 수 있다.

STEP 4. Parameter 변경.
STEP 3에서 구한 각 영역의 허용 범위를 기준으로 Old영역과 New 영역을 적절하게 조절한다.
PermSize와 New영역의 배분 (Eden,Survivor)영역등을 조정한다.
PermSize 는 대부분 Log에서 명확하게 나타나기 때문에, 크게 조정이 필요가 없고 New영역내의 Eden과 Survivor는 거의 조정하지 않는다. 가장 중요한것은 Old영역과 New 영역의 비율을 어떻게 조정하는가가 관건이다.

이 비율을 결정하면서, STEP1에서 세운 튜닝 목표에 따라서 JVM의 GC Algorithm을 적용한다. GC Algorithm을 결정하는 기본적인 판단 내용은 아래와 같다.



이렇게 Parameter를 변경하면서 테스트를 진행하고, 다시 변경하고 테스트를 진행하는 과정을 거쳐서 최적의 Parameter와 GC Algorithm을 찾아내는것이 JVM의 메모리 튜닝의 이상적인 절차이다.


지금까지 JVM의 메모리 구조와 GC 모델 그리고 GC 튜닝에 대해서 알아보았다.

정 리하자면 GC 튜닝은 Application의 구조나 성격 그리고, 사용자의 이용 Pattern에 따라서 크게 좌우 되기때문에, 얼마만큼의 Parameter를 많이 아느냐 보다는 얼마만큼의 테스트와 로그를 통해서 목표 값에 접근하느냐가 가장 중요하다.


출처 : http://www.j2eestudy.co.kr/lecture_data/lecture0401_1_1_index.html
반응형

(1) [2006-12-27] 특집5부_ARM과 파워PC에 기반한 임베디드 프로그래밍 최적화 기법
(2) [2006-12-27] 특집4부_성능 이슈 해결을 위한 닷넷 프로그래밍 최적화 기법
(3) [2006-12-27] 특집3부_리팩토링을 이용한 자바 성능 최적화 기법
(4) [2006-12-26] 특집2부_OOP적 개발을 위한 C++ 프로그래밍 최적화 기법
(5) [2006-12-26] 특집1부_개발 환경의 변화와 대응하는 프로그래밍 최적화의 재발견

출처: http://www.dbguide.net/know/know101003.jsp?IDX=1164&K=TITLE&K2=REGID&V=¨?¨????%20?????¡?&catenum=14

분야별 특성에 맞춘 Programming Optimization



프로그래밍 최적화. 코드 몇 줄을 줄이고 실행 속도를 높이기 위해 머리를 쥐어짜던 시절이 있었다. 이미 추억 저편으로 멀어진 그 기억 속에서는 그런 것이 바로 프로그래밍 최적화였다. 그럼 하드웨어의 성능이 예전의 슈퍼컴퓨터와 맞먹을 정도로 높아진 지금, 프로그래밍 최적화가 대체 무슨 의미를 가질 수 있을까? 프로그램의 속도가 조금 빠르거나 느린 정도라면 이제 거의 체감할 수 없는 상황이 되지 않았는가. 성능이 아주 떨어지지만 않는다면 이제 약간의 실행 속도의 차이는 무의미해진 지 오래다. 이처럼 시대가 변했다고는 하지만 분명히 프로그래밍을 위해 갖춰야 할 최적화 항목들은 여전히 존재한다. 코드의 수를 줄이는 것이 아니더라도 OOP적 개발을 위한 기법들이 필요하고, 보다 개선된 프로그램을 위한 리팩토링도 필요하다. 심지어 예전처럼 속도를 따져야 하는 분야도 있다. 바로 임베디드 분야이다. 이번 특집에서는 각 개발 분야에서 중요하게 다뤄져야할 최적화 기법들에 대해 알아본다.


기획·정리
| 정희용 기자 flytgr@imaso.co.kr


리팩토링을 이용한 자바 성능 최적화 기법


허광남 | GS홈쇼핑 EC정보팀 과장


리팩토링, 복잡다단해지는 현대의 소프트웨어 개발에서 이 단어는 점점 중요한 위치를 차지해 가고 있다. 이제 리팩토링은 진정한 개발자의 덕목 중에 하나라고 단언할 수 있을 정도다. 리팩토링을 한다는 것은 개선에 대한 의지가 있음을 뜻하고, 좀 더 나은 코드, 구조, 프로세스를 지향한다는 의미가 된다. 리팩토링으로 소프트웨어의 성능을 직접적으로 높이지는 못 한다. 하지만 코드의 가독성을 증대시켜, 생각하는 프로그래머들의 머릿속 성능을 높여준다. 3부에서는 리팩토링 방법들에 대해 알아본다.


햄버거나 커피 등을 살 때, 또는 백화점이나 편의점에서 물건을 살 때, 우리는 1회용 물건을 쓰는 것에 대한 세금을 낸다. 1회용 물건을 쓰면 환경이 그만큼 빨리 피폐해지기 때문이란다. 그게 사실인지 아닌지 모르겠지만, 내 돈이 나가는 것은 용납이 안 된다. 1회용품의 편리함. 그 반대급부로 만들어지는 쓰레기 처리에 따른 비용을 지불한다고 하는데, 영 맘에 안 든다.

혹시 프로그램을 짤 때도 1회용 프로그램을 짠다는 생각을 해본 적이 있는가? 그런데 우리는 1회용 프로그램을 짜도 세금을 내지 않는다. 다행일까? 1회용 프로그램이 환경 자원을 소모시키지는 않는다. 다만 1회용 프로그램은 쓰레기를 양산한다. 그때 그때 필요한대로 찍어낸 프로그램은 수많은 중복코드를 양산해낸다. 재활용하지 않는 습관 탓에 시스템이라는 환경이 무거워지고 손이 많이 가도록 바뀌는 것이다.

재활용성은 객체지향 프로그램의 핵심원리 중의 하나이다. 재활용성을 높인다는 것은 찍어낼 때 사용하는 템플릿을 얘기하는 것이 아니다. 오히려 업무나 기능을 제어 가능한 곳에 집약시켜서 관리할 수 있도록 시스템 전체의 청결한 상태를 유지하는 것이다. 이리저리 산재된 중복 코드를 정리하는 것이 핵심이다. 이 때 필요한 기술이 리팩토링이다. 이쯤 얘기하면 리팩토링은 정리 정돈에 비견된다. 군대에서 총기수입을 하는 것과도 같고 집에서 설거지를 하는 것과도 같다.




리팩토링이란



Refactoring (Re + Factor + ing) 영어 단어를 요소별로 나눠보면 요소들을 재구성한다는 뉘앙스를 받을 수 있다. 이는 마틴 파울러의 책에서 비롯된 단어인데, 책에 있는 리팩토링의 정의를 보면 다음과 같다.

“리팩토링은 외부 동작을 바꾸지 않으면서 내부 구조를 개선하는 방법으로, 소프트웨어 시스템을 변경하는 프로세스이다.” 마틴 파울러, 리팩토링, P10. 대청출판사 책에 이어서 나오는 내용은 버그가 끼어들지 않도록 주의하면서 코드를 작성한 후에 더 나은 디자인으로 개선하는 방법이라고 한다. 디자인을 먼저 한 후 코드를 만드는 것이 아니라 일단 돌아가는 코드를 작성하고, 그 후에 그 코드가 더 좋은 구성을 갖도록 바꾼다는 것이다. 우리들의 코딩 관행을 돌아보면, 일단 돌아가는 프로그램을 짠다. 그리고? 끝이다. 그 다음으로 넘어간다. 정리? 남은 사람이 알아서 할 것이다. 남은 사람이 자기 밖에 없다면? 날 잡아서 정리하거나, 회사 옮긴다.




리팩토링을 하는 이유



야심찬 초급 개발자가 자주하는 것 중에 하나가 이전 소스에 대한 비평이다. “도대체 어떻게 이렇게 소스를 짤 수 있지. 발로 짜도 이것보다는 낫겠네. 왜 이렇게 if else가 많은 거야. 이 소스 이해할 시간 있으면 차라리 다시 짜고 만다.” 그래서, 다시 짠다. 그리고 오픈하면 이것 저것 버그 리포트와 요구사항이 들어온다. 이것 저것 예외 처리를 해주다 보면 내가 짠 코드지만 보기 싫어진다. 어느 정도 서비스가 안정적으로 돌아가도록 소스를 수정해 놓으니, 이런, 전에 내가 막 뭐라고 했던 이전 개발자의 소스와 별반 차이가 없다.

“제길, 다음 후임이 누가 될지는 몰라도 내 욕 무진장 하겠군.” 문서라도 잘 주면 모르겠지만, 처음 개발할 때 보고했던 문서 그대로다. 요구사항과 수정을 통해서 변경된 내용을 문서에 업데이트하질 못했다. “할 시간이 있어야지.” SM(System Maint enance)분야에서는 거의 이렇게 사는 것이 보통이다.

이전 사람이 만든 소스에서 버릴 것은 거의 없다. 정리가 안 되서 몇 달간 목욕 못한 모습일 뿐이지, 처리할 수 있는 모든 경우의 수는 그 안에 다 가지고 있다. 이런 코드를 새로 짠다는 것은 그 모든 경우의 수를 처음부터 다시 감수하겠다는 의미가 된다.
이전 소스를 씻기고 다듬는 것이 소스 수정을 위한 필수 과정이다. 정리하지 않고 계속해서 소스를 추가해 가는 일은 운동하지 않고 계속해서 먹어대는 것과 같이 시스템을 비만상태로 만들어간다. 움직임이 점차 둔해질 것이다. 정리 안 된 방처럼 발 디딜 팀이 없는 소스가 될 것이다.

무엇인가 소스의 변경이 필요할 때, 기능 추가나 삭제, 수정 작업이 일어날 때 소스의 리팩토링은 포장이사처럼 편하게 작업하도록 도와준다. 리팩토링은 소스의 중복된 부분을 모듈화 시켜준다. 모듈화는 입출력이 명확하기 때문에 이식성을 높여준다. 중복을 제거한다는 것은 시스템의 칼로리를 빼는 것과 같다. 시스템의 복잡도, 즉 코드를 읽는 사람의 머리가 열받는 정도를 낮춰준다. 물론 그렇다 해도 이사 자체는 귀찮은 일이다.




리팩토링을 위한 도구



리팩토링은 그로 인해 영향 받는 프로그램의 수가 적을 대에만 수작업으로 작업해야 한다. 사실 리팩토링을 수작업으로 한다는 것은 추천하지 않는다. 좋은 개발 환경이 있는데 사서 고생할 필요가 없는 탓이다. 리팩토링을 위한 좋은 툴이 많이 나왔다. 일단 통합개발환경(IDE, Integrated Development Environment)을 준비한다. 요즘의 자바 개발 시 많이 사용되는 IDE는 기본적으로 리팩토링을 지원한다.

리팩토링과 함께 진행되어야 할 JUnit 테스트케이스 자동 생성도 같이 지원되고 있다. 리팩토링 작업을 할 경우 여러 줄의 코드들이 수정된다. 이때 영향을 받는 프로그램들을 모두 불러내서 수작업으로 수정할 경우 리팩토링에 대한 공수가 많이 필요한 탓에 감히 리팩토링에 대한 엄두를 낼 수 없다. 하지만 요즘 통합 개발 환경을 지원하는 개발 도구들은 변경 받는 파일들의 목록과 변경 전 후의 코드 비교, 자동 변경 기능을 지원한다. 덕분에 리팩토링에 드는 수고가 전혀 수고로 생각되지 않을 정도다.




리팩토링 진행 방법



리팩토링하는 이유와 리팩토링 도구까지 알아보았으니 이제 리팩토링 방법에 대해 알아볼 차례다. 주저리 주저리 방법들을 늘어놓을 수 도 있겠지만 개발자는 코드로 얘기한다. 바로 이클립스에서 리팩토링을 사용하는 방법을 설명하도록 하자.


<리스트 1> 리팩토링 샘플
1 public void deleteArticle(Connection conn, int seq) throws SQLException {
2 if (conn == null)
3 return;
4
5 // db에서 삭제 - 삭제 테이블로 이동
6 PreparedStatement pstmt = null;
7 pstmt = conn.prepareStatement(QUERY_MOVE);
8 pstmt.setInt(1, seq);
9 pstmt.executeUpdate();
10
11 pstmt.close();
12
13 pstmt = conn.prepareStatement(QUERY_DELETE);
14 pstmt.setInt(1, seq);
15 pstmt.executeUpdate();
16
17 pstmt.close();
18
19 // memo 삭제 생략
20
21 }


리팩토링에 대한 간단한 예를 들기 위해서 <리스트 1>을 보며 설명하겠다. 7~11번 줄의 코드가 13~17번 줄의 코드와 유사한 것을 알 수 있다. 중복이 계속되는 것은 일정한 패턴을 갖고 있는데 중복이 심해지면 패턴 변경에 따른 공수가 많이 필요하므로 소스의 유연성이 떨어지게 된다. 때문에 반복되는 패턴을 메소드화 시켜서 쉽게 코드를 읽을 수 있도록 한다.

<화면 1> 반복되는 부분, 메소드 추출의 대상


<화면 2> 메소드 추출(Extract Method)

이클립스에서 패턴부분을 선택하고, 오른쪽 버튼을 눌러 콘텍스트 메뉴를 열면 중간 위치에 [Refactor…]라는 메뉴가 보인다. 확장 메뉴에서 [Extract Method…]를 선택하면 <화면 2>와 같은 다이얼로그 창이 뜬다. ‘doQuery’라고 메소드명을 입력한 뒤에 파라미터들을 확인한다.

화면 아래쪽의 버튼 중 [Preview]를 클릭하면 <화면 3>과 같이 미리보기 창으로 바뀐다. 이때 화면에 표시되는 정보들이 기가 막힌다. 리팩토링을 통해서 변경되는 소스의 비교와 상단에는 이 리팩토링에 영향을 받는 소스들과 메소드명까지 친절하게 알려준다. 게다가 이클립스가 모두 다 자동으로 바꿔준다.


<화면 3> 리팩토링 결과 미리보기


<화면 4>에서는 다이얼로그에서 만든 doQuery() 메소드의 내용을 볼 수 있다. 소스 비교란의 맨 오른쪽에 있는 네모는 소스 전체에서 변경이 일어난 부분을 표시한 것이다.


<화면 4> 리팩토링으로만들어진 메소드

비교가 끝났다면 [OK] 버튼을 클릭해서 리팩토링을 실행한다. 소스 리팩토링을 마친 뒤에 doQuery() 메소드를 보면, <화면 5>처럼 파라미터가 Connection conn, int seq 두 개임을 알 수 있는데, 여기에 하나가 더 필요하다. 바로 쿼리 부분인데, 이것을 파라미터로 받아야 비로로 doQuery()가 공용으로 쓰일 수 있게 된다.

<화면 5> 리팩토링으로 만들어진 약간 아쉬운 메소드

QUERY_MOVE라는 상수를 파라미터로 대치한다. 이 상수에 마우스 오른쪽 버튼을 클릭한 뒤에 [Refactor]-[Introduce Para meter] 메뉴를 실행시키면 <화면 6>과 같은 다이얼로그 창을 볼 수 있다. 새로운 파라미터 이름을 ‘query’로 정하고 우측의 [up] 버튼을 클릭해서 파라미터의 위치를 조정한다. 파라미터의 변경은 메소드의 모습인 시그니처(signature)를 변경하는 것이다.


마찬가지로 [Preview] 버튼을 클릭하면 <화면 7>과 같이 리팩토링 전후의 소스를 비교할 수 있다.

<그림 7>에서 [OK] 버튼을 클릭해서 만들어진 doQuery() 메소드는 반복되는 쿼리 실행 부분을 메소드 추출(Extract Met hod)과 파라미터로 빼기(Introduce Parameter) 리팩토링을 이용해서 만든 것이다. <리스트 2>는 그것을 이용해서 바뀐 소스의 모습이다.

<그림 7> 파라미터로 만들기 적용하기 전 미리보기


<리스트 2> QUERY_DELETE 부분 리팩토링 과정
1 public void deleteArticle(Connection conn, int seq) throws SQLException {
2 if (conn == null)
3 return;
4
5 // db에서 삭제 - 삭제 테이블로 이동
6 PreparedStatement pstmt;
7 doQuery(conn, QUERY_MOVE, seq);
8 doQuery(conn, QUERY_DELETE, seq);
9
10 pstmt = conn.prepareStatement(QUERY_DELETE);
11 pstmt.setInt(1, seq);
12 pstmt.executeUpdate();
13
14 pstmt.close();
15
16 // memo 삭제 생략
17
18 }

19 private void doQuery(Connection conn, String query, int seq) throws SQLException {
20 PreparedStatement pstmt = null;
21 pstmt = conn.prepareStatement(query);
22 pstmt.setInt(1, seq);
23 pstmt.executeUpdate();
24
25 pstmt.close();
26 }


<리스트 2>는 아직 변경 중인 샘플코드이다. 앞서 만든 doQuery() 메소드를 이용해서 쿼리만 다른 것을 보내면 된다. 필자가 추가한 8번 줄은 10~14번 줄과 동일한 기능을 수행하게 된다. 코드를 정리하면 다음과 같이 된다.


<리스트 3> QUERY_DELETE 부분 리팩토링 후
1 public void deleteArticle(Connection conn, int seq) throws SQLException {
2 if (conn == null)
3 return;
4
5 // db에서 삭제 - 삭제 테이블로 이동
6 doQuery(conn, QUERY_MOVE, seq);
7 doQuery(conn, QUERY_DELETE, seq);
8 // memo 삭제 생략
9
10 }


하단의 구문이 지워지면서 이 deleteArticle() 메소드 내의 PreparedStatement pstmt 선언은 불필요하기 때문에 삭제했다. 처음 보았던 소스에서 많이 정리되었다. 정리를 하고 보니 deleteArticle() 메소드를 호출하는 곳에서 비슷한 기능을 하는 부분을 볼 수 있다.


<리스트 4> 리팩토링 적용 범위 확대
1 // password 확인
2 if (confirmPassword.equals(MASTER_PASSWORD)
3 || confirmPassword.equals(article.getPassword())) {
4 deleteArticle(conn, seq);
5 deleteFiles(conn, seq);
6 } else {
7 resourceName = "/jsp/error.jsp";
8 throw new Exception(CommonUtil.k2a("잘못된 비밀번호"))
9 }

10 public void deleteFiles(Connection conn, int seq) throws SQLException {
11 if (seq == 0){
12 return;
13 }
14
15 // file db에서 삭제 - sts 값 0 로 변경
16 PreparedStatement pstmt = conn.prepareStat ement(QUERY_DEL_SEQ_FILE);
17 pstmt.setInt(1, seq)
18 pstmt.executeUpdate();
19
20 pstmt.close();
21
22 // file 삭제 생략
23 }


<리스트 4>의 16~20번 줄을 보면 앞서 추출한 메소드 doQuery()로 변경할 수 있을 것 같다. 그럼 코드는 <리스트 5>와 같이 수정될 것이다.


<리스트 5> QUERY_DEL_SEQ_FILE 부분 리팩토링 과정

10 public void deleteFiles(Connection conn, int seq) throws SQLException {
11 if (seq == 0){
12 return;
13 }
14
15 // file db에서 삭제 - sts 값 0 로 변경
16 doQuery(conn, QUERY_DEL_SEQ_FILE, seq);
17
18 // file 삭제 생략
19 }


이렇게 정리하고 난 후에 다시 전체적인 코드를 생각해보면 두 개의 메소드가 불필요하다 생각이 든다. 즉 <리스트 6>과 같이 deleteArticles()와 deleteFiles() 메소드를 지우고 바로 doQuery() 를 호출하도록 바꿀 수 있을 것이다. <리스트 6>은 리팩토링을 통해 최종적으로 정리된 소스이다.


<리스트 6> 리팩토링 적용으로 개선된 코드
1 // password 확인
2 if (confirmPassword.equals(MASTER_PASSWORD)
3 || confirmPassword.equals(article.getPassword())) {
4 // db에서 삭제 - 삭제 테이블로 이동
5 doQuery(conn, QUERY_MOVE, seq);
6 doQuery(conn, QUERY_DELETE, seq);
7 // memo 삭제 생략
8 // file db에서 삭제 - sts 값 0 로 변경
9 doQuery(conn, QUERY_DEL_SEQ_FILE, seq);
10 // file 삭제 생략
11 } else {
12 resourceName = "/jsp/error.jsp";
13 throw new Exception(CommonUtil.k2a("잘못된 비밀번호"));
14 }


앞에서 보았던 소스의 if else 구문과 비교해보면 doQuery() 라는 공통으로 사용할 수 있는 메소드와 5줄이 늘어났지만 deleteArticle(), deleteFiles() 두 개의 메소드가 사라졌다. 이전 소스와 비교해보면 메소드 구성은 <화면 8>과 같이 변경되는 것을 알 수 있다.

추가된 메소드는 +화살표, 제거된 메소드는 화살표로 표시되고 변경된 메소드는 그냥 검은 화살표로 표시된다. 화면 아래쪽에 표시되는 소스 비교하는 곳을 보면 더욱 명확하게 알 수 있다.

지금까지 샘플 소스의 구조를 개선하면서 두 가지 리팩토링 기법에 대해 알아보았다. 이 외에도 많은 기법들이 리팩토링 책에 소개되어있고, 이클립스에도 더 많은 리팩토링 기능이 지원된다.


<그림 8> 리팩토링 전 후 메소드 비교



리팩토링 경험담



필자는 이 글을 쓰고 있는 지금 큰 프로젝트를 진행하고 있다. 6년간 하나도 버려지지 않고 운영되면서 그때그때 패치된 페이지를 스프링 프레임워크에 맞춰서 바꾸는 작업이다. 그런데, 작업을 하는 동안 필자가 간과한 것이 있었다. 그렇게 복잡하게 얽히고설킨 페이지를 스프링 프레임워크의 새로운 바닥부터 하나씩 쌓아 올린 것이다. 기존에 운영하고 있는 소스에서 하나씩 뜯어서 새로운 토양으로 옮겨심기를 한 것이다. 이것은 재개발에 가까운 것이었고, 굉장히 많은 시간이 필요했다. 만약 옮겨야 할 소스를 기존의 토양 위에서 조금씩 리팩토링한 후에 옮겼다면 오히려 많은 시간을 절약할 수 있었을 것이다.

실수했다고 생각하는 부분은 다음과 같다. 우선적으로 모든 기능을 다 옮겨올 때까지 신규 페이지는 아직 미완성이다. 하지만 기존의 페이지 내에서 리팩토링을 한다고 하면 이미 모든 기능과 데이터를 다 갖고 있는 상태이다. 다른 파트에서 데이터가 필요하다고 할 때에도 현재 갖고 있는 데이터에서 데이터를 뽑아내서 보다 빨리 전달할 수 있을 것이다.

두 번째로 시간의 압박이다. 기존의 페이지는 언제든지 답이 나온다. 하지만 신규페이지는 모든 테스트를 마칠 때까지 계속 기다리라고 얘기해야만 한다. 바닥부터 모든 것들에 대해서 테스트를 만들어야 하는 탓에 더 많은 테스트 코드들이 필요하다. 여기에도 만만치 않은 시간이 투입된다.

세 번째는 애플리케이션에 대한 자신감이 떨어진다는데 있다. 맥가이버도 아닌데 시간에 쫓기면서 개발할 경우 만들어진 소스는 분명히 수많은 버그를 품고 있을 가능성이 높다. 그 값이 절대 정확하다고 이야기하기 힘들다. 하지만 리팩토링을 통해서 내부로부터 개혁해 나갈 경우 빠진 것 없이 소스를 재구성할 수 있기 때문에 안정된 기반에서 작업할 수 있을 것이다.

전산의 불문율 가운데 유명한 것이 하나 있다. ‘잘 돌아가는 것은 손대지 마라.’ 칼퇴근을 위해서 절대 절명으로 필요한 말이다. 이렇게 관리되는 소프트웨어의 품질은 논하기 힘들다. 그냥 먹고 살기 위한 프로그램과 그것을 관리하는 직장인이 되어버리게 된다. 반면에 리팩토링의 기본 사상은 개선을 위한 노력이다. 막무가내 개선이 아니라 현명한 개선을 위한 방법을 제시하고 있고, 친구격인 테스트 케이스가 그 안전장치가 되어 준다. 한 순간의 품질이 아닌 지속적인 소프트웨어의 건강을 생각한다면 꾸준히 리팩토링으로 손질할 필요가 있다. 그것이 끝없이 변하는 웹 애플리케이션과 같은 소프트웨어일 경우는 더욱 그렇다.
개선을 위한 작은 몸짓에 진정한 프로그래머가 되고 싶은 독자들을 초대한다.


참고 자료
1. 리팩토링, 마틴파울러, 윤성준,조재박 역, 대청, 2002년3월
2. 패턴을 활용한 리팩터링, 죠슈아 케리에브스키, 윤성준,조상민 역, 인사이트,
2006년7월


리팩토링 관련 사이트

1. http://www.refactoring.com/
Refactoring Home Page

2. http://xper.org/wiki/xp/ReFactoring
김창준 님의 Refactoring에 관한 정보

3. http://c2.com/cgi/wiki?CodeSmell
Code Smell

4. http://xper.org/wiki/xp/CodeSmell
Code Smell 번역

5. http://www.okjsp.pe.kr/lecture/ide/eclipse/refactor/eclipse_refactoring.html
Eclipse의 refactoring기능

6. http://www.okjsp.pe.kr/lecture/ide/eclipse/eclipse_install.html
Eclipse 시작하기


 



제공 : DB포탈사이트 DBguide.net

반응형

Jad Decompiler 사용법

Jad home page: http://www.geocities.com/SiliconValley/Bridge/8617/jad.html
Copyright 2000 Pavel Kouznetsov (kpdus@yahoo.com). 


[ 사용방법 ]


1. 클래스 하나만 디컴파일시

           example1.class   를 디컴파일시 

           jad.exe 를 디컴파일할 파일과 동일한 폴더에 놓는다.

         

           Command 창에   jad -o -sjava example1.class   

       

   결과물 : 'example1.java' 

   

2. Package 를 디컴파일시   

         tree  폴더 아래의 모든 클래스파일을 디컴파일시 

         폴더와 같은 폴더에 jad.exe  를 위치하고


          Command 창에    jad -o -r -sjava -dsrc tree/**/*.class 

          

          결과물 : 폴더내에 [src] 폴더가 생성된다. 

=============================================================================================================

윈도우버전(for Windows 9x/NT/2000 on Intel platform)


jad -r -d .\src -s java .\ifxjdbc\**\*.class


참고 : jad -r [-d<directory_for_sources>] [<other_options>] <directory_with_classes>**/*.class



설명 :


-r : 해당 패키지 형태로 디렉토리 구조를 만듬( restore package directory structure)

-d : 디컴파일될 디렉토리(-d <dir> - directory for output files)

 -s java : 디컴파일된 파일의 확장자를 java로 하라


.\ifxjdbc\**\*.class : ifxjdbc 디렉토리 아래의 모든 클래스들 지정



-----------------------------------------------------------------------------------------


이클립스 디컴파일러인 JAD를 설치하는 방법입니다.


이클립스로 개발을하다보면 F3버튼으로 열심히 따라가는 경우가생깁니다.


그러다 라이브러리로 묶여있는 클래스파일들을 로딩하게되면 읽긴읽되 내용을 분석하지못하죠~


그래서 디컴파일러 ~ 컴파일한클래스파일을 다시 자바파일로 보여주는 도구를 설치하게되면


클래스파일도 자바파일처럼 열수있게됩니다^^~



우선 jad.exe, jadclipse_3.1.0.jar를 다운로드 합니다.


jad.exe파일은 이클립스 폴더에 jadclipse_3.1.0.jar파일은 플러그인 폴더에 카피합니다.


window->preference->general->editors->file assosiationsd에서 *.class선택후


하단의 JadClipse Class File Viewr 를 선택후 default 를 선택합니다.


window->preference->java->JadClipse를 선택한후 ignore existing source를 체크해줍니다.


이렇게하시고 이클립스를 실행시키시면 *.class파일을 찾아갈경우 자동으로 디컴파일해줍니다~

http://sourceforge.net/projects/jadclipse



반응형

Mocha - http://www.brouhaha.com/~eric/computers/mocha.html 
Hanpeter van Vliet's first Java Decompiler. Orphaned at Java 1.02, so crashes on inner classes. [Freeware]

WingDis 2.15 - http://www.wingsoft.com/wingdis.shtml 
Command line Java decompiler. [Commercial]

SourceAgain - http://www.ahpah.com/product.html 
Java Decompiler for Win95/NT, Unix, web-based trial version from Ahpah Software. [Commercial]

Jad - http://www.geocities.com/SiliconValley/Bridge/8617/jad.html 
the fast Java decompiler. Separate versions for Win32, OS/2, most Unixen. [Freeware].

SourceTec Java decompiler - http://www.srctec.com/decompiler.htm 
Patch to Mocha which defeats Crema. [Shareware]

NMI's Java Code Viewer - http://njcv.htmlplanet.com 
The "user-friendly, visual" Java decompiler and disassembler for Win32 platform. [Shareware]

Java Code Engineering - http://www.meurrens.org/ip-Links/Java/codeEngineering/ 
Marc Meurrens' page on engineering & reverse engineering Java class files. Review of books and on line resources on: the Java Virtual Marchine (JVM); code engineering & reverse engineering; class browsers, viewers & editors; assemblers; compilers; disassemblers & decompilers; obfuscators & unobfuscators. Comprehensive, well organized site.

Decafe Pro - http://decafe.hypermart.net/ 
Java Decompiler for Win32 platform. [Commercial]

 Kimera - http://kimera.cs.washington.edu/disassembler.html 
Online Java Disassembler. Just enter an URL of a .class file and it will be disassembled into a form suitable for re-assembly by Jasmin. [Freeware] 

Dumping Class Files - http://professionals.com/~cmcmanis/java/dump/index.html 
Chuck McManis's code to parse class files. [Free for non-commercial].

ClassCracker - http://www.pcug.org.au/~mayon/ 
Visual Java decompiler from Mayon Software Research. [Commercial]

Java Optimize and Decompile Environment (JODE) - http://jode.sourceforge.net/ 
Java decompiler with full source code. Supports inner/anonymous classes. Also contains a small, but extensible bytecode obfuscator/optimizer. [Open Source, GPL]

CafeBabe - http://jfa.javalobby.org/projects/CafeBabe.html 
Graphical disassembler and editor of Java bytecodes. Part of the JFA. [Open Source]

RIIS: Decompiling Java - http://www.riis.com/depile.html 
Godfrey Nolan's book on writing your own Java decompiler may never be published, so early chapters are online in PDF format.

dis - http://www.cs.princeton.edu/~benjasik/dis/index.html 
A functional, fast and simple disassembler that is written in C. Like javap, produces bytecode. Download Solaris, Win95/NT versions. Mac or other Unixen or source code by email request. [Freeware]

BackToJava (BTJ) - http://www.backtojava.org/ 
General tool for manipulation of Java bytecode (disassembly, assembly, statistics, decompilation, debug information...), written in Java. In progress. [Open Source, LPGL]

JReveal.Org - http://www.jreveal.org 
Online decompiler and obfuscator. Also has a resource directory. [Freeware]

IceBreaker - http://www.suddendischarge.com/cgi-bin/antileech.cgi?icebreak10.zip 
(Zip-file) A 'Visual' Java decompiler/disassembler. Allows side-by-side comparison of disassembled byte code from class files, for easier decompilation. "Replete with bugs" and dependent on command-line decompiler, such as Mocha or Javap. [Freeware] 

+ Recent posts