'05.JSP'에 해당되는 글 65건
- 2009.01.20 jsp, ie 첨부파일 열기 실행시 에러 ( 캐쉬폴더에서 실행시 에러), jsp 2
- 2009.01.20 request.getParameter null 값 처리 5
- 2009.01.19 JSP 페이지 이동 4가지 방법 및 특성
- 2009.01.19 JSTL (JSP Standard Tag Library)
- 2009.01.19 jstl/el db접속 간단예제 2
- 2009.01.19 jstl 문법정리 2
- 2009.01.19 JSTL과 Velocity를 활용한 UI 레이어 구현 방법
- 2009.01.14 JAVA를 이용한 JSP페이지에서 날짜 구하기 및 계산
- 2008.12.28 [ JSP ] 게시판 페이징 처리 로직 JSP
- 2008.12.08 JSP Error Pages
- 2008.12.08 JSP 2.0 프로그래밍 1
- 2008.12.08 jsp errorPage 작성 2
- 2008.11.19 [펌] JExcelApi & POI
- 2008.11.19 Excel File(엑셀파일) 다운로드 받기 1
- 2008.11.19 jsp에 엑셀 자료 넣기 jexcel or poi
- 2008.11.13 세션과 쿠키보는 방법
- 2008.11.12 Sun Developer J2EE 문서
- 2008.11.12 JSTL
- 2008.11.12 JSTL과 Velocity를 활용한 UI 레이어 구현 방법
- 2008.11.06 JSTL SQL Tag
- 2008.10.19 대용량 데이타 INSERT 하는 방법
- 2008.10.19 jsp 헤더 정보 구하기
- 2008.10.19 CallableStatement 사용예
- 2008.10.19 서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-
- 2008.10.19 서블렛 + JDBC 연동시 코딩 고려사항 -제1탄-
- 2008.10.19 session
- 2008.10.19 자카르타 DBCP API를 이용한 커넥션 풀 사용하기
- 2008.10.19 자카르타 DBCP API를 이용한 커넥션 풀 사용하기
- 2008.08.26 jsp에서 RSS 읽어들이는 방법을
- 2008.08.25 JSP 웹서버 셋팅 정리 문서
먼저 다음 페이지 이동 특성들을 미리 알아볼 필요가 있습니다
JSP에서는 페이지 이동시 다음 4가지 정도의 방법이 있습니다
① JavaScript를 이용
window.open, location.href, location.replace 등을 이용할수 있습니다
login_process.jsp
특징적인부분은 브라우져의 주소창이 변경되며
(이말은 즉슨 클라이언트가 다시 admin.jsp를 서버에 요청한다는 말입니다)
login_process.jsp 에서 jsp가 다 실행되고 브라우져에 out put된 html 및 javascript들만으로
실행된 코드들이라는 것입니다
② response.sendRedirect를 이용
login_process.jsp
이 코드에서 a가 출력될까요 안될까요?
출력 됩니다.
sendRedirect가 되더라도 밑에 jsp 코드들은 모두 실행 된다는 말입니다
response.sendRedirect는 기본적으로 모든 로직들을 다 처리한 후 코드 맨 마지막 부분에
사용하는 것이 올바른 방법입니다
만약 그렇지 못한 경우는 response.sendRedirect 다음 바로 return; 이라는 코드를 집어 넣어야 합니다
response.sendRedirect은 HTTP 헤더정보를 변경하여 redirect시키기 때문에 역시 브라우져의 주소창이 변경되며 sendRedirect가 실행되기전 html이나 javascript로 out put되는 코드들은 모두 실행되지 않습니다.
③ forward 이용
jsp 태그의 <jsp:forward> 나 servlet의 RequestDispatcher.forward 를 이용할수 있습니다
login_process.jsp
그럼 위의 코드의 경우 a가 출력 될까요 안될까요?
정답은 출력 안됩니다. 바로 forward 되어 버립니다.
클라이언트로 응답주지 않고 바로 서버측에서 admin.jsp로 이동하기 때문에
주소창이 바뀌지 않고 그로인해 브라우져에 출력되는 응답속도 또한 사용자가 보기에는
응답이 빠른 장점이 있습니다
하지만 forward 이후 JSP 코드들이 실행되지 않는것 사실이지만 만약 finally절이 있는경우
finally절은 실행 됨으로 확실히 알아둡시다.
④ meta 태그 이용
마지막으로 meta 태그를 이용할 수도 있습니다.
즉 요약하자면..
페이지 이동 방법이 여러가지가 있습니다.
그 특성들을 잘 알고 올바르게 사용하여야 합니다.
JSTL (JSP Standard Tag Library)
http://jakarta.apache.org/site/downloads/downloads_taglibs-standard.cgi
1. 위 사이트에서 JSTL 을 다운 받습니다.
2. 압축을 풀고 lib 디렉토리 안의 jstl.jar 와 standard.jar 를 복사합니다.
3. Tomcat 의 디렉토리 내의 common\lib 안에 복사 해놓고 쓰면 됩니다.
<%@ taglib prefix="c" uri="java.sun.com/jsp/jstl/core" %> 와 같이 선언하면 됩니다.
기본적인 기능
<%@ taglib prefix="c" uri="java.sun.com/jsp/jstl/core" %>
XML 처리
<%@ taglib prefix="x" uri="java.sun.com/jsp/jstl/xml" %>
국제화와 형식화
<%@ taglib prefix="fmt" uri="java.sun.com/jsp/jstl/fmt" %>
데이터베이스 작업
<%@ taglib prefix="sql" uri="java.sun.com/jsp/jstl/sql" %>
JSTL 설치
- 톰캣 설치기 설치 옵션에서 Examples를 선택하면 설치되는 예제 애플리케이션에 들어있음
- 위치 : webapps/jsp-examples/WEB-INF/lib/jstl.jsr
- 위의 파일을 WEB-INF/lib 디렉토리에 복사
스크립팅 없이 루핑 돌리기
<c:forEach>
- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
- <c:forEach var="movie" items="${movieList}" varStatus="movieLoopCount" >
-
<tr>
-
<td>${movie} </td>
-
<td>${movieLoopCount.count}</td>
-
</tr>
- </c:forEach>
var : 컬렉션에 있는 각각 항목을 나타내는 변수로, 한번 회전할 때마다 값이 바뀝니다.
items : 루핑을 돌 실제 데이터(배열, 컬렉션, 맵 또는 콤마 분리 문자열)
varStatus : 루핑 횟수 알기
배열을 불러올때는 ${param.movieList} 가 아닌 ${paramValues.movieList}로 items에 저장해야 한다.
<c:forEach>안에 <c:forEach>품기
- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
- <c:forEach var="listElement" items="${movies}" varStatus="movieLoopCount" >
- <c:forEach var="movie" items="${listElement}">
-
<tr>
-
<td>${movie} </td>
-
<td>${movieLoopCount.count}</td>
-
</tr>
- </c:forEach>
- </c:forEach>
속성 : begin, end, step
조건문
<c:if>
- <c:if test="${userType eq 'member'}:>
-
<jsp:include page="inputComments.jsp" />
- </c:if>
<c:choose>, <c:when>, <c:otherwise>
- <c:choose>
-
<c:when test="${userPref == 'preformance'}">
-
go 1
-
</c:when>
-
<c:when test="${userPref == 'preformance'}">
-
go 2
-
</c:when>
-
<c:otherwise>
-
go 3
-
</c:otherwise>
- </c:choose>
속성 입력
<c:set>
- <c:set var="userLevel" scope="session" value="Cowboy" />
- <c:set var="userLevel" value="${person.dog}" />
- <c:set var="userLevel" scope="session">
-
Sheriff, Bartender, Cowgirl
- </c:set>
- var : session 생존범위에 "userLevel" 이란 이름의 속성이 없으면 <c:set> 태그는 하나를 새로 만듭니다.
- scope : 옵션값
- value : 꼭 문자열일 필요는 없습
- 기타 : value가 널 인경우 변수는 제거됩니다.
빈과 맵에 <c:set> 태그 사용하기
<c:set> 태그는 빈 프로퍼티와 맵 값 두 가지 경우에만 동작합니다.
리스트나 배열에 값을 추가하기 위해서 사용할 수 없슴
- <c:set target="${petMap} property="dogName" value="Clover" />
- // target은 널이어서는 안 됩니다.
- // target이 맵인 경우 dogName은 키 이름이 됩니다
- // target이 빈인 경우 dogName은 프로퍼티 이름이 됩니다.
- <c:set target="${person}" property="name" >
- // target 속성에 id 이름을 기입하면 안됩니다.
-
${foo.name}
-
// 몸체 안에는 문자열 또는 표현식이 들어갈 수 있습니다.
- </c:set>
target 속성에는 객체(Object)가 들어가야 합니다. 빈이나 맵의 "id" 이르을 쓰면 안됩니다.
scope 속성을 명기하지 않으면?
컨테이너는
- page
- request
- session
- application(context)
순으로 찾음, 못 찾을 경우 page 생존범위로 해서 하나를 새로 만듬
<c:remove> 로 속성 제거하기
- <c:remove var="userStatus" scope="request" />
- // var 속성에는 문자열을 넣어야함. 표현식을 넣으면 안됨
<c:import> 로 컨텐츠 포함
- <c:import url="http://www.lazyartist.co.kr"/>
요청이 들어오는 시점에 url 속성에 명기한 파일을 현재 컨텐츠에 포함합니다.
<c:import>는 외부 자원도 포함할 수 있습니다.
<c:param> 으로 포함될 파일 동적 변화 시키기
- <c:import url="header.jsp">
-
<c:param name="subTitle" value="we take the String" />
- </c:import>
포함될 파일에
- ${param.subTitle}
<c:url>로 url 재작성
- <a href="<c:url value='/imputComments.jsp' />
- // value에 들어있는 상대경로 뒤에 jsessionid를 추가합니다.(쿠키를 사용하지 못 하는 경우)
url 인코딩
- <c:url value='/imputComments.jsp' var="inputURL" />
-
<c:param name="firstName" value="${first}" />
-
<c:param name="lastName" value="${last}" />
-
// param 태그를 사용합으로써 url인코딩까지 해준다.(공백에 +를 넣어줌)
-
</c:url>
- <a href="${inputURL}">
오류 페이지 설정
오류 페이지에서만 쓸 수 있는 객체 : exception
- <%@ page isErrorPage="true" %>
- ${pageContext.exception}
오류 발생 후 오류 페이지로 이동하지 않고 자체 페이지에서 해결하기 : <c:catch>
- <c:catch>
-
<% int x = 10/0; %>
-
// 에러가 발생하면 다음을 실행하지 않고 </c:catch>로 곧장 이동한다.
- </c:catch>
exception을 속성으로 만들어 에러 메시지 읽기
- <c:catch var="myException">
-
...
- </c:catch>
- ${myException.message}
- // 타입이 Throwable이니 message프로퍼티가 있음
JSTL API
코어(Core) 라이브러리
-
일반적인 것
- <c:out>
- <c:set>
- <c:remove>
- <c:catch>
-
조건
- <c:if>
- <c:choose>
- <c:when>
- <c:otherwise>
-
URL 관련
- <c:import>
- <c:url>
- <c:redirect>
- <c:param>
-
반복
- <c:forEach>
- <c:forEachToken>
포맷팅 라이브러리
-
국제화
- <fmt:message>
- <fmt:setLocale>
- <fmt:bundle>
- <fmt:setBundle>
- <fmt:param>
- <fmt:requestEncoding>
-
포맷팅
- <fmt:timeZone>
- <fmt:setTimeZone>
- <fmt:formatNumber>
- <fmt:parseNumber>
- <fmt:parseData>
SQL 라이브러리
-
데이터베이스 접근
- <sql:query>
- <sql:update>
- <sql:setDataSource>
- <sql:param>
- <sql:dataParam>
XML 라이브러리
-
코어 xml 액션
- <x:parse>
- <x:out>
- <x:set>
-
xml 흐름 제어
- <x:if>
- <x:choose>
- <x:when>
- <x:otherwise>
- <x:forEach>
-
변환 액션
- <x:transform>
- <x:param>
- <%@ page language="java" contentType="text/html; charset=EUC-KR"
- pageEncoding="EUC-KR"%>
- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
- <%@ taglib prefix="sql" uri="http://java.sun.com/jsp/jstl/sql" %>
- <html><head><title>Insert title here</title></head>
- <body>
- <center>
- ***상품자료***<br/>
- <sql:setDataSource var="ds" url="jdbc:oracle:thin:@localhost:1521:java"
- driver="oracle.jdbc.driver.OracleDriver"
- user="scott" password="tiger"/>
- <sql:query var="rs" dataSource="${ds}">
- select * from sangdata where code >= ? and code <= ?
- <sql:param value="${2}" />
- <sql:param value="${100}" />
- </sql:query>
- <table align="center" border="1">
- <tr>
- <th>코드</th><th>수량</th><th>단가</th><th>금액</th>
- </tr>
- <c:forEach var="r" items="${rs.rows}">
- <tr>
- <td><c:out value="${r.code}" /></td>
- <td><c:out value="${r.sang}" /></td>
- <td><c:out value="${r.su}" /></td>
- <td><c:out value="${r.dan}" /></td>
- <c:set var="tot" value="${r.su * r.dan}"></c:set>
- <td><c:out value="${tot}" /></td>
- </tr>
- </c:forEach>
- </table>
- </center></body>
- </html>
<c:set var="t" value="hello"/>
<c_rt:set var="color" value="<%=abc%>"/>
=> 이 경우 c_rt 대신 c 를 사용하면 <%= %>을 포함한 문자열로 인식해 버린다.
<c:set var="hit" value="1" scope="session"/>
=> session 스코프 내에서 살아있는 변수가 된다.
<c:set value="value" target="target" property="propertyName"/>
=> 속성 value로 target객체의 프로퍼티 값을 정한다.
<c:set target="target" property="propertyName">
body content
</c:set>
=> body 값으로 target객체의 프로퍼티 값을 정한다.
=> Scope : page | request | session | application
* 변수의 제거
<c:remove var="name" scope="session"/>
* forEach 예문 (보통의 for문 & 개선된 루프)
<c:forEach items="${pageScope }" var="h">
<c:out value="${h }"/>
</c:forEach>
=> 위의 예문은 pageScope EL내장객체의 모든 요소를 루프를 통해 뽑아내어 출력하는 문장이다.
<c:forEach var="h" items="${header}">
<c:out value="${h.key}:${h.value }"/><br>
</c:forEach>
=> 위의 예문은 header 내장객체의 모든 요소를 key와 value로 구분하여 출력하는 문장이다.
<c:forEach var="one" begin="1" end="10" step="1">
<c:out value="${one}"/>
</c:forEach>
=> 위의 예문은 반복할 횟수를 직접 지정하여 루프를 돌리는 문장이다. (var, step은 생략가능)
<c:forEach items="${color }" varStatus="i">
<c:out value="${i.count }"/>
<c:out value="${color[i.index] }"/><br/>
</c:forEach>
=> 위의 예문은 카운트숫자 i 를 설정하여 for 문을 돌리는 문장이다.
* forTokens 예문 (StringTokenizer)
<c:forTokens var="one" items="서울|인천,대구,부산,찍고" delims="," varStatus="sts">
<c:out value="${sts.count }:${one}"/>·
</c:forTokens>
=> forTokens 역시 begin, step, end 를 지정할 수 있고, varStatus는 생략가능하다.
* IF 예문
<c:if test="${empty hit }">
<c:set var="hit" value="1" scope="session"/>
세션에 값을 저장하였습니다.
</c:if>
=> empty는 NULL을 판별하는 EL연산자이다.
=> hit 이라는 변수가 NULL 이면 session 스코프에 1이라는 값으로 hit 변수를 생성하는 코드.
=> empty hit 대신에 empty sessionScope.hit 으로 해도 동일한 결과를 얻을 수 있다.
* <c:out> - System.out.println()
1. body가 없는 경우
<c:out value="value" [escapeXml="{true|false}"] [default="기본값"] />
2. body가 있는 경우
<c:out value="value" [escapeXml="{true|false}"] >
기본값
</c:out>
=> escapeXml 속성은 값 중에 포함된 < > & ' " 문자들을 각각 < > & ' " 로 출력한다.
생략할 경우 true가 기본값이다.
=> NULL 값의 처리에 대해서 JSP에서는 "null" 문자열로 출력되었던 것에 비해 JSTL의 스펙에서는 빈문자열("")
또는 기본값으로 처리한다고 명시되어 있다.
* <c:catch/> : Try~Catch
<c:catch var="errmsg">
line1
<%=1/0 %> => 에러가 나는 코드
line2
</c:catch>
<c:out value="${errmsg }"/>
=> 시작 catch 태그에 변수를 선언하면 그 변수에 Exception의 내용이 들어가게 된다.
* <c:choose/>, <c:when test=""/>, <c:otherwise/> : IF~ELSE, SWITCH
<c:choose>
<c:when test="${empty param.name}">
<form>
이름을 적어주세요
<input type="text" name="name">
<input type="submit" value="확인">
</form>
</c:when>
<c:when test="${param.name=='admin' }">
안녕하세요 관리자님.
</c:when>
<c:otherwise>
안녕하세요. <c:out value="${param.name}"/>님.
</c:otherwise>
</c:choose>
* <c:import/>
- 웹어플리케이션 내부의 자원접근은 물론이고, http, ftp 같은 외부에 있는 자원도 가져와서 페이지
내에 귀속시킨다. 자유롭게 가공할 수도 있고, 편집도 가능하다.
- 스트림으로 받아와서 파일로 저장하거나, DB에 입력할 수도 있도록 되어 있다.
<c:set var="url" value="http://www.google.co.kr"/>
<c:import url="${url}" var="u"/> => URL에 해당하는 페이지에 있는 모든 요소를 변수에 할당함.
<c:out value="${url }"/> 가져옵니다.
<base href="<c:out value='${url }'/>"> => base 태그는 해당 URL을 기반으로 상대경로화 하므로 이미지가
깨지지 않도록 만들어 준다.
<c:out value="${u }" escapeXml="false"/> => 위에서 만든 변수를 출력하면 모든 코드가 출력된다.
</base> escapeXml="false" 로 해야 화면이 제대로 보인다.
* <c:url/> : request.getContextPath()
<img src="<c:url value="/images/winter.jpg"/>">
=> 이렇게 c:url 태그의 value로 넣어주는 것 만으로, 현재 컨텍스트의 경로를 갖다가 붙여서 새 문자열을 만든다.
=> context 속성을 지정해 줄 수도 있는데, context 속성을 지정해 주었을 경우에, context, value 는 "/"로 시작
해야 한다.
* <c:redirect/> : request.sendRedirect()
<c:redirect url="jstlcore01.jsp">
<c:param name="number" value="300"/>
</c:redirect>
=> url에 해당하는 페이지로 리다이렉션 하면서 파라미터를 가지고 간다. body 없이 사용 가능하다.
=> c:url 과 마찬가지로 context 속성을 지정하면, value와 context는 "/"로 시작해야 한다.
* <c:param/>
<c:import url="http://bruce.com">
<c:param name="action" value="register"/>
</c:import>
=> 위 태그는 다음과 같은 쓸 수 있다.
<c:import url=http://bruce.com?action=register>
* EL연산자간의 우선순위
1. []
2. ()
3. - (단항) not ! empty
4. * / div % mod
5. + - (이항)
6. < > <= => lt gt le ge
7. == != eq ne
8. && and
9. || or
JSTL과 Velocity를 활용한 UI 레이어 구현 방법
|
|
[ JSP ] 게시판 페이징 처리 로직 JSP
2007/03/09 03:05 |
매번 페이징을 구현할 때마다, 한 두시간 씩 끙끙대는 것 같다. 생각을 의사코드로 적은 후에 그걸 코드화 하는 습관을 들여야하는데, 성질이 급해서 일단 생각을 코드화 해서 버그를 잡은 다음에.. 그제서야 의사코드로 적고 있다. 다음은 생각의 흐름를 정리해본 것이다. ************************************************************************************************
데이터는 DB에서 가져오거나 xml파일을 읽어올 것이다. 페이지를 3 페이지씩 보여줄 수도 있고 [1] [2] [3] [다음] 혹은 10페이지씩 보여줄 수도 있다. [1] [2] [3] [4] [5] ...[10] [다음] 값 셋팅에 따라 달라질 수 있도록 페이지그룹 사이즈(pageGroupSize) 변수를 사용하겠다. 셋팅할 수 있는 값이 페이지그룹 사이즈 말고 또 무엇이 있을까? 한 페이지 당 보여주는 리스트 갯수도 변할 가능성이 많다. 이것은 페이지 사이즈(pageSize) 변수로 놓자. 그러면, 페이징 로직의 결과를 예측하기 위해서 이 두개의 변수에 값을 넣어보자.
이렇게 셋팅을 할 경우, 나오는 결과는? 총 페이지 수(pageCount)는 37 / 4 = 9.25 니까 10 페이지 되겠다. pageCount = (count / pageSize ) + ( count % pageSize == 0 ? 0 : 1); => count % pageSize == 0 ? 0 : 1 이 수식을 풀어서 쓰면 if(count % pageSize == 0) 이렇게 표현된다. ============================================================================= 이 페이지 그룹을 본 후, 현재페이지가 호출될 경우를 생각해보면 5페이지가 호출된다면 numPageGroup이 2인 상태로 페이지바가 보여질 것이다. 그렇다면, 호출된 현재페이지를 가지고 페이지그룹넘버를 구하고, 페이지그룹넘버값으로 페이지바의 첫번째 페이지를 계산해보자. numPageGroup = 올림(currentPage/pageGroupSize) => int numPageGroup = (int) Meth.ceil ((double)currentPage/pageGroupSize) => numPageGroup : 올림(5/3) = 2 startPage = (numPageGroup - 1)*pageGroupSize + 1; => stratPage : (2-1)*3+1 = 4 이제 시작페이지를 가지고 마지막 페이지도 구해보자 endPage = startPage + pageGroupSize -1 => 4+3-1 = 6 이런식으로 시작페이지와 마지막페이지를 구했다면, for문을 돌려서 페이지바를 출력할 수 있을 것이다. 그런데, 마지막 페이지그룹을 위 공식대로 계산하면 endPage가 12페이지가 나온다. 총 페이지수가 10개인데 12페이지까지 출력되면 안 될것이다. 따라서, if문 처리를 추가해주자. if( 마지막페이지 > 총페이지 ) ************************************************************************************************ 로직은 자바 클래스로, 출력부분은 JSTL 코드로 작성했는데.. 페이징 부분 코드는 다음과 같다. [ 자바 클래스 ] //한 페이지 당 보여줄 글 갯수 String pageNum = request.getParameter("pageNum");//페이지 번호
//해당 뷰에서 사용할 속성 request.setAttribute("number", new Integer(number)); <c:if test="${count > 0}"> <c:forEach var="i" begin="${startPage}" end="${endPage}"> <c:if test="${numPageGroup < pageGroupCount}"> |
출처 : Tong - 민뎅님의 java통
|
|
|
JSP 2.0: 뭐가 바뀌었나? - 2부
원문 : http://www.onjava.com/pub/a/onjava/2003/12/03/JSP2part2.html
by Hans Bergsten, JavaServer Pages, 3판의 저자
번역 손권남(kwon37xi_aT_yahoo dOt co DoT kr
2004/07/29
이것은 JavaServer Pages(JSP) 2.0 명세에 추가된 사항들을 설명하는 시리즈의 두번째 글이다. 1부에서는 새로운 표현식(Expression Language)에 대해서 설명했으나, 알아둘게 더 많이 있다. 이번 회에서는 오류 처리 부문에서의 발전된 모습과 새로운 배치 기술자(deployment descriptor) 기능에 대해 알아볼 것이다. 나는 당신이 JSP 1.2에 익숙하고 JSP Standard Tag Library(JSTL)에 대해 들어보기는 했다고 가정하고 글을 썼다.
JSP Error Pages
JSP나 서블릿에서 JSP 오류 페이지를 사용해 보았다면, 이 페이지를 불러들인 예외에 관해 출력하거나 로그 정보를 보여주기를 바랬을 것이다. 하지만 JSP 1.2에서는 그게 그리 쉬운일이 아니다. 그 이유는 errorPage 선언을 가진 서블릿과 JSP가 예외를 request 속성(attribute으로 전달하고, 서로 다른 속성 이름을 사용하기 때문이다. 오직 JSP 속성 이름으로 전달된 예외만이 자동으로 JSP 오류 페이지에 나타날 뿐이다(exception 스크립트 변수나 ${pageContext.exception} EL을 이용해서).
JSP 2.0에서는 서블릿 명세와 같은 속성이름 - javax.servlet.error.exception으로 변경함으로써 이 문제를 해결했다. 또한, 내장 EL pageContext 변수에 errerData라는 새로운 프라퍼티가 있어, 발생한 문제에 대한 다른 정보들도 제공해 준다. errerData 속성은 javax.servlet.jsp.ErrorData 클래스의 인스턴스이며 다음과 같은 프라퍼티로 빈(Bean)으로써 사용할 수 있다.
프라퍼티 | 자바 형 | 설명 |
---|---|---|
requestURI | String | 요청이 실패한 URI. |
servletName | String | 실패한 JSP나 서블릿의 이름. |
statusCode | int | 실패 상태 코드. |
throwable | Throwable | 오류 페이지를 불러들인 예외. |
저 프라퍼티들을 사용하는 JSP 오류 페이지의 예제가 있다 :
<%@ page isErrorPage="true" contentType="text/html" %>
<%@ taglib prefix="log" uri="http://jakarta.apache.org/taglibs/log-1.0" %>
Sorry, but things didn't work out as planned. I've logged as much as
I know about the problem, so rest assured that my master will look
into what's wrong as soon as he's sober.
<jsp:useBean id="now" class="java.util.Date" />
<log:fatal>
-----
${now}
Request that failed: ${pageContext.errorData.requestURI}
Status code: ${pageContext.errorData.statusCode}
Exception: ${pageContext.errorData.throwable}
-----
</log:fatal>
이 페이지에서 사용자들을 다소 안심시켜주는 메시지와 아파치 자카르타 Taglib 프로젝트의 Log 태그 라이브러리를 사용해서 상세한 로그를 보여준다.
특정 파일이 서블릿과 JSP 페이지의 오류 페이지임을 선언하기 위해, web.xml 파일에서 <error-page> 요소를 사용할 수도 있다.
...
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/error.jsp</location>
</error-page>
<error-page>
<exception-code>500</exception-code>
<location>/error.jsp</location>
</error-page>
...
특정 JSP 페이지에 대해서 다른 오류 페이지를 보여주고자 한다면, 페이지 지시자의 errorPage 속성을 이용해서 web.xml의 선언을 오버라이드 할 수 있다.
<%@ page errorPage="/other_error.jsp" contentType="text/html" %>
JSP 문법 오류 보고
JSP 1.2와 2.0간의 작지만 중요한 차이점은 JSP 2.0이 JSP 컨테이너가 jsp:id 기능을 지원하도록 한다는 점이다. 이것은 1.2에서는 단지 "권장사항"일 뿐이었다. JSP 개발자들에게 이것이 무엇을 의미하는가? 바로 JSTL이나 커스텀 태그 라이브러리의 문법 오류 내용이 더욱 쓸만해진다는 것이다.
어떻게 작동하냐구? 컨테이너가 JSP 페이지를 실행 가능한 형태(서블릿 클래스)로 변환할 때, 페이지 내에 선언된 모든 태그 라이브러리를 살펴본다. 한 개 혹은 여러개의 태그 라이브러리 선언이 태그 라이브러리 유효성 검사기(TLV; Tag Library Validator)를 가지고 있다면, 컨테이너가 페이지를 받아들이기 전에 TLV가 페이지의 유효성을 검사할 기회를 주게 된다.
이것은 TLV가 페이지를 분석할 때 XML 뷰(View)를 제공 한다. XML 뷰는, 이름에서 보듯이, 모든 정규 JSP 요소와 템플릿 텍스트(HTML 같은..)를 웰 폼드(Well-Formed) XML 문서로 변환한 것이다.
XML 뷰는 TLV가 모든 커스텀 액션이 올바르게 사용되었는지를 확신하도록 파싱 하는 것을 쉽게 만들어 준다.(예를 들어, 엘리먼트들이 올바르게 중첩되었는가, 함께 나와서는 안되는 속성들이 동일한 액션 요소에 같이 사용되지는 않았는가?)
여기 jsp:id가 온 곳이 있다. 컨테이너는 페이지의 각 커스텀 액션 요소에 ID를 부여하고 ID와 요소의 위치(파일, 줄 번호, 행 번호)간의 맵을 유지한다. 컨테이너는 jsp:id 속성을 ID를 값으로 하여 XML 뷰의 모든 커스텀 액션 요소에 추가한다. TLV가 오류를 발견하면, 오류 메시지에 잘못된 액션 요소의 jsp:id 속성 값을 추가하여 컨테이너에 넘겨주게 된다. 컨테이너는 매핑 정보를 이용해서 잘못된 커스텀 액션 요소의 위치 정보를 사용자에게 보여주는 에러 메시지에 추가한다. 이로 인해 문제점을 찾고 수정하기가 쉬워진다.
모든 JSTL 라이브러리는 TLV를 갖고 있다. 나는 당신이 작성할 커스텀 라이브러리에 TLV를 만들고, 당신이 사용할 서드 파티 라이브러리 개발자들에게도 그렇게 하도록 주장할것을 강력히 권한다.
당신이 XML 뷰와 TLV를 잘 모르겠다면, 내가 2001년에 JSP 1.2에 대해 쓴 글에 간단한 설명을 해 뒀으니 보기 바란다.(JSP 1.2: Great News for the JSP Community).
JSP 배치 기술자(deployment descriptor) 새소식
JSP 2.0 은 이전 JSP 명세에서 그랬던 것 처럼 서블릿 명세에 의해 정의된 배치 기술자(web.xml)파일 형식을 사용한다.
어쨌든, JSP 2.0에는 두가지 중요한 변화가 있다: web.xml의 규칙이 XML 스키마(Schema)로 정의되었고, 대부분의 JSP 관련 설정 아이템들이 새로운 XML 요소로 이동해서 JSP 명세의 제어 하에 놓이게 되었다(서블릿과 JSP 명세간의 결합도(coupling)를 최소화 하기 위해서).
XML 스키마는 XML 문서의 문법 규칙을 설명하는 XML 언어이다(예. 요소가 어떻게 중첩되는가, 요소가 어떤 값을 가질 수 있는가, 값이 중복 될 수 있는가, 그리고 그 외 여러가지). 이건 매우 복잡한 명세이지만 운좋게도, 당신이 web.xml을 작성하기 위해 그 XML 스키마의 문법 규칙을 이해해야 할 필요는 없다. 왜냐하면 서블릿과 JSP 명세는 이해하기 쉬운 도표를 제공하기 때문이다(나의 JavaServer Pages, 제 3판 책은 읽기 쉬운 문법 도표를 포함하고 있다). 그래도 XML 스키마를 공부하고 싶다면 W3C 웹 사이트를 참조하라.
규칙을 예전의 DTD 대신 XML 스키마를 이용해서 선언하는 것의 장점은 XML 스키마는 더욱 세부적이라 web.xml 파일을 파싱할 때 더 많은 오류를 찾을 수 있고, 덕분에 컨테이너들 간의 더 나은 이식성을 가져온다는 것이다.
내 생각에 당신이 더욱 기뻐할 다른 장점으로는 XML 스키마가 web.xml의 최상위 요소들이 어떤 순서로 배열되도 상관없게 만들어 준다는 것이다. 이전 버전의 서블릿과 JSP 명세에서는 예를 들어 <error-page> 요소가 <welcome-file-list> 요소보다 먼저 오기라도 하면 오류를 발생시켰지만, 새로운 버전에서는 그런것도 아무 상관 없다. 그렇더라도 최상위 요소 안에 오는 요소들의 순서는 여전히 엄격한 순서에 따라야만 한다. 어쨌든 당신은 최상위 요소들을 마음대로 할 수 있는 자유를 가졌다.
<servlet> 요소 안에 중첩된 <jsp-file> 요소를 제외하고 모든 JSP 관련 요소들은 <jsp-config>라는 최상위 요소 아래에 함께 묶이게 되었다. <jsp-config>안에 중첩해서 <taglib>요소도 JSP 1.2에서와 같은 문법과 의미로 사용할 수 있다. 하지만 사실 JSP 1.2 이후의 컨테이너에서는 <taglib>가 불필요하다. 컨테이너가 배치된 JAR파일로 부터 자동으로 태그 라이브러리 정의를 뽑아내기 때문이다.
새로운 <jsp-property-group> 하위요소(subelement)는 훨씬 더 관심있게 볼만하다. 이 요소는 특정한 URL패턴에 부합하는 JSP 페이지의 집합에 특정한 설정을 적용할 수 있게 한다. 예를 들어보면:
...
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<scripting-invalid>true</scripting-invalid>
</jsp-property-group>
</jsp-config>
...
<url-pattern> 요소는 설정을 적용할 JSP 페이지의 집합을 구분한다.다른 중첩된 요소들은 설정 옵션을 정의한다. 이 예제에서는, <scripting-invalid> 요소가 모든 JSP 페이지에서 JSP 스크립팅 요소(Java 코드 같은)를 사용할 수 없게 만든다.
대체로(all in all), 다음과 같은 설정 요소들을 <jsp-property-group> 요소 안에 중첩해서 <scripting-invalid> 요소와 같은 방식으로 사용할 수 있다:
요소 | 설명 |
---|---|
<el-ignored> | true이면, URL 패턴이 일치하는 JSP 페이지 내의 EL 표현식을 EL 표현식이 아니라 일반적인 텍스트로써 다루게 된다. 이것은 JSP 1.2에서 EL 표현식처럼 보이는 텍스트를 JSP 2.0 으로 천천히 마이그레이션 하는데 유용하게 사용될 수 있다. 'elIgnored' 페이지 지시자 속성을 이용해서 JSP 2.0으로 변환한 페이지들에서 선택적으로 EL을 사용하게 만들 수도 있다. |
<scripting-invalid> | true이면, URL 패턴이 일치하는 JSP 페이지에서 스크립팅 요소를 사용하면 번역시(translation-time) 오류를 발생시킨다. |
<page-encoding> | URL 패턴이 일치하는 모든 JSP 페이지에 대해 지정된 페이지 문자 인코딩을 사용한다. 모든 JSP 페이지에 인코딩을 지정하는 대신 사용할 수 있고, JSP 페이지에 몇몇 특수한 파일 인코딩(예를들면 EBCDIC)을 지정할 수 있는 유일한 방법이다. |
<include-coda> | URL 패턴이 일치하는 모든 JSP 페이지의 끝에 자동으로 포함될 파일의 컨텍스트에 상대적인 경로를 지정한다. 한개의 <jsp-property-group> 요소 안에서 여러개의 요소를 사용할 수도 있고, 여러개의 <jsp-property-group> 요소에서 사용할 수도 있다. |
<include-prelude> | URL 패턴이 일치하는 모든 JSP 페이지의 시작 부분에 자동으로 포함될 파일의 컨텍스트에 상대적인 경로를 지정한다. 한개의 <jsp-property-group> 요소 안에서 여러개의 요소를 사용할 수도 있고, 여러개의 <jsp-property-group> 요소에서 사용할 수도 있다. |
<is-xml> | true이면, URL 패턴이 일치하는 모든 JSP 페이지가 JSP XML 문법을 사용한다고 지정하는 것이다(이것이 JSP 문서이다). |
마무리
이번 회에서는 JSP 2.0 오류 처리방식의 개선점과 새로운 배치 기술자의 기능들을 설명했다. 이 시리즈의 다음번 글에서는 JSP 2.0이 XML 컨텐트를 위해 JSP를 사용하는 것이 어떻게 쉬워졌는지와 커스텀 태그 라이브러리에 관련된 새로운 기능들을 알아볼 것이다.
JSP 2.0의 새로운 기능들을 접해보고 싶다면, Apache Tomcat 5를 사용해보라고 권하고 싶다. 톰캣은 새로운 JSP 스펙을 최초로 구현한 JSP 컨테이너이다. Jakarta Project 사이트에서 구할 수 있다.
JSP 2.0 프로그래밍
목차
java 환경변수 설정
시작 > 설정 > 제어판 > 시스템 > 고급 을 클릭하고 '환경변수'버튼을 클릭.
시스템 변수 부분에서 Path라는 변수 이름을 찾아 선택한 후 '편집' 버튼을 클릭.
변수 값 뒤쪽에 JDK bin 경로를 추가. (오라클을 사용한다면 가장 앞쪽에 추가)
'새로만들기' 버튼을 누르고 JAVA_HOME을 만들고 JDK 경로를 입력.
CLASSPATH를 만들고 %classpath%;. 를 입력.
Request로 checkbox 파라미터 받아오기#
checkbox 사용시 같은 이름의 파라미터의 값이 여러 개 전달 될 수 있기 때문에, getParameter() 메소드로는 모두 다 읽어 오지 못한다. getParameterValues() 메소드를 사용해야 한다.
- Enumeration enum = request.getParameterNames();
- while(enum.hasMoreElements()){
- String name = (String)enum.nextElement();
- }
getParameterValues() 메소드의 리턴값은 java.util.Enumeration 이다.
checkbox와 radiobutton의 경우 선택하지 않게 되면 파라미터 자체가 전송되지 않지만 텍스트 입력과 같은 일반적인 입력요소들은 값을 입력하지 않더라도 빈 문자열("")이 파라미터 값으로 전달된다.
한글 파라미터값 인코딩 하기#
알파벳과 숫자 그리고 몇몇 기호를 제외한 나머지 글자들을 URL에 포함시키기 위해서는 인코딩 작업을 해주어야 한다.
- String value = "자바"
- String encodedValue = URLEncoder.encode(value);
- response.sendRedirect("/index.jsp?name="+encodedValue);
최근의 웹 브라우저와 웹 서버들은 한글 파라미터 값을 인코딩하지 않더라도 알맞게 처리해 주지만, 표준에 따라서 인코딩을 해주는 것이 올바른 습관이여, 나중에 서버가 변경되더라도 소스 코드를 변경하는 일이 줄어든다.
pageContext 기본객체#
하나의 페이지와 1대1로 매핑되는 객체로서 다른 기본 객체를 구할때 주로 사용한다.
request 기본 객체를 구하고 싶다면.
- HttpServletRequest httpRequest = (HttpServletRequest)pageContext.getRequest();
getRequest() 메소드의 리턴타입은 ServletRequest 이기 때문에 HTTP 요청을 처리하는 경우 형변환을 해준다.
Application 기본객체#
웹 어플리케이션 초기화 파라미터 읽어 오기#
web.xml 파일에 다음 태그를 추가한다.
- <context-param>
-
<description>파라미터 설명(선택)</description>
<param-name>파라미터 이름</param-name>
- <param-value>파라미터 값</param-value>
- </context-param>
웹페이지에서 읽어올 수 있다.
- application.getInitParameter(initParamName)
로그메세지 기록하기#
- application.log("로그메세지 기록")
- 또는
- log("로그메세지 기록") //JSP 자체지원 메소드
기본 데이터 타입(int, double)을 기본객체에 속성값으로 지정하기#
기본객체의 속성값은 Object 타입으로 모든 클래스 타입을 지정할 수 있지만 기본 데이터 타입은 래퍼 클래스를 이용해 Object 타입으로 바꿔줘야한다.
- Integer intValue = new Integer(100);
- request.setAttrubute("ratio",intValue);
읽어올땐
- Integer intValue = (Integer)request.getAttribute("ratio");
- int value = intValue.intValue();
JSP 페이지 에러 처리#
에러 페이지 지정하기#
jsp 페이지에 에러페이지를 지정한다.
- <%@ page errorPage = "/error/viewErrorMessage.jsp" %>
지정한 경로로 보이고 싶은 에러페이지를 작성한다. 에러페이지에는 다음 페이지 디렉티브를 추가한다.
- <%@ page isErrorPage = "true" %>
- <%=exception.getClass().getName()%> //exception 기본객체 클래스 이름(선택)
- <%=exception.getMessage()%> //예외 메세지(선택)
isErrorPage 속성 값을 true 로 지정하면 jsp 페이지가 에러페이지가 되며 exception 기본객체는 에러페이지에서만 사용할 수 있다.
에러 코드별 에러 페이지 지정하기#
web.xml 에서 에러 상태 코드별로 보여줄 페이지를 지정할 수 있다.
- <error-page>
- <error-code>500</error-code>
- <location>/error/error500.jsp</location>
- </error-page>
다음 에러 페이지를 작성하는데 다음 구문을 꼭 추가한다.
- <% response.setStatus(HttpServletResponse.SC_OK);%>
응답 코드를 200(정상상태)으로 지정한다. 응답 코드를 200 으로 지정하지 않으면 웹 브라우저에는 500 응답 코드가 전달되며, 이 경우 웹 브라우저는 자체적으로 500 에러일 때 보여주는 화면을 출력한다. 따라서 꼭 응답 코드를 지정해 주어야 한다.(웹 콘테이너에 따라서 이렇게 안 해주어도 된다.)
예외 종류별 에러 페이지 지정하기#
에러 코드별 에러 페이지 지정 방법과 거의 같은 방법으로 지정한다.
web.xml에 에러페이지를 지정한다.
- <error-page>
- <error-type>java.lang.NullPointerException</error-type>
- <location>/error/errorNullPointer.jsp</location>
- </error-page>
다음 에러 페이지를 작성하는데 다음 구문을 꼭 추가한다.
- <% response.setStatus(HttpServletResponse.SC_OK);%>
에러 페이지 우선순위#
우선순위는 다음과 같다.
- page 디렉티브의 errorPage 속성에서 지정한 에러 페이지
- web.xml 파일의 <exception-type>에서 지정한 예외타입(예외 종류별 에러)
- web.xml 파일의 <exception-code>에서 지정한 예외타입
절대 경로와 상대 경로#
절대 경로#
웹 어플리케이션의 폴더를 루트로 사용하는 경로이다.
- /to/to.jsp
'/to/to.jsp' 는 '/'로 시작하는데, 맨 앞의 '/'가 절대 경로의 기준점인 웹 어플리케이션 폴더를 의미한다.
상대경로#
현재 JSP 페이지 기준으로 경로를 결정한다. 예를 들어 to.jsp 기준으로 봤을때 from.jsp의 상대경로는 다음과 같다.
- ../from/from.jsp
'..'는 상위 디렉터리를 나타낸다.
요청 흐름 제어하기#
<jsp:forward>액션 태그를 이용한 JSP 페이지 이동#
요청 흐름이 이동할때(A1.jsp에서 A2.jsp 로 흐름이 이동할때) A1.jsp 에서 사용한 request 기본객체와 response 기본객체가 A2.jsp로 전달된다.
또 A1.jsp가 아닌 A2.jsp 에서 생성한 응답 결과가 웹 브라우저에 전달된다.(요청 흐름이 이동하면 A1.jsp에서 생성했던 출력 결과는 모두 제거된다.)
A1.jsp의 출력 결과가 웹브라우저에 전송이 되지 않는 이유는 출력 버퍼 때문이다. <jsp:forward>태그를 만나면 출력 버퍼를 비우고 A2.jsp 의 출력 결과를 저장하기 때문에 A1.jsp의 출력 결과는 모두 제거된다.
따라서 페이지에 출력버퍼가 없거나(page 디렉티브에서 쓰지 않도록 설정) 출력 버퍼가 꽉 차서 <jsp:forward>태그를 만나기 전에 비워진다면 페이지는 에러를 발생한다.
또 JSP 코드에서 <jsp:forward>태그를 만나면 이후에 코드는 실행하지 않고 바로 요청 흐름을 이동시키기 때문에 주위 해야 한다.
<jsp:forward>는 웹 콘테이너 내에서 요청의 흐름을 이동시키기 때문에, 웹 브라우저는 전혀 요청의 흐름이 이동됐다는 사실을 알지 못한다. 따라서 웹 브라우저의 주소는 변경되지 않으며 출력결과도 A1.jsp의 결과라고 여기게 된다.
파라미터 전달법
- <jsp:forward page="moveTo.jsp">
- <jsp:param name="first" value="BK"/> //표현식 사용불가
- <jsp:param name="last" value="choi"> //표현식 사용가능
- </jsp:forward>
리다이렉트와 자바 스크립트를 이용한 페이지 이동#
<jsp:forward> 태그가 웹 콘테이너 내부에서 이루어지는 페이지 흐름이라면 리다이렉트 방식이나 자바 스크립트 방식은 웹 브라우저 차원에서 페이지가 이동하길 원할 때 쓴다.
- <%
- response.sendRedirect("/to/move.jsp");
- %>
리다이렉트 방식은 웹 브라우저에 어떤 페이지로 이동하라http://subclipse.tigris.org/update_1.0x는 명령를 전달하는 반면, 자바 스크립트 방식은 웹 브라우저가 자바 스크립트를 실행하는 시점에서 페이지를 이동하게 된다.
- <script language="JavaScript">
- location.href = "/list.jsp";
- </script>
파라미터를 전달하는 방법은 get방식으로 주소 뒤에 파라미터를 붙이는 방법이 있다.
보통 글 쓰기 완료 후, 글 목록으로 이동할때 보통 리다이렉트나 자바 스크립트 방식을 사용한다.
Subclipse 설치방법#
이클립스의 메뉴에서
Help -> Software Updates -> Find and Install 을 선택한다.
새로 생긴 창에서 Next를 한번 누르면 New Remote Site 버튼을 볼 수 있다. 클릭한 후
http://subclipse.tigris.org/update_1.0.x 를 주소창에 입력한다.
그러면 프로그램을 다운로드 하여 나머지 설치과정을 진행할 수 있다.
JSP 페이지 모듈화#
<jsp:include> 액션 태그를 이용한 페이지 모듈화#
- <jsp:include page="포함할페이지" flush="true"/
flush : 지정한 jsp페이지를 실행하기 전에 출력 버퍼의 플러스 여부를 지정한다. true이면 <jsp:include> 액션 태그를 만나기 전까지 출력버퍼에 쌓여있던 내용이 출력되고 페이지 흐름이 이동된다.
- 요청 파라미터를 통해서 값 전달하기
- <jsp:include page="/module/top.jsp" flush="false">
- <jsp:param name="param1" value="value1"/>
- <jsp:param name="param2" value="value2"/>
- </jsp:include>
request 기본객체를 이용해서도 값을 전달할 수 있다.
레이아웃 템플릿 구현 방법#
useTemplate.jsp -> template.jsp -> left.jsp
(메인페이지를 지정하여 파라미터로 넘겨줌) (페이지의 레이아웃만 구현) top.jsp
내용페이지
include 디렉티브를 이용한 중복된 코드 삽입#
- <%@ include file="포함할 파일.jspf" %>
JSP 파일을 자바 파일로 변환하기 전에 지정된 파일의 내용을 해당 위치에 삽입시키고, 그 결과 생긴 자바 파일을 컴파일한다. 따라서 삽입되는 파일에서 선언한 변수를 삽입하는 JSP 에서 사용할 수 있다.
주로 확장자로 jspf를 사용한다.
- 모든 JSP 페이지에서 사용되는 공용 변수 지정
- 저작권 표시와 같은 간단하면서도 모든 페이지에서 중복되는 문장
등에 사용된다.
쿠키#
웹 서버가 웹 브라우저에 정보를 전달하는 방법.
'쿠키'는 웹 브라우저가 보관하고 있는 데이터로 웹 서버에 요청을 보낼 때 함께 전송된다. 웹 서버와 웹 브라우저 양쪽에서 생성할 수 있다.
- 쿠키 생성 - jsp 에서 쿠키는 주로 웹 서버 측에서 생성, 응답데이터와 함께 웹 브라우저에 전송
- 쿠키 저장 - 쿠키 저장소에 보관
- 쿠키 전송 - 웹 브라우저는 한번 저장된 쿠키를 매번 요청이 있을 때마다 웹 서버에 전송
일단 웹 브라우저에 쿠키가 저장되면 쿠키가 삭제되기 전까지는 매번 웹서버에 전송되기 때문에, 웹 어플리케이션을 사용하는 동안 지속적으로 유지해야 하는 정보는 쿠키를 사용해서 저장하면 된다.
쿠키의 구성#
- 이름
- 값
- 유효 시간
- 도메인 - 쿠키를 전송할 도메인
- 경로 - 쿠키를 전송할 요청 경로
쿠키의 사용#
쿠키의 생성
- <%@ page import = "java.net.URLEncoder" %>
- <%
- Cookie cookie = new Cookie("cookieName",URLEncoder.encode("하하"));
- response.addCookie(cookie);
- %>
알파벳과 숫자 등의 값이 아닌 경우 BASE64 인코딩으로 처리해 주어야 한다.
쿠키값 읽어 오기
- Cookie[] cookies = request.getCookies();
쿠키는 응답헤더 형태로 웹 브라우저에 전달되기 때문에, 쿠키 역시 출력버퍼가 플러시된 이후에는 새롭게 추가할 수 없다. 쿠키의 추가 및 변경 작업은 반드시 출력 버퍼가 플러시 되기 전에 처리해 주어야 한다.
쿠키의 삭제
- cookie.setMaxAge(0);
유효시간을 0으로 지정해주면 웹브라우저가 관련 추키를 삭제.
쿠키의 도메인
아마도 전에도 이런 질문과 답변이 있었지만.. 중복 질문이 되므로 차후 사용을 위해서 답글을 달아 놓겠습니다.
internal server error에 대한 정답은 없습니다. 왜냐하면 이것은 HTTP 명세에 정의된 것으로 서버의 모든 예외 상황을 나타내는 것이기 때문입니다.
<error-page> <error-code>404</error-code> <location>/errors/404.jsp</location> </error-page>
<error-page> <error-code>500</error-code> <location>/errors/500.jsp</location> </error-page>
<error-page>
<exception-type>java.lang.Throwable</exception-type>
<location>/errors/throwable.jsp</location>
</error-page>
위는 web.xml 파일에 지정하는 것입니다.
<%@ page isErrorPage="true" errorPage="특정에러발생시.jsp" %>
<%= exception.getMessage() %>
(1) exception 내장 객체는 isErrorPage="true"인 곳에서만 사용 가능
(2) errorPage 속성은 옵션 속성이므로 생략 하면 web.xml에서 지정한 위치로 갈 수 있음. 예를 들어 다른 곳은 모두 같은 기본 에러 페이지를 사용하지만, 로그인에 대해서만 따로 에러 페이지를 지정하려면 위와 같이 지정하게 됨.
JExcelApi(jxl.jar 다운로드)
http://www.andykhan.com/jexcelapi/index.html 에서 download JExcelApi에 가면
(http://www.andykhan.com/jexcelapi/download.html) tar.gz로 되어 있는데 압축풀면 jxl.jar있음
POI다운로드
http://jakarta.apache.org/poi/index.html /download/release
[엑셀파일읽기]
<%@ page language="java" %>
<%@ page contentType="text/html; charset=euc-kr"%>
<%@ page import="java.io.File,jxl.*"%>
<%
Workbook workbook = Workbook.getWorkbook(new File("E:/_Workspace/blr/blr/cms/excel/data/Book1.xls"));
Sheet sheet = workbook.getSheet(0);
Cell a1 = sheet.getCell(0,0);
Cell b2 = sheet.getCell(1,1);
Cell c3 = sheet.getCell(0,2);
String stringa1 = a1.getContents();
String stringb2 = b2.getContents();
String stringc3 = c3.getContents();
workbook.close();
%>
<%=stringa1%>
<%=stringb2%>
<%=stringc3%>
[엑셀파일로 저장]
<%@ page language="java" %>
<%@ page contentType="text/html; charset=euc-kr"%>
<%@ page import="java.io.File,java.util.Date,jxl.*,jxl.write.*"%>
<%
WritableWorkbook workbook = Workbook.createWorkbook(new File("E:/_Workspace/blr/blr/cms/excel/data/aaa.xls"));
WritableSheet sheet = workbook.createSheet("First Sheet", 0);
Label label = new Label(0,2,"A label record");
sheet.addCell(label);
// java.lang.Number 인지 jxl.write.Number 구분해야지^^
jxl.write.Number number = new jxl.write.Number(3,4,3.1415);
sheet.addCell(number);
workbook.write();
workbook.close();
%>
[POI로 테이블에 값넣기] poi.jsp참조
/***************************************************************************************/
엑셀파일처리 POI,JXL비교해 보았습니다.
10000번 for문을 돌려 첫번째 칼럼에 데이타를 입력하였습니다.
이걸 10번 반복한 체크시간입니다.
측정시간단위(밀리세컨)
D:\>java writeTest
============= jxl write time =============
0=>write time::719
1=>write time::281
2=>write time::250
3=>write time::266
4=>write time::234
5=>write time::266
6=>write time::234
7=>write time::266
8=>write time::234
9=>write time::329
============= end =============
============= poi write time =============
0=>write time::1110
1=>write time::922
2=>write time::1031
3=>write time::734
4=>write time::1328
5=>write time::766
6=>write time::797
7=>write time::1515
8=>write time::1719
9=>write time::2890
============= end =============
거의 2~3배 가까이 시간의 차이가 있더군여.. 단순히 엑셀파일만 다룬다면
jxl패키지를 사용하시는게 훨씬 좋을듯 합니다.
<%@ page language="java" contentType="application/vnd.ms-excel; name='My_Excel', text/html; charset=euc-kr"%>
<%
response.setHeader("Content-Disposition",
"inline; filename=excel_" +new java.sql.Date(System.currentTimeMillis()) + "_.xls");
response.setHeader("Content-Description", "JSP Generated Data");
%>
<html><body><table><tr><td>아아아</td><td>우우우</td></tr>
<tr><td>우이이</td><td>아요에</td></tr>
</table></body></html>
예제인데요. 정말 쉽다 ^^;
브라우져로 날짜별로 받습니다 jsp호출시 excel 파일로 인식하여 다운로드를 하거나, 열게 됩니다.
JExcelApi(jxl.jar 다운로드)
http://www.andykhan.com/jexcelapi/index.html 에서 download JExcelApi에 가면
(http://www.andykhan.com/jexcelapi/download.html) tar.gz로 되어 있는데 압축풀면 jxl.jar있음
POI다운로드
http://jakarta.apache.org/poi/index.html /download/release
[엑셀파일읽기]
<%@ page language="java" %>
<%@ page contentType="text/html; charset=euc-kr"%>
<%@ page import="java.io.File,jxl.*"%>
<%
Workbook workbook = Workbook.getWorkbook(new File("E:/_Workspace/blr/blr/cms/excel/data/Book1.xls"));
Sheet sheet = workbook.getSheet(0);
Cell a1 = sheet.getCell(0,0);
Cell b2 = sheet.getCell(1,1);
Cell c3 = sheet.getCell(0,2);
String stringa1 = a1.getContents();
String stringb2 = b2.getContents();
String stringc3 = c3.getContents();
workbook.close();
%>
<%=stringa1%>
<%=stringb2%>
<%=stringc3%>
[엑셀파일로 저장]
<%@ page language="java" %>
<%@ page contentType="text/html; charset=euc-kr"%>
<%@ page import="java.io.File,java.util.Date,jxl.*,jxl.write.*"%>
<%
WritableWorkbook workbook = Workbook.createWorkbook(new File("E:/_Workspace/blr/blr/cms/excel/data/aaa.xls"));
WritableSheet sheet = workbook.createSheet("First Sheet", 0);
Label label = new Label(0,2,"A label record");
sheet.addCell(label);
// java.lang.Number 인지 jxl.write.Number 구분해야지^^
jxl.write.Number number = new jxl.write.Number(3,4,3.1415);
sheet.addCell(number);
workbook.write();
workbook.close();
%>
[POI로 테이블에 값넣기] poi.jsp참조
/***************************************************************************************/
엑셀파일처리 POI,JXL비교해 보았습니다.
10000번 for문을 돌려 첫번째 칼럼에 데이타를 입력하였습니다.
이걸 10번 반복한 체크시간입니다.
측정시간단위(밀리세컨)
D:\>java writeTest
============= jxl write time =============
0=>write time::719
1=>write time::281
2=>write time::250
3=>write time::266
4=>write time::234
5=>write time::266
6=>write time::234
7=>write time::266
8=>write time::234
9=>write time::329
============= end =============
============= poi write time =============
0=>write time::1110
1=>write time::922
2=>write time::1031
3=>write time::734
4=>write time::1328
5=>write time::766
6=>write time::797
7=>write time::1515
8=>write time::1719
9=>write time::2890
============= end =============
거의 2~3배 가까이 시간의 차이가 있더군여.. 단순히 엑셀파일만 다룬다면
jxl패키지를 사용하시는게 훨씬 좋을듯 합니다.
<%-- requestSession.jsp --%>
%@page import="java.util.*" contentType="text/html;charset=euc-kr"%
<HTML><HEAD><TITLE>세션과 쿠키정보</TITLE></HEAD>
<BODY style="font-size:9pt">
<H3> 세션 정보출력</H3>
<%
HttpSession sess = request.getSession(true);
out.print("<b>[Requested Session유효여부]:</b>" + request.isRequestedSessionIdValid() + "<BR>");
out.print("<b>[Session 쿠키사용여부]:</b>" + request.isRequestedSessionIdFromCookie() + "<BR>");
out.print("<b>[Session URL사용여부]:</b>"
+ request.isRequestedSessionIdFromURL() + "<BR>");
out.print("<b>[Requested Session Id]:</b>" + request.getRequestedSessionId() + "<BR>");
out.print("<b>[Session ID]:</b>" + sess.getId());
%>
<H3>쿠키정보출력</H3>
<%
Cookie [] cookies = request.getCookies();
for(int i = 0; i < cookies.length; i++) {
out.print("<b>" + cookies[i].getName() + "</b>: " + cookies[i].getValue());
}
%>
</FONT></BODY></HTML>
- 2008/10/13 Metro 1.3으로 첨부 파일 보호
- 2008/10/12 진정한 추상화: JSF 2.0의 컴포지트 UI 구성 요소 -- 2부
- 2008/10/12 진정한 추상화: JSF 2.0의 컴포지트 UI 구성 요소 -- 1부
- 2008/10/12 Phobos and jMaki를 사용하여 Ajax 지원 웹 애플리케이션 빌드
- 2008/08/20 Groovy, Grails, MySQL 및 Java Persistence API의 조합
- 2008/08/13 P6Spy 및 GlassFish 연결 풀을 사용하여 데이터베이스 작업 추적
- 2008/07/09 EclipseLink를 사용하여 JPA에서 반복 불가능한 읽기 방지
- 2008/07/02 OpenSolaris에서 PHP와 함께 jMaki 사용
- 2008/05/20 GlassFish 서블릿 컨테이너에 인증 메커니즘 추가
- 2008/04/28 jMaki 이벤트 처리 방법
- 2008/04/07 Metro를 이용한 웹 서비스용 보안 대화
- 2008/04/04 애플리케이션 클라이언트에서 여러 웹 서비스 참조
- 2008/03/13 SIP 서블릿을 사용하여 자바 EE에 음성 추가하기
- 2008/03/10 상태 유지 세션 빈에서의 확장 지속성 컨텍스트
- 2008/02/26 Maven을 지원하는 JAX-WS 사용
- 2008/02/26 Metro를 사용하여 Kerberos 기반의 안전한 서비스 구축
- 2008/02/20 레슨: JavaBeans의 개념
- 2008/02/20 스윙 애플리케이션 프레임워크 소개
- 2008/02/13 GlassFish v2: Open for Business
- 2008/01/18 GlassFish v2에서 SSL 사용하기
- 2007/12/04 Java에서 RESTful 웹 서비스 구현하기 (1)
- 2007/12/04 스팸에 종지부를 찍다: RSS로 마이그레이션하는 SDN 뉴스레터
- 2007/11/12 Java 및 .NET 기술을 사용하여 Ajax를 통한 상호운용성 실현 (1)
- 2007/11/12 Metro와 .NET 간의 상호운용성 테스트
- 2007/11/12 Dynamic Faces를 사용한 Client-Side Polling
- 2007/09/03 현지화된 메시지 로깅 (23)
- 2007/09/03 Attach API (18)
- 2007/08/20 Japex 및 WSTest를 사용하는 성능 회귀 테스트 (14)
- 2007/07/23 매쉬업(Mashup) 스타일, 서버 사이드 매쉬업 (14)
- 2007/07/23 Java Persistence를 최상으로 구현하는 방법 (12)
JSTL
소개
- J2EE 소형 클라이언트 기술인 JSP(JavaServer Pages)가 지난 몇 년 동안 널리 일반화되면서 독립적인 개발자들은 많은 사용자 정
의 JSP 태그 라이브러리를 만들었습니다. 이러한 태그 라이브러리는 대부분 서로 다른 목표를 달성하도록 작성되었지만 반복, 조건 등의 일
반적인 작업을 위한 유사한 솔루션을 제공하는 경향이 있습니다.
유사하고 일반적이 문제점을 해결하는 독립적인 태그 라이브러리에 대한 필요성을 줄이기 위해 Java Community Process(JSR 52)의 지
원하에 JSTL(JavaServer Pages Standard Tag Library)이 개발되었습니다. JSTL은 이러한 일반 기능을 처리하는 하나의 표준 솔루션
을 제공합니다. (말그대로 표준태그라이브러리) - JSTL의 주요 강점 중 하나는 서블릿 컨텍스트에 저장된 데이타 같은 애플리케이션 데이타를 액세스 및 조작하는 쉬운 방법을 제공하는 간
단한 EL을 사용할 수 있다는 것입니다.
설치 방법
- http://cvs.apache.org/builds/jakarta-taglibs/nightly/ 에서 다운
\jakarta-taglibs-20051024\jakarta-taglibs\standard\lib
에서 jstl 과 standard 파일 을 이 두개의 jar 파일을 우리의 웹애플리케이션의 /WEB-INF/lib 폴더에 넣습니다
그 다음 tld 폴더의 tld 파일을 /WEB-INF/lib/tld 폴더 아래 넣습니다.
- web.xml 에
<!-- jstl 1.2 taglig -->
<taglib>
<taglib-uri>jstl-c</taglib-uri>
<taglib-location>/WEB-INF/tlds/jstl/c.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>jstl-fmt</taglib-uri>
<taglib-location>/WEB-INF/tlds/jstl/fmt.tld</taglib-location>
</taglib>
<taglib>
<taglib-uri>jstl-fn</taglib-uri>
<taglib-location>/WEB-INF/tlds/jstl/fn.tld</taglib-location>
</taglib>
를 추가한다.
- jsp 에 추가
<%@ taglib uri="jstl-c" prefix="c" %>
<%@ taglib uri="jstl-fmt" prefix="fmt" %>
<%@ taglib uri="jstl-fn" prefix="fn" %>
사용 예(기본 문법)
Area | Subfunction | Prefix | Description |
---|---|---|---|
Core | Variable support | c | 변수지원 |
Core | Flow control | c | 흐름제어 |
Core | URL management | c | URL 처리 |
Core | Miscellaneous | c | |
XML | Core | x | XML 코어 |
XML | Flow control | x | 흐름 제어 |
XML | Transformation | x | XML 변환 |
I18n | Locale | fmt | 지역 |
I18n | Message formatting | fmt | 메시지 형식 |
I18n | Number and date formatting | fmt | 숫자 및 날짜 형식 |
Database | SQL | sql | SQL |
Functions | Collection length | fn | 콜렉션 처리 |
Functions | String manipulation | fn | String 처리 |
http://java.sun.com/products/jsp/jstl/1.1/docs/tlddocs/index.html 참고
- 변수지원태그
set: <c:set var="varName" scope="session" value="someValue">
var속성은 값을 지정할 변수의 이름
<c:set t a r g e t ="" property="userName" value="someValue">
target : 빈 프로퍼티나 맵 값을 설정한다.(Object)
var와 target을 동시에 사용할 수 없다.
scope속성은 변수가 위치하는 영역(page,request,session,application)
value :저장하는 값
remove :<c:remove var="varName" scope="session">
var :삭제할 변수의 이름
scope 속성은 삭제할 변수의 영역
out : <c:out value="">
value속성은 출력하려는 값
catch : <c:catch var="">
</c:catch>
예외사항이 한번이라도 발생시 </c:catch>로 점프
var에 정의 된 객체를 페이지 생존범위에 자동으로 묶어 나중에 var에 정의된 변수이름을 사용할 수 있다.
예외 발생시
: var 속성 사용시 exception 객체를 설정.
<c:catch> 문 밖으로 제어가 떨어진다
<c:set var="num1" value="${20}" />
<c:set var="num2">
10.5
</c:set>
<c:set var="today" value="<%= new java.util.Date()%>" /><br>
변수 num1 = ${num1} <br>
변수 num2 = ${num2} <br>
num1 + num2 = ${num1+num2}<br>
오늘은 ${today}입니다.
<c:remove var="num1" scope="page" />
<p>
삭제한 후의 num1=${num1} <br>
삭제한 후의 num1 + num2 = ${num1 + num2}
<c:catch var="myException">
It's catch
<% int x = 10/0; %>
실행안됨.
</c:catch>
<c:if test="${myException != null}">
발생된 예외는 : ${myException.message} <br>
</c:if>
- URL 관련
import : <c:import url=""/>
url속성에 명기한 파일을 현재 컨텐츠에 포함
param : <c:param name="" value=""/>
<jsp:param />과 같은 역할
url : <c:url value="" var=""/>
value에 들어있는 상대 경로 뒤에 jsessionid를 추가한다.(쿠키를 사용 못하는 경우)
var : 옵션 속성으로 url을 참조하기 위해쓴다.
redirect :<c:redirect url="' context=""/>
context : url경로의 이름
<c:import url="Header.jsp" >
<c:param name="subTitle" value="This is subTitle"/>
</c:import>
- 흐름제어 태그
if : <c:if test="조건"> </c:if>
test속성의 값에는 "조건"이 오는데 이 조건문의 결과값이 true 면 처리
<c:if test="true">
무조건 수행<br>
</c:if>
<c:if test="${param.name == 'bk'}">
name 파라미터의 값이 ${param.name}입니다 <br>
</c:if>
<c:if test="${param.name eq 'bk'}">
name 파라미터의 값이 ${param.name}입니다 <br>
</c:if>
<c:if test="${18 < param.age}">
당신의 나이는 18세 이상입니다.
</c:if>
choose,when,otherwise : <c:choose>
<c:when test="조건">
</c:when>
<c:otherwise>
</c:otherwise>
</c:choose>
choose 태그는 자바의 switch 문과 if-else 의 혼합한 형태, 다수의 조건문을 하나의 블록에서 수행하고자 할때 사용
-> switch문과의 차이점은 중간에 빠져 나가지 못한다는 것이다.
-> when 의 어느 하나에도 실행되지 않을때 otherwise 실행
-> otherswise 태그가 반드시 있어야 하는것은 아니다.
<c:choose>
<c:when test="${param.name == 'bk' }">
<li>당신의 이름은 ${param.name}입니다.
</c:when>
<c:when test="${param.age > 18 }">
<li>당신은 18세 이상입니다.
</c:when>
<c:otherwise>
<li> 당신은 'bk'가 아니고 18세 이상이 아닙니다.
</c:otherwise>
</c:choose>
forEach : <c:forEach var="변수" items="아이템" begin="시작값" end="끝값" step="증가값">
</c:forEach>
item 속성에 올수 있는 것들로는 Map,배열,Collection 이 있다.
varStatus는 javax.servlet.jsp.jstl.core.LoopTagStatus 객체 인스턴스변수를 만들며 count라는 프로퍼티가 있어 몇번의 회전인지 알 수있다.
<c:forEach var="i" begin="1" end="9">
<li>4 *${i} = ${4 *i}
</c:forEach>
<h4>int 형 배열</h4>
<c:forEach var="i" items="${intArray}" begin="2" end="4">
[${i}]
</c:forEach>
<h4>Map</h4>
<c:forEach var="i" items="${map}">
${i.key} = ${i.value}<br>
</c:forEach>
<c:forEach var="member" items="${memberList}" varStatus="memberLoopCount">
회원 $(memberLoopCount.count} : ${member} <br>
</c:forEach>
forTokens : <c:forTockens var="token" items="문자열" delins="구분자">
</c:forTockens>
forTokens 태그는 StringTokenizer 와 같은 기능을 제공한다.
<c:forTokens var="token" items="빨강색, 주황색, 노란색, 초록색, 파랑색, 남색, 보라색" delims=",">
${token}<br>
</c:forTokens>
- 숫자 및 날짜 지원 형식
The JSTL formatting actions allow various data elements in a JSP page, such as numbers,dates and times
to be formatted and parsed in a locale-sensitive or customized manner.
formatNumber : 숫자 형식을 표현
number : <fmt:formatNumber value="9876543.61" type="number"/>
currency: <fmt:formatNumber value="9876543.61" type="currency"/>
percent : <fmt:formatNumber type="percent">9876543.61</fmt:formatNumber>
pattern=".000" :<fmt:formatNumber value="9876543.61" pattern=".000" />
pattern="#,#00.0#":<fmt:formatNumber value="9876543.612345" pattern="#,#00.0#"/>
parseNumber : 정해진 패턴을 문자열에서 수치를 파싱해내는 태그
formatDate :날짜 형식을 표현
<jsp:useBean id="now" class="java.util.Date"/>
<c:out value="${now}"/>
date: <fmt:formatDate value="${now}" type="date"/>
time: <fmt:formatDate value="${now}" type="time"/>
both: <fmt:formatDate value="${now}" type="both"/>
default:<fmt:formatDate value="${now}"
type="both" dateStyle="default" timeStyle="default"/>
short :<fmt:formatDate value="${now}"
type="both" dateStyle="short" timeStyle="short" />
medium :<fmt:formatDate value="${now}"
type="both" dateStyle="medium" timeStyle="medium" />
long :<fmt:formatDate value="${now}"
type="both" dateStyle="long" timeStyle="long" />
full :<fmt:formatDate value="${now}"
type="both" dateStyle="full" timeStyle="full" />
pattern="yyyy년MM월dd일 HH시mm분ss초"
<fmt:formatDate value="${now}" type="both"
pattern="yyyy년MM월dd일 HH시mm분ss초"/>
<fmt:formatDate value="${now}" pattern="yyyy/MM/dd" />
parseDate :정해진 패턴의 문자열에서 날짜를 파싱해내는 태그
timeZone : <fmt:timeZone value=""/>
setTimeZone : <fmt:timeZone value="" var="" scope=""/>
- 국제화
message <fmt:message
setLocale <fmt:setLocale
bundle <fmt:bundle
setBundle <fmt:setBundle
param <fmt:param
requestEncoding <fmt:requestEncoding
- SQL
<sql:query sql="sqlQuery" var="varName" [scope="{page|request|session|application}"]
[dataSource="dataSource"] [maxRows="maxRows"] [startRow="startRow"]>
<sql:param>
</sql:query><sql:query var="customers" dataSource="${dataSource}">
SELECT * FROM customers
WHERE country ='China'
ORDER BY lastname
</sql:query>
<table>
<c:forEach var="row" items="">
<tr>
<td><c:out value="${row.lastName}"/></td>
<td><c:out value="${row.firstName}"/></td>
<td><c:out value="${row.address}"/></td>
</tr>
</c:forEach>
</table>
JSTL과 Velocity를 활용한 UI 레이어 구현 방법
Table of Contents
JSTL
소개
- J2EE 소형 클라이언트 기술인 JSP(JavaServer Pages)가 지난 몇 년 동안 널리 일반화되면서 독립적인 개발자들은 많은 사용자 정
의 JSP 태그 라이브러리를 만들었습니다. 이러한 태그 라이브러리는 대부분 서로 다른 목표를 달성하도록 작성되었지만 반복, 조건 등의 일
반적인 작업을 위한 유사한 솔루션을 제공하는 경향이 있습니다.
유사하고 일반적이 문제점을 해결하는 독립적인 태그 라이브러리에 대한 필요성을 줄이기 위해 Java Community Process(JSR 52)의 지
원하에 JSTL(JavaServer Pages Standard Tag Library)이 개발되었습니다. JSTL은 이러한 일반 기능을 처리하는 하나의 표준 솔루션
을 제공합니다. (말그대로 표준태그라이브러리) - JSTL의 주요 강점 중 하나는 서블릿 컨텍스트에 저장된 데이타 같은 애플리케이션 데이타를 액세스 및 조작하는 쉬운 방법을 제공하는 간
단한 EL을 사용할 수 있다는 것입니다.
설치 방법
- http://cvs.apache.org/builds/jakarta-taglibs/nightly/ 에서 다운
\jakarta-taglibs-20051024\jakarta-taglibs\standard\lib
에서 jstl 과 standard 파일 을 이 두개의 jar 파일을 우리의 웹애플리케이션의 /WEB-INF/lib 폴더에 넣습니다
그 다음 tld 폴더의 tld 파일을 /WEB-INF/lib/tld 폴더 아래 넣습니다.
- web.xml 에
<!-- jstl 1.2 taglig --> <taglib> <taglib-uri>jstl-c</taglib-uri> <taglib-location>/WEB-INF/tlds/jstl/c.tld</taglib-location> </taglib> <taglib> <taglib-uri>jstl-fmt</taglib-uri> <taglib-location>/WEB-INF/tlds/jstl/fmt.tld</taglib-location> </taglib> <taglib> <taglib-uri>jstl-fn</taglib-uri> <taglib-location>/WEB-INF/tlds/jstl/fn.tld</taglib-location> </taglib>
를 추가한다.
- jsp 에 추가
<%@ taglib uri="jstl-c" prefix="c" %> <%@ taglib uri="jstl-fmt" prefix="fmt" %> <%@ taglib uri="jstl-fn" prefix="fn" %>
사용 예(기본 문법)
Area | Subfunction | Prefix | Description |
---|---|---|---|
Core | Variable support | c | 변수지원 |
Core | Flow control | c | 흐름제어 |
Core | URL management | c | URL 처리 |
Core | Miscellaneous | c | |
XML | Core | x | XML 코어 |
XML | Flow control | x | 흐름 제어 |
XML | Transformation | x | XML 변환 |
I18n | Locale | fmt | 지역 |
I18n | Message formatting | fmt | 메시지 형식 |
I18n | Number and date formatting | fmt | 숫자 및 날짜 형식 |
Database | SQL | sql | SQL |
Functions | Collection length | fn | 콜렉션 처리 |
Functions | String manipulation | fn | String 처리 |
http://java.sun.com/products/jsp/jstl/1.1/docs/tlddocs/index.html 참고
- 변수지원태그
set: <c:set var="varName" scope="session" value="someValue">
var속성은 값을 지정할 변수의 이름
<c:set t a r g e t ="" property="userName" value="someValue">
target : 빈 프로퍼티나 맵 값을 설정한다.(Object)
var와 target을 동시에 사용할 수 없다.
scope속성은 변수가 위치하는 영역(page,request,session,application)
value :저장하는 값
remove :<c:remove var="varName" scope="session">
var :삭제할 변수의 이름
scope 속성은 삭제할 변수의 영역
out : <c:out value="">
value속성은 출력하려는 값
catch : <c:catch var="">
</c:catch>
예외사항이 한번이라도 발생시 </c:catch>로 점프
var에 정의 된 객체를 페이지 생존범위에 자동으로 묶어 나중에 var에 정의된 변수이름을 사용할 수 있다.
예외 발생시
: var 속성 사용시 exception 객체를 설정.
<c:catch> 문 밖으로 제어가 떨어진다
<c:set var="num1" value="${20}" /> <c:set var="num2"> 10.5 </c:set> <c:set var="today" value="<%= new java.util.Date()%>" /><br> 변수 num1 = ${num1} <br> 변수 num2 = ${num2} <br> num1 + num2 = ${num1+num2}<br> 오늘은 ${today}입니다. <c:remove var="num1" scope="page" /> <p> 삭제한 후의 num1=${num1} <br> 삭제한 후의 num1 + num2 = ${num1 + num2}
<c:catch var="myException"> It's catch <% int x = 10/0; %> 실행안됨. </c:catch> <c:if test="${myException != null}"> 발생된 예외는 : ${myException.message} <br> </c:if>
- URL 관련
import : <c:import url=""/>
url속성에 명기한 파일을 현재 컨텐츠에 포함
param : <c:param name="" value=""/>
<jsp:param />과 같은 역할
url : <c:url value="" var=""/>
value에 들어있는 상대 경로 뒤에 jsessionid를 추가한다.(쿠키를 사용 못하는 경우)
var : 옵션 속성으로 url을 참조하기 위해쓴다.
redirect :<c:redirect url="' context=""/>
context : url경로의 이름
<c:import url="Header.jsp" > <c:param name="subTitle" value="This is subTitle"/> </c:import>
- 흐름제어 태그
if : <c:if test="조건"> </c:if>
test속성의 값에는 "조건"이 오는데 이 조건문의 결과값이 true 면 처리<c:if test="true"> 무조건 수행<br> </c:if> <c:if test="${param.name == 'bk'}"> name 파라미터의 값이 ${param.name}입니다 <br> </c:if> <c:if test="${param.name eq 'bk'}"> name 파라미터의 값이 ${param.name}입니다 <br> </c:if> <c:if test="${18 < param.age}"> 당신의 나이는 18세 이상입니다. </c:if>
choose,when,otherwise : <c:choose>
<c:when test="조건">
</c:when>
<c:otherwise>
</c:otherwise>
</c:choose>
choose 태그는 자바의 switch 문과 if-else 의 혼합한 형태, 다수의 조건문을 하나의 블록에서 수행하고자 할때 사용
-> switch문과의 차이점은 중간에 빠져 나가지 못한다는 것이다.
-> when 의 어느 하나에도 실행되지 않을때 otherwise 실행
-> otherswise 태그가 반드시 있어야 하는것은 아니다.
<c:choose> <c:when test="${param.name == 'bk' }"> <li>당신의 이름은 ${param.name}입니다. </c:when> <c:when test="${param.age > 18 }"> <li>당신은 18세 이상입니다. </c:when> <c:otherwise> <li> 당신은 'bk'가 아니고 18세 이상이 아닙니다. </c:otherwise> </c:choose>
forEach : <c:forEach var="변수" items="아이템" begin="시작값" end="끝값" step="증가값">
</c:forEach>
item 속성에 올수 있는 것들로는 Map,배열,Collection 이 있다.
varStatus는 javax.servlet.jsp.jstl.core.LoopTagStatus 객체 인스턴스변수를 만들며 count라는 프로퍼티가 있어 몇번의 회전인지 알 수있다.
<c:forEach var="i" begin="1" end="9"> <li>4 *${i} = ${4 *i} </c:forEach> <h4>int 형 배열</h4> <c:forEach var="i" items="${intArray}" begin="2" end="4"> [${i}] </c:forEach> <h4>Map</h4> <c:forEach var="i" items="${map}"> ${i.key} = ${i.value}<br> </c:forEach> <c:forEach var="member" items="${memberList}" varStatus="memberLoopCount"> 회원 $(memberLoopCount.count} : ${member} <br> </c:forEach>
forTokens : <c:forTockens var="token" items="문자열" delins="구분자">
</c:forTockens>
forTokens 태그는 StringTokenizer 와 같은 기능을 제공한다.
<c:forTokens var="token" items="빨강색, 주황색, 노란색, 초록색, 파랑색, 남색, 보라색" delims=","> ${token}<br> </c:forTokens>
- 숫자 및 날짜 지원 형식
The JSTL formatting actions allow various data elements in a JSP page, such as numbers,dates and times
to be formatted and parsed in a locale-sensitive or customized manner.
formatNumber : 숫자 형식을 표현
number : <fmt:formatNumber value="9876543.61" type="number"/> currency: <fmt:formatNumber value="9876543.61" type="currency"/> percent : <fmt:formatNumber type="percent">9876543.61</fmt:formatNumber> pattern=".000" :<fmt:formatNumber value="9876543.61" pattern=".000" /> pattern="#,#00.0#":<fmt:formatNumber value="9876543.612345" pattern="#,#00.0#"/>
parseNumber : 정해진 패턴을 문자열에서 수치를 파싱해내는 태그
formatDate :날짜 형식을 표현
<jsp:useBean id="now" class="java.util.Date"/> <c:out value="${now}"/> date: <fmt:formatDate value="${now}" type="date"/> time: <fmt:formatDate value="${now}" type="time"/> both: <fmt:formatDate value="${now}" type="both"/> default:<fmt:formatDate value="${now}" type="both" dateStyle="default" timeStyle="default"/> short :<fmt:formatDate value="${now}" type="both" dateStyle="short" timeStyle="short" /> medium :<fmt:formatDate value="${now}" type="both" dateStyle="medium" timeStyle="medium" /> long :<fmt:formatDate value="${now}" type="both" dateStyle="long" timeStyle="long" /> full :<fmt:formatDate value="${now}" type="both" dateStyle="full" timeStyle="full" /> pattern="yyyy년MM월dd일 HH시mm분ss초" <fmt:formatDate value="${now}" type="both" pattern="yyyy년MM월dd일 HH시mm분ss초"/> <fmt:formatDate value="${now}" pattern="yyyy/MM/dd" />
parseDate :정해진 패턴의 문자열에서 날짜를 파싱해내는 태그
timeZone : <fmt:timeZone value=""/>
setTimeZone : <fmt:timeZone value="" var="" scope=""/>
- 국제화
message <fmt:message
setLocale <fmt:setLocale
bundle <fmt:bundle
setBundle <fmt:setBundle
param <fmt:param
requestEncoding <fmt:requestEncoding
- SQL
<sql:query sql="sqlQuery" var="varName" [scope="{page|request|session|application}"]
[dataSource="dataSource"] [maxRows="maxRows"] [startRow="startRow"]>
<sql:param>
</sql:query><sql:query var="customers" dataSource="${dataSource}"> SELECT * FROM customers WHERE country ='China' ORDER BY lastname </sql:query> <table> <c:forEach var="row" items=""> <tr> <td><c:out value="${row.lastName}"/></td> <td><c:out value="${row.firstName}"/></td> <td><c:out value="${row.address}"/></td> </tr> </c:forEach> </table>
<sql:update>
<sql:setDataSource>
<sql:param>
<sql:dateParam>
- XML 코어
<x:parse>
<x:out>
<x:set>
- 흐름제어
<x:if>
<x:choose>
<x:when>
<x:otherwise>
<x:forEach>
- XML 변환
<x:transform>
<x:param>
- function
contains
containsIgnoreCase
endsWith
escapeXml
indexOf
join
length
replace
split
startsWith
substring
substringAfter
substringBefore
toLowerCase
toUpperCase
trim<c:if test="${fn:contains(name, searchString)}"> <c:if test="${fn:containsIgnoreCase(name, searchString)}"> <c:if test="${fn:endsWith(filename, ".txt")}"> ${fn:escapeXml(param:info)} ${fn:indexOf(name, "-")} ${fn:join(array, ";")} You have ${fn:length(shoppingCart.products)} in your shopping cart. ${fn:replace(text, "-", "•")} ${fn:split(customerNames, ";")} <c:if test="${fn:startsWith(product.id, "100-")}"> P.O. Box: ${fn:substring(zip, 6, -1)} P.O. Box: ${fn:substringAfter(zip, "-")} Zip (without P.O. Box): ${fn:substringBefore(zip, "-")} Product name: ${fn.toLowerCase(product.name)} Product name: ${fn.UpperCase(product.name)} Name: ${fn.trim(name)}
Velocity
Velocity
소개
- 벨로시티란 자바 기반의 템플릿 엔진입니다.
벨로시티를 활용하면 간단하면서도 강력한 템플릿 언어를 통하여 자바 코드에 정의된 객체를 액세스할 수 있습니다.
벨로시티를 웹 개발에 사용하면, 웹 디자이너는 자바 프로그래머와 병렬로 작업을 할 수 있으며 MVC(모델-뷰-컨트롤러) 모델에 따라 웹 사이트를 개발할 수 있습니다. 더 자세히 설명하면 웹 페이지 디자이너의 경우 보기 좋은 사이트를 만드는 데만 집중하면 되고, 프로그래머는 잘 동작하는 코드를 만드는 데만 집중하면 된다는 뜻입니다.
벨로시티는 웹 페이지와 자바 코드를 분리하여, 장기적인 측면에서 볼 때 웹 사이트를 손쉽게 유지보수할 수 있도록 하고, 자바 서버 페이지 (JSP) 또는 PHP를 대체할 수 있는 방안을 제시합니다. 벨로시티의 쓰임새는 웹 사이트에 국한되지 않습니다. 예를 들면, 템플릿으로부터 SQL이나 포스트스크립트, 또는 XML(XML 변환에 대해서는 벨로시티 툴 중 하나인 아나키아(Anakia)를 참조)문서를 생성하는 데 쓰일 수 있습니다. 벨로시티는 스탠드얼론 유틸리티처럼 사용하여 소스 코드나 리포트를 생성할 수도 있고, 다른 시스템의 컴포넌트로 통합할 수도 있습니다. 또한 벨로시티는 터빈 (또다른 자카르타 서브 프로젝트 중 하나) 웹 애플리케이션 프레임웍에 템플릿 서비스를 제공합니다. 벨로시티와 터빈을 조합하면 진정한 MVC 모델에 따라 웹 애플리케이션을 개발할 수 있습니다
설치 방법
- http://jakarta.apache.org/site/downloads/downloads_velocity-engine.cgi
에서 1.4zip 를 다운
velocity-dep-1.4.jar
velocity-tools-1.1.jar
파일을 web_inf/lib 아래 놓습니다.
- web.xml 수정
<servlet> <servlet-name>velocity</servlet-name> <servlet-class>org.apache.velocity.tools.view.servlet.VelocityViewServlet </servlet-class> <init-param> <param-name>org.apache.velocity.toolbox</param-name> <param-value>/WEB-INF/velocity-toolbox.xml</param-value> </init-param> <init-param> <param-name>org.apache.velocity.properties</param-name> <param-value>/WEB-INF/velocity.properties</param-value> </init-param> <load-on-startup>10</load-on-startup> </servlet> <servlet-mapping> <servlet-name>velocity</servlet-name> <url-pattern>*.vm</url-pattern> </servlet-mapping>
- 파일 생성
velocity.properties 파일
velocity-toolbox.xml 을 생성 한 후 web_inf/lib 아래 둡니다.
velocity-toolbox.xml<tool> <key>date</key> <scope>application</scope> <class>org.apache.velocity.tools.generic.DateTool</class> </tool> <tool> <key>math</key> <scope>application</scope> <class>org.apache.velocity.tools.generic.MathTool</class> </tool> ...
사용 예(기본 문법)
Velocity Template Language(VTL) 은 Template 에서 사용되는 Velocity 고유의 언어를 의미합니다.
- References(참조형)
Variables(변수) - 다음과 같이 $를 먼저 쓰고 그 뒤에 식별자를 적어주는 방식으로 사용
ex) $foo
Property(특성) - $ 다음에 식별자를 쓰고, 마침표(.)후에 다시 식별자의 형태로 사용
ex) $foo.name
Method(메소드) - $다음에 식별자를 쓰고 마침표 후에 호출할 메소드의 이름을 적는다
ex)$foo.getName()
- Directive(지시형)
#set - reference 의 값을 설정한다.
#if/elseif/else - 조건문 제어
#foreach ---- 제어
#include - velocity 로 파싱되지 않는 파일의 출력
#parse -velocity 로 파싱된 파일 출력
#stop -template 엔진의 정지
#macro - 반복적으로 사용할 vm정의
- Comment (주석)
## - 한줄짜리 주석 #* ... *# 여러줄 짜리 주석
##This is an example velocity template #set($this = "Velocity") $this is great! But It's so hard. #foreach($name in $list) $name is great! #end #set($condition = true) #if ($condition) The condition is true! #else The condition is false #end
http://jakarta.apache.org/velocity/docs/vtl-reference-guide.html
Tool box 에 대해
VelocityTools is a collection of Velocity subprojects with a common goal of creating tools and infrastructure for building both web and non-web applications using the Velocity template engine.
- Generic Tool (http://jakarta.apache.org/velocity/tools/generic/)
*DateTool : A tool for manipulating and formatting dates
*MathTool :A tool for performing floating point math.
*NumberTool :A tool for formatting numbers
*IteratorTool :A convenience tool to use with #foreach loops. It wraps a list to let the designer specify a
condition to terminate the loop, and reuse the same list in different loops.
*RenderTool:A tool to evaluate and render arbitrary strings of VTL (Velocity Template Language).Example uses: $date -> Oct 19, 2003 9:54:50 PM $date.long -> October 19, 2003 9:54:50 PM PDT $date.medium_time -> 9:54:50 PM $date.full_date -> Sunday, October 19, 2003 $date.get('default','short') -> Oct 19, 2003 9:54 PM $date.get('yyyy-M-d H:m:s') -> 2003-10-19 21:54:50 $myDate -> Tue Oct 07 03:14:50 PDT 2003 $date.format('medium',$myDate) -> Oct 7, 2003 3:14:50 AM
- VelocityView (http://jakarta.apache.org/velocity/tools/view/)
- VelocityStruts(http://jakarta.apache.org/velocity/tools/struts/)
JSTL 과 Velocity
같은 로직에 대해 두가지를 사용하여 각각 구현
다양한 View Technology에 대한 실행 속도 비교.
현재 개발중 우리들이 많이 사용하는 View 기술들을 이용하여 실행속도를 비교해 보면 다음 그래프와 같다.
이 그래프는 100명의 동시 접속자에 대한 테스트를 진행한 결과이다. 이 그래프는 Expert One-on-One J2EE Design and Development 책의 Chapter 15의 Web-Tier Performance Issues에서 인용하였다. |
위와 같이 테스트를 진행한 결과 JSP는 초당 54페이지, Velocity는 초당 112페이지, XMLC는 초당 128 페이지, XSLT는 6,7페이지를 서비스하였다. 이 부분에서 눈여겨 볼 부분은 Velocity에 대한 결과라할 수 있다. 국내에서는 아직까지 많이 사용되지 않는 기술이지만 위의 실행 결과를 보면 사용할만한 가치가 있다는 것을 확인할 수 있다.
참고문헌
문서에 대하여
최초작성자 : 박재성
최초작성일 : 2005년 10월 15일
버전 : 1.0
문서이력 :
- 2005년 10월 15일 박재성 문서 최초 생성 : 강좌 문서 템플릿 및 JSP와 Velocity에 대한 속도 비교문서 추가
4. SQL Tag
sql 태그를 사용하기 위해서 페이지 상단에 다음과 같이 선언되어야 된다.
<%@ taglib uri="http://java.sun.com/jstl/sql" prefix="sql"%>
DataSource 를 이용해서 SQL을 처리하는 sql 태그는 다음과 같은 것들이 있다.
기능 | 태그 | prefix |
---|---|---|
DataSource 설정 | SetDataSource | sql |
SQL | query (dateParam, param) , update (dateParam, param) , transaction | sql |
4.1 <sql:setDataSource/>
javax.sql.DataSource의 인스턴스를 얻을 수 있다.
<sql:setDataSource {dataSource="dataSource" |url="jdbcUrl" [driver="driverClassName"] [user="userName"] [password="password"]} [var="varName"] [scope="{page|request|session|application}"]/> |
4.2 <sql:query/>
쿼리 실행.
JDBC의 java.sql.PreparedStatement 인터페이스에 기반한 방식을 사용하여 매개변수화 된다.
매개변수 값은 중첩된 <sql:param>과 <sql:dateParam> 태그를 사용하여 지정된다.
<%@ page pageEncoding="MS949" %> <%@ taglib prefix="sql" uri="http://java.sun.com/jstl/sql" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jstl/fmt" %> <%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> <fmt:setLocale value="ko" /> <sql:query var="emp" dataSource="jdbc/myora81"> SELECT EMPNO AS 사원번호, ENAME AS 이름, SAL AS 월급여, HIREDATE AS 입사일 FROM EMP </sql:query> <table border="1"> <tr> <%-- 필드의 정보를 출력한다. --%> <c:forEach var="columnName" items="${emp.columnNames}"> <th><c:out value="${columnName}"/></th> </c:forEach> <%-- 데이터를 한 줄씩 출력한다. --%> <c:forEach var="row" items="${emp.rowsByIndex}"> <tr> <%-- 필드의 길이만큼 반복한다. --%> <c:forEach var="column" items="${row}" varStatus="i"> <c:choose> <c:when test="${i.index==3}"> <td><fmt:formatDate value="${column}" pattern="yyyy/MM/dd"/></td> </c:when> <c:otherwise> <td><c:out value="${column}"/></td> </c:otherwise> </c:choose> </c:forEach> </c:forEach> </table> <hr> <table border="1"> <c:forEach var="row" items="${emp.rows}"> <tr> <td>번호: <c:out value="${row['사원번호']}"/></td> <td>이름: <c:out value="${row['이름']}"/></td> </tr> </c:forEach> </table>
4.3 <sql:dateParam/> , <sql:param/>
<sql:dateParam/>은 java.sql.PreparedStatement.setTimestamp() 역할을 하고,
<sql:param/> 은 java.sql.PreparedStatement.setString() 의 역할을 한다.
<sql:dateParam value="value" type="[timestamp|time|date]"/> |
<sql:param value="value"/> |
4.4 <sql:update/>
java.sql.Statement.executeUpdate() 메소드에 해당.
형식과 동작은 <sql:query/>태그와 동일.
<sql:update sql="sqlUpdate" [dataSource="dataSource"] [var="varName"] [scope="{page|request|session|application}"]/> |
<html> <body> <sql:setDataSource var="dataSrc" url="jdbc:oracle:thin:@localhost:1521:ORCL" driver="oracle.jdbc.OracleDriver" user="scott" password="tiger"/> <sql:transaction dataSource="${dataSrc}"> <sql:update sql="insert into dept(deptno, dname, loc) values(?,?,?)"> <sql:param value="50"/> <sql:param value="DEVS"/> <sql:param value="SEOUL"/> </sql:update> <sql:update sql="update dept set LOC=? where deptno=?"> <sql:param value="BUSAN"/> <sql:param value="50"/> </sql:update> </sql:transaction> </body> </html>
4.5 <sql:transaction/>
트랜잭션 구현.
(트랜잭션은 하나의 작업 그룹으로서 성공 또는 실패 시에 데이타베이스 연산 결과를 보호하는데 사용)
<sql:transaction [dataSource="dataSource"] [isolation=isolationLevel]> <sql:query> 과 <sql:update> 문들 </sql:transaction> |
isolationLevel ::= "read_committed"| "read_uncommitted"| "repeatable_read"| "serializable"
문서에 대하여
- 문서참고 : 이 문서는 okjsp 운영자 허광남씨가 작성한 JSP Standard Tag Library (JSTL)를 참고하여 작성하였습니다.
- 최초작성자 : 고유정
- 최초작성일 : 2007년 10월 6일
- 이 문서는 오라클클럽 자바 웹개발자 스터디 모임에서 작성하였습니다.
50만건이 넘는 데이타를 읽어서 insert 할 일이 생겼는데 보통의 방법으로 하니 톰켓이 계속 죽어 버리는 일이 생겼습니다.
자바 API를 보니 이러한 방법이 있더군요...
try {
StringBuffer query = new StringBuffer();
query.append("INSERT INTO ABT231 ");
query.append("(customer_no, item_cd, occur_amt, reason_cd, register_ymd, register_no) ");
query.append(" VALUES (?, ?, ?, '9', ?, ?) ");
conn = getConnection();
conn.setAutoCommit(false);
pstmt = conn.prepareStatement(query.toString());
Iterator iter = m_abt231InsertList.iterator();
int count = 0;
while( iter.hasNext() ) {
m_abt231 = (Abt231) iter.next();
pstmt.setInt(1, m_abt231.getCustomerNo());
pstmt.setString(2, m_abt231.getItemCd());
pstmt.setLong(3, m_abt231.getOccurAmt());
pstmt.setString(4, s_magamCurrentTime);
pstmt.setInt(5, Integer.parseInt(s_workCd));
pstmt.addBatch();
count++;
if( (count % 10000) == 0){
System.out.println(count + "건 처리중");
pstmt.executeBatch();
}
}
pstmt.executeBatch();
conn.commit();
}
<%@ page contentType="text/html;charset=EUC-KR"%>
<%
String protocol = request.getProtocol();
String serverName = request.getServerName();
int serverPort = request.getServerPort();
String remoteAddr = request.getRemoteAddr();
String remoteHost = request.getRemoteHost();
String method = request.getMethod();
StringBuffer requestURL = request.getRequestURL();
String requestURI = request.getRequestURI();
String useBrowser = request.getHeader("User-Agent");
String fileType = request.getHeader("Accept");
String tt = request.getHeader("referer");
%>
<html>
<body>
<h1>Request Example2</h1>
프로토콜 : <%=protocol%><p>
서버의 이름 : <%=serverName%><p>
서버의 포트 번호 :<%=serverPort%><p>
사용자 컴퓨터의 주소 : <%=remoteAddr%><p>
사용자 컴퓨터의 이름 : <%=remoteHost%><p>
사용 method : <%=method%><p>
요청 경로(URL) : <%=requestURL%><p>
요청 경로(URI) : <%=requestURI%><p>
현재 사용하는 브라우저 : <%=useBrowser%><p>
브라우저가 지원하는 file의 type : <%=fileType%><p>
이전 URL : <%=tt%><p>
</body>
</html>
CallableStatement
: SQL의 스토어드프로시저(Stored Procedure)를 실행시키기 위해 사용되는 인터페이스 이다.
스토어드프로시저란
: query문을 하나의 파일 형태로 만들거나 데이터베이스에 저장해 놓고 함수처럼 호출해서 사용하는 것임.
이것을 이용하면 연속되는 query문에 대해서 매우 빠른 성능을 보인다.
보안적인 장점 역시 가지고 있음.
스토어드프로시저로 값을 받아오려면,
호출하기에 앞서 반드시 CallableStatement인터페이스의 registerOutParameter()메서드를 호출해야 함.
이 인터페이스는 PreparedStatement 인터페이스로부터 상속 받았기 때문에 setXXX()메서드를 사용할 수 있다.
(CallableStatement 예제)
CallableStatementTest.java
import java.sql.*;
public class CallableStatementTest{
public static void main(String[] args){
try{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
Connection con = DriverManager.getConnection("jdbc:odbc:dbdsn", "id", "password");
CallableStatement cs = con.prepareCall("{call myStoredProcedure(?,?,?)}");
cs.setInt(1,2);
cs.registerOutParameter(2, java.sql.Types.VARCHAR);
cs.registerOutParameter(3, java.sql.Types.INTEGER);
cs.execute();
System.out.println("*name : "+ cs.getString(2) +"*age : "+ cs.getInt(3));
cs.close();
con.close();
}catch(Exception e){System.out.println(e);}
}
}
C:\JavaExample\19>javac CallableStatementTest.java
C:\JavaExample\19>java CallableStatementTest
*name : Jabook *age : 2
MS-SQL에서의 스토어드프로시저 myStoredProcedure 작성구문
CREATE PROCEDURE myStoredProcedure
@age int
, @na varchar(20) OUTPUT
, @ageo int OUTPUT
AS
SELECT @na = name, @ageo = age FROM mytest
Where age = @age
주의 1. Java의 코드에는 SQL의 query문이 들어가 있지 않았음.
그리고 위에 정리해 놓은 것처럼, SQL서버 자체에 스토어드프로시저를 작성하여 query를 작성해 놓음.
위에서 골뱅이(@)가 붙은 것들이 매개변수이고 그 중에서도 뒤에 OUTPUT이라고 붙은 것들이 리턴 될 값.
CallableStatement객체 cs를 생성하여 프로시저를 호출하기 위한 prepareCall()메서드를 사용한다.
여기서 물음표(?)가 프로시저로 전달되고 받아올 매개변수인 것입니다.
CallableStatement cs = con.prepareCall("{call myStoredProcedure(?, ?, ?)}");
setXXX()메서드를 이용하여 프로시저에 사용할 인자값을 넣어주게 됨.
그리고 리턴되는 값들을 받아야 겠죠. 일반 메서드와 달리 여러 개의 인자값을 받을 수 있음.
이때 스토어드프로시저에서 넘어오는 값을 얻기 위해서 registerOutParameter()메서드를 이용하여
반환되는 값들을 셋팅하게 됩니다.
cs.setInt(1,2);
cs.registerOutParameter(2, java.sql.Types.VARCHAR);
cs.registerOutParameter(3, java.sql.Types.INTEGER);
반환되는 값을 얻기 위해서는 CallableStatement를 실행한 후 다음과 같이 반환값을 얻어 낼 수 있습니다.
cs.execute();
System.out.println("*name : "+ cs.getString(2) +"*age : "+ cs.getInt(3));
CallableStatement인터페이스는 데이터베이스의 스토어드프로시저를 호출하기 위해
prepareCall()메서드를 이용하여 CallableStatement객체를 생성한다.
그 prepareCall()메서드는 Connection인터페이스의 메서드.
스토어드프로시저를 실행하기 전에 받아올 값에 대비하기 위해서
registerOutParameter()메서드를 사용하는 주의 할점.
☞ CallableStatement
데이터베이스의 스토어드프로시저를 실행시키기 위해 사용되는 메서드.
스토어드프로시저를 사용하면 속도, 코드의 독립성, 보안성등의 다양한 이점을 얻을 수 있다.
CallableStatement인터페이스 주요 메서드
public void registerOutParameter(int parameterIndex, int sqlType) throws SQLException
: 프로시저로 받아온 값을 JDBC타입으로 등록.
모든 받아온 값은 반드시 이 과정을 거쳐야 합니다.
대표적인 sqlType을 알아보면 NULL, FOLAT, INTEGER, DATE등이 있습니다.
*PreparedStatement클래스를 상속하므로 getXXX()등, PreparedStatement가 가지고 있는 메서드를 사할수 있음.
Pooling 이란 용어는 일반적인 용어입니다. Socket Connection Pooling, Thread
Pooling, Resource Pooling 등 "어떤 자원을 미리 Pool 에 준비해두고 요청시 Pool에
있는 자원을 곧바로 꺼내어 제공하는 기능"인 거죠.
JDBC Connection Pooling 은 JDBC를 이용하여 자바에서 DB연결을 할 때, 미리 Pool에
물리적인 DB 연결을 일정개수 유지하여 두었다가 어플리케이션에서 요구할 때 곧바로
제공해주는 기능을 일컫는 용어입니다. JDBC 연결시에, (DB 종류마다, 그리고 JDBC
Driver의 타입에 따라 약간씩 다르긴 하지만 ) 대략 200-400 ms 가 소요 됩니다. 기껏
0.2 초 0.4 초 밖에 안되는 데 무슨 문제냐 라고 반문할수도 있습니다.
하지만, 위 시간은 하나의 연결을 시도할 때 그러하고, 100 - 200개를 동시에 연결을
시도하면 얘기가 완전히 달라집니다.
아래는 직접 JDBC 드라이버를 이용하여 연결할 때와 JDBC Connection Pooling 을 사용
할 때의 성능 비교 결과입니다.
------------------------------------------------------------------
테스트 환경
LG-IBM 570E Notebook(CPU:???MHz , MEM:320MB)
Windows NT 4.0 Service Pack 6
IBM WebSphere 3.0.2.1 + e-Fixes
IBM HTTP Server 1.3.6.2
IBM UDB DB2 6.1
아래에 첨부한 파일는 자료를 만들때 사용한 JSP소스입니다. (첨부파일참조)
[HttpConn.java] 간단한 Stress Test 프로그램
아래의 수치는 이 문서 이외에는 다른 용도로 사용하시면 안됩니다. 테스트를 저의
개인 노트북에서 측정한 것이고, 또한 테스트 프로그램 역시 직접 만들어한 것인
만큼, 공정성이나 수치에 대한 신뢰를 부여할 수는 없습니다.
그러나 JDBC Connection Pooling 적용 여부에 따른 상대적인 차이를 설명하기에는
충분할 것 같습니다.
테스트 결과
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
매번 직접 JDBC Driver 연결하는 경우
java HttpConn http://localhost/db_jdbc.jsp -c <동시유저수> -n <호출횟수> -s 0
TOTAL( 1,10) iteration=10 , average=249.40 (ms), TPS=3.93
TOTAL(50,10) iteration=500 , average=9,149.84 (ms), TPS=4.83
TOTAL(100,10) iteration=1000 , average=17,550.76 (ms), TPS=5.27
TOTAL(200,10) iteration=2000 , average=38,479.03 (ms), TPS=4.89
TOTAL(300,10) iteration=3000 , average=56,601.89 (ms), TPS=5.01
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
DB Connection Pooing 을 사용하는 경우
java HttpConn http://localhost/db_pool_cache.jsp -c <동시유저수> -n <호출횟수> -s 0
TOTAL(1,10) iteration=10 , average=39.00 (ms), TPS=23.26
TOTAL(1,10) iteration=10 , average=37.10 (ms), TPS=24.33
TOTAL(50,10) iteration=500 , average=767.36 (ms), TPS=45.27
TOTAL(50,10) iteration=500 , average=568.76 (ms), TPS=61.26
TOTAL(50,10) iteration=500 , average=586.51 (ms), TPS=59.79
TOTAL(50,10) iteration=500 , average=463.78 (ms), TPS=67.02
TOTAL(100,10) iteration=1000 , average=1,250.07 (ms), TPS=57.32
TOTAL(100,10) iteration=1000 , average=1,022.75 (ms), TPS=61.22
TOTAL(200,10) iteration=1462 , average=1,875.68 (ms), TPS=61.99
TOTAL(300,10) iteration=1824 , average=2,345.42 (ms), TPS=61.51
NOTE: average:평균수행시간, TPS:초당 처리건수
------------------------------------------------------------------
즉, JDBC Driver 를 이용하여 직접 DB연결을 하는 구조는 기껏 1초에 5개의 요청을
처리할 수 있는 능력이 있는 반면, DB Connection Pooling 을 사용할 경우는 초당
60여개의 요청을 처리할 수 있는 것으로 나타났습니다.
12배의 성능향상을 가져온거죠. (이 수치는 H/W기종과 측정방법, 그리고 어플리케이션에
따라 다르게 나오니 이 수치자체에 너무 큰 의미를 두진 마세요)
주의: 흔히 "성능(Performance)"을 나타낼 때, 응답시간을 가지고 얘기하는 경향이
있습니다. 그러나 응답시간이라는 것은 ActiveUser수가 증가하면 당연히 그에 따라
느려지게 됩니다. 반면 "단위시간당 처리건수"인 TPS(Transaction Per Second) 혹은
RPS(Request Per Second)는 ActiveUser를 지속적으로 끌어올려 임계점을 넘어서면,
특정수치 이상을 올라가지 않습니다. 따라서 성능을 얘기할 땐 평균응답시간이 아니라
"단위시간당 최대처리건수"를 이야기 하셔야 합니다.
성능(Performance)의 정의(Definition)은 "단위시간당 최대처리건수"임을 주지하세요.
성능에 관련한 이론은 아래의 문서를 통해, 함께 연구하시지요.
[강좌]웹기반시스템하에서의 성능에 대한 이론적 고찰
http://www.javaservice.net/~java/bbs/read.cgi?m=resource&b=consult&c=r_p&n=1008701211
PS: IBM WebSphere V3 의 경우, Connection 을 가져올 때, JNDI를 사용하게 되는데
이때 사용되는 DataSource 객체를 매번 initialContext.lookup() 을 통해 가져오게
되면, 급격한 성능저하가 일어납니다.(NOTE: V4부터는 내부적으로 cache를 사용하여
성능이 보다 향상되었습니다)
DataSource를 매 요청시마다 lookup 할 경우 다음과 같은 결과를 가져왔습니다.
java HttpConn http://localhost/db_pool.jsp -c <동시유저수> -n <호출횟수> -s 0
TOTAL(1,10) iteration=10 , average=80.00 (ms), TPS=11.61
TOTAL(50,10) iteration=500 , average=2,468.30 (ms), TPS=16.98
TOTAL(50,10) iteration=500 , average=2,010.43 (ms), TPS=18.18
TOTAL(100,10) iteration=1000 , average=4,377.24 (ms), TPS=18.16
TOTAL(200,10) iteration=1937 , average=8,991.89 (ms), TPS=18.12
TPS 가 18 이니까 DataSource Cache 를 사용할 때 보다 1/3 성능밖에 나오지 않는 거죠.
6. JDBC Connection Pooling 을 사용하여 코딩할 때 고려사항
JDBC Connecting Pooling은 JDBC 1.0 스펙상에 언급되어 있지 않았습니다. 그러다보니
BEA WebLogic, IBM WebSphere, Oracle OAS, Inprise Server, Sun iPlanet 등 어플리케
이션 서버라고 불리는 제품들마다 그 구현방식이 달랐습니다.
인터넷에서 돌아다니는 Hans Bergsten 이 만든 DBConnectionManager.java 도 그렇고,
JDF 에 포함되어 있는 패키지도 그렇고 각자 독특한 방식으로 개발이 되어 있습니다.
JDBC를 이용하여 DB연결하는 대표적인 코딩 예를 들면 다음과 같습니다.
------------------------------------------------------------------
[BEA WebLogic Application Server]
import java.sql.*;
// Driver loading needed.
static {
try {
Class.forName("weblogic.jdbc.pool.Driver").newInstance();
}
catch (Exception e) {
...
}
}
......
Connection conn = null;
Statement stmt = null;
try {
conn = DriverManager.getConnection("jdbc:weblogic:pool:<pool_name>", null);
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ....");
while(rs.next()){
.....
}
rs.close();
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) try{conn.close();}catch(Exception e){}
}
------------------------------------------------------------------
IBM WebSphere Application Server 3.0.2.x / 3.5.x
/* IBM WebSphere 3.0.2.x for JDK 1.1.8 */
//import java.sql.*;
//import javax.naming.*;
//import com.ibm.ejs.dbm.jdbcext.*;
//import com.ibm.db2.jdbc.app.stdext.javax.sql.*;
/* IBM WebSphere 3.5.x for JDK 1.2.2 */
import java.sql.*;
import javax.sql.*;
import javax.naming.*;
// DataSource Cache 사용을 위한 ds 객체 static 초기화
private static DataSource ds = null;
static {
try {
java.util.Hashtable props = new java.util.Hashtable();
props.put(Context.INITIAL_CONTEXT_FACTORY,
"com.ibm.ejs.ns.jndi.CNInitialContextFactory");
Context ctx = null;
try {
ctx = new InitialContext(props);
ds = (DataSource)ctx.lookup("jdbc/<data_source_name>");
}
finally {
if ( ctx != null ) ctx.close();
}
}
catch (Exception e) {
....
}
}
.....
Connection conn = null;
Statement stmt = null;
try {
conn = ds.getConnection("userid", "password");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ....");
while(rs.next()){
.....
}
rs.close();
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) try{conn.close();}catch(Exception e){}
}
------------------------------------------------------------------
IBM WebSphere Application Server 2.0.x
import java.sql.*;
import com.ibm.servlet.connmgr.*;
// ConnMgr Cache 사용을 위한 connMgr 객체 static 초기화
private static IBMConnMgr connMgr = null;
private static IBMConnSpec spec = null;
static {
try {
String poolName = "JdbcDb2"; // defined in WebSphere Admin Console
spec = new IBMJdbcConnSpec(poolName, false,
"com.ibm.db2.jdbc.app.DB2Driver",
"jdbc:db2:<db_name>", // "jdbc:db2://ip_address:6789/<db_name>",
"userid","password");
connMgr = IBMConnMgrUtil.getIBMConnMgr();
}
catch(Exception e){
.....
}
}
.....
IBMJdbcConn cmConn = null; // "cm" maybe stands for Connection Manager.
Statement stmt = null;
try {
cmConn = (IBMJdbcConn)connMgr.getIBMConnection(spec);
Connection conn = jdbcConn.getJdbcConnection();
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ....");
while(rs.next()){
.....
}
rs.close();
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( cmConn != null ) try{cmConn.releaseIBMConnection();}catch(Exception e){}
}
// NOTE: DO NOT "conn.close();" !!
------------------------------------------------------------------
Oracle OSDK(Oracle Servlet Development Kit)
import java.sql.*;
import oracle.ec.ctx.*;
.....
oracle.ec.ctx.Trx trx = null;
Connection conn = null;
Statement stmt = null;
try {
oracle.ec.ctx.TrxCtx ctx = oracle.ec.ctx.TrxCtx.getTrxCtx();
trx = ctx.getTrx();
conn = trx.getConnection("<pool_name>");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ....");
while(rs.next()){
.....
}
rs.close();
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) try{ trx.close(conn,"<pool_name>");}catch(Exception e){}
}
// NOTE: DO NOT "conn.close();" !!
------------------------------------------------------------------
Hans Bergsten 의 DBConnectionManager.java
import java.sql.*;
.....
db.DBConnectionManager connMgr = null;
Connection conn = null;
Statement stmt = null;
try {
connMgr = db.DBConnectionManager.getInstance();
conn = connMgr.getConnection("<pool_name>");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ....");
while(rs.next()){
.....
}
rs.close();
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) connMgr.freeConnection("<pool_name>", conn);
}
// NOTE: DO NOT "conn.close();" !!
------------------------------------------------------------------
JDF 의 DB Connection Pool Framework
import java.sql.*;
import com.lgeds.jdf.*;
import com.lgeds.jdf.db.*;
import com.lgeds.jdf.db.pool.*;
private static com.lgeds.jdf.db.pool.JdbcConnSpec spec = null;
static {
try {
com.lgeds.jdf.Config conf = new com.lgeds.jdf.Configuration();
spec = new com.lgeds.jdf.db.pool.JdbcConnSpec(
conf.get("gov.mpb.pbf.db.emp.driver"),
conf.get("gov.mpb.pbf.db.emp.url"),
conf.get("gov.mpb.pbf.db.emp.user"),
conf.get("gov.mpb.pbf.db.emp.password")
);
}
catch(Exception e){
.....
}
}
.....
PoolConnection poolConn = null;
Statement stmt = null;
try {
ConnMgr mgr = ConnMgrUtil.getConnMgr();
poolConn = mgr.getPoolConnection(spec);
Connection conn = poolConnection.getConnection();
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ....");
while(rs.next()){
.....
}
rs.close();
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( poolConn != null ) poolConn.release();
}
// NOTE: DO NOT "conn.close();" !!
------------------------------------------------------------------
여기서 하고픈 얘기는 DB Connection Pool 을 구현하는 방식에 따라서 개발자의 소스도
전부 제 각기 다른 API를 사용해야 한다는 것입니다.
프로젝트를 이곳 저곳 뛰어 본 분은 아시겠지만, 매 프로젝트마나 어플리케이션 서버가
다르고 지난 프로젝트에서 사용된 소스를 새 프로젝트에 그대로 적용하지 못하게 됩니다.
JDBC 관련 API가 다르기 때문이죠.
같은 제품일지라도 버전업이 되면서 API가 변해버리는 경우도 있습니다. 예를 들면,
IBM WebSphere 버전 2.0.x에서 버전 3.0.x로의 전환할 때, DB 연결을 위한 API가
변해버려 기존에 개발해둔 400 여개의 소스를 다 뜯어 고쳐야 하는 것과 같은 상황이
벌어질 수도 있습니다.
닷컴업체에서 특정 패키지 제품을 만들때도 마찬가지 입니다. 자사의 제품이 어떠한
어플리케이션 서버에서 동작하도록 해야 하느냐에 따라 코딩할 API가 달라지니 소스를
매번 변경해야만 하겠고, 특정 어플리케이션 서버에만 동작하게 하려니 마켓시장이
좁아지게 됩니다.
IBM WebSphere, BEA WebLogic 뿐만 아니라 Apache JServ 나 JRun, 혹은 Tomcat 에서도
쉽게 포팅하길 원할 것입니다.
이것을 해결하는 방법은 우리들 "SE(System Engineer)"만의 고유한 "Connection Adapter
클래스"를 만들어서 사용하는 것입니다.
예를 들어 개발자의 소스는 이제 항상 다음과 같은 유형으로 코딩되면 어떻겠습니까 ?
-----------------------------------------------------------
.....
ConnectionResource resource = null;
Statement stmt = null;
try {
resource = new ConnectionResource();
Connection conn = resource.getConnection();
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ....");
while(rs.next()){
.....
}
rs.close();
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( resource != null ) resource.release();
}
// NOTE: DO NOT "conn.close();" !!
-----------------------------------------------------------
이 때 ConnectionResource 는 다음과 같은 형식으로 누군가 한분이 만들어 두면 되겠죠.
-----------------------------------------------------------
import java.sql.*;
public class ConnectionResource
{
private java.sql.Connection conn = null;
public ConnectionResource() throws Exception {
......
conn = ..... GET Connection by Connection Pooling API
}
public Connection getConnection() throws Exception {
return conn;
}
public void release(){
// release conn into "Connection Pool"
.......
}
}
-----------------------------------------------------------
예를 들어 IBM WebSphere Version 3.0.2.x 의 경우를 든다면 다음과 같이 될 겁니다.
-----------------------------------------------------------
package org.jsn.connpool;
/*
* ConnectionResource V 1.0
* JDBC Connection Pool Adapter for IBM WebSphere V 3.0.x/3.5.x
* Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
* Last Modified : 2000.11.10
* NOTICS: You can re-distribute or copy this source code freely,
* you can NOT remove the above subscriptions.
*/
/* IBM WebSphere 3.0.2.x for JDK 1.1.8 */
//import java.sql.*;
//import javax.naming.*;
//import com.ibm.ejs.dbm.jdbcext.*;
//import com.ibm.db2.jdbc.app.stdext.javax.sql.*;
/* IBM WebSphere 3.5.x for JDK 1.2.2 */
import java.sql.*;
import javax.sql.*;
import javax.naming.*;
public class ConnectionResource
{
private static final String userid = "userid";
private static final String password = "password"
private static final String datasource = "jdbc/<data_source_name>";
private static DataSource ds = null;
private java.sql.Connection conn = null;
public ConnectionResource() throws Exception {
synchronized ( ConnectionResource.class ) {
if ( ds == null ) {
java.util.Hashtable props = new java.util.Hashtable();
props.put(Context.INITIAL_CONTEXT_FACTORY,
"com.ibm.ejs.ns.jndi.CNInitialContextFactory");
Context ctx = null;
try {
ctx = new InitialContext(props);
ds = (DataSource)ctx.lookup("jdbc/<data_source_name>");
}
finally {
if ( ctx != null ) ctx.close();
}
}
}
conn = ds.getConnection( userid, password );
}
public Connection getConnection() throws Exception {
if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
return conn;
}
public void release(){
// release conn into "Connection Pool"
if ( conn != null ) try { conn.close(); }catch(Excepton e){}
conn = null;
}
}
-----------------------------------------------------------
만약 Hans Bersten 의 DBConnectionManager.java 를 이용한 Connection Pool 이라면
다음과 같이 만들어 주면 됩니다.
-----------------------------------------------------------
package org.jsn.connpool;
import java.sql.*;
public class ConnectionResource
{
private String poolname = "<pool_name>";
private Connection conn = null;
private db.DBConnectionManager connMgr = null;
public ConnectionResource() throws Exception {
connMgr = db.DBConnectionManager.getInstance();
conn = connMgr.getConnection(poolname);
}
public Connection getConnection() throws Exception {
if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
return conn;
}
public void release(){
if ( conn != null ) {
// Dirty Transaction을 rollback시키는 부분인데, 생략하셔도 됩니다.
boolean autoCommit = true;
try{ autoCommit = conn.getAutoCommit(); }catch(Exception e){}
if ( autoCommit == false ) {
try { conn.rollback(); }catch(Exception e){}
try { conn.setAutoCommit(true); }catch(Exception e){}
}
connMgr.freeConnection(poolname, conn);
conn = null;
}
}
}
-----------------------------------------------------------
또, Resin 1.2.x 의 경우라면 다음과 같이 될 겁니다.
-----------------------------------------------------------
package org.jsn.connpool;
/*
* ConnectionResource V 1.0
* JDBC Connection Pool Adapter for Resin 1.2.x
* Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
* Last Modified : 2000.10.18
* NOTICS: You can re-distribute or copy this source code freely,
* you can NOT remove the above subscriptions.
*/
import java.sql.*;
import javax.sql.*;
import javax.naming.*;
public class ConnectionResource
{
private static final String datasource = "jdbc/<data_source_name>";
private static final String userid = "userid";
private static final String password = "password"
private static DataSource ds = null;
private java.sql.Connection conn = null;
public ConnectionResource() throws Exception {
synchronized ( ConnectionResource.class ) {
if ( ds == null ) {
Context env = (Context) new InitialContext().lookup("java:comp/env");
ds = (DataSource) env.lookup(datasource);
}
}
conn = ds.getConnection( userid, password );
}
public Connection getConnection() throws Exception {
if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
return conn;
}
public void release(){
// release conn into "Connection Pool"
if ( conn != null ) try { conn.close(); }catch(Excepton e){}
conn = null;
}
}
-----------------------------------------------------------
Oracle 8i(8.1.6이상)에서 Oracle의 JDBC 2.0 Driver 자체가 제공하는 Connection
Pool을 이용한다면 다음과 같이 될 겁니다.
-----------------------------------------------------------
package org.jsn.connpool;
/*
* ConnectionResource V 1.0
* JDBC Connection Pool Adapter for Oracle JDBC 2.0
* Author: WonYoung Lee, javaservice@hanmail.net, 011-898-7904
* Last Modified : 2001.10.29
* NOTICS: You can re-distribute or copy this source code freely,
* but you can NOT remove the above subscriptions.
*/
import java.sql.*;
import javax.sql.*;
import oracle.jdbc.driver.*;
import oracle.jdbc.pool.*;
public class ConnectionResource
{
private static final String dbUrl = "jdbc:oracle:thin@192.168.0.1:1521:ORCL";
private static final String userid = "userid";
private static final String password = "password"
private static OracleConnectionCacheImpl oraclePool = null;
private static boolean initialized = false;
private java.sql.Connection conn = null;
public ConnectionResource() throws Exception {
synchronized ( ConnectionResource.class ) {
if ( initialized == false ) {
DriverManager.registerDriver (new oracle.jdbc.driver.OracleDriver());
oraclePool = new OracleConnectionCacheImpl();
oraclePool.setURL(dbUrl);
oraclePool.setUser(userid);
oraclePool.setPassword(password);
oraclePool.setMinLimit(10);
oraclePool.setMaxLimit(50);
//oraclePool.setCacheScheme(OracleConnectionCacheImpl.FIXED_WAIT_SCHEME);
// FIXED_WAIT_SCHEME(default), DYNAMIC_SCHEME, FIXED_RETURN_NULL_SCHEME
//oraclePool.setStmtCacheSize(0); //default is 0
initialized = true;
}
}
conn = oraclePool.getConnection();
}
public Connection getConnection() throws Exception {
if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
return conn;
}
public void release(){
// release conn into "Connection Pool"
if ( conn != null ) try { conn.close(); }catch(Exception e){}
conn = null;
}
}
-----------------------------------------------------------
또한 설령 Connection Pool 기능을 지금을 사용치 않더라도 향후에 적용할 계획이
있다면, 우선은 다음과 같은 Connection Adapter 클래스를 미리 만들어 두고 이를
사용하는 것이 효과적입니다. 향후에 이 클래스만 고쳐주면 되니까요.
Oracle Thin Driver를 사용하는 경우입니다.
-----------------------------------------------------------
import java.sql.*;
public class ConnectionResource
{
private static final String userid = "userid";
private static final String password = "password"
private static final String driver = "oracle.jdbc.driver.OracleDriver"
private static final String url = "jdbc:oracle:thin@192.168.0.1:1521:ORCL";
private static boolean initialized = false;
private java.sql.Connection conn = null;
public ConnectionResource() throws Exception {
synchronized ( ConnectionResource.class ) {
if ( initialized == false ) {
Class.forName(driver);
initialized = true;
}
}
conn = DriverManager.getConnection( url, userid, password );
}
public Connection getConnection() throws Exception {
if ( conn == null ) throw new Exception("Connection is NOT avaiable !!");
return conn;
}
public void release(){
if ( conn != null ) try { conn.close(); }catch(Excepton e){}
conn = null;
}
}
-----------------------------------------------------------
프로그램의 유형이나, 클래스 이름이야 뭐든 상관없습니다. 위처럼 우리들만의 고유한
"Connection Adapter 클래스"를 만들어서 사용한다는 것이 중요하고, 만약 어플리케이션
서버가 변경된다거나, DB Connection Pooling 방식이 달라지면 해당 ConnectionResource
클래스 내용만 살짝 고쳐주면 개발자의 소스는 전혀 고치지 않아도 될 것입니다.
NOTE: ConnectionResource 클래스를 만들때 주의할 것은 절대 Exception 을 클래스 내부에서
가로채어 무시하게 하지 말라는 것입니다. 그냥 그대로 throw 가 일어나게 구현하세요.
이렇게 하셔야만 개발자의 소스에서 "Exception 처리"를 할 수 있게 됩니다.
NOTE2: ConnectionResource 클래스를 만들때 반드시 package 를 선언하도록 하세요.
IBM WebSphere 3.0.x 의 경우 JSP에서 "서블렛클래스패스"에 걸려 있는 클래스를
참조할 때, 그 클래스가 default package 즉 package 가 없는 클래스일 경우 참조하지
못하는 버그가 있습니다. 통상 "Bean"이라 불리는 클래스 역시 조건에 따라 인식하지
않을 수도 있습니다.
클래스 다지인 및 설계 상으로 보더라도 package 를 선언하는 것이 바람직합니다.
NOTE3: 위에서 "userid", "password" 등과 같이 명시적으로 프로그램에 박아 넣지
않고, 파일로 관리하기를 원한다면, 그렇게 하셔도 됩니다.
JDF의 Configuration Framework 을 참조하세요.
PS: 혹자는 왜 ConnectionResource 의 release() 메소드가 필요하냐고 반문할 수도 있습
니다. 예를 들어 개발자의 소스가 다음처럼 되도록 해도 되지 않느냐라는 거죠.
-----------------------------------------------------------
.....
Connection conn = null;
Statement stmt = null;
try {
conn = ConnectionResource.getConnection(); // <---- !!!
stmt = conn.createStatement();
.....
}
catch(Exception e){
.....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) try{conn.close();}catch(Exception e){} // <---- !!!
}
-----------------------------------------------------------
이렇게 하셔도 큰 무리는 없습니다. 그러나, JDBC 2.0 을 지원하는 제품에서만
conn.close() 를 통해 해당 Connection 을 실제 close() 시키는 것이 아니라 DB Pool에
반환하게 됩니다. BEA WebLogic 이나 IBM WebSphere 3.0.2.x, 3.5.x 등이 그렇습니다.
그러나, 자체제작된 대부분의 DB Connection Pool 기능은 Connection 을 DB Pool에
반환하는 고유한 API를 가지고 있습니다. WebSphere Version 2.0.x 에서는
cmConn.releaseIBMConnection(), Oracle OSDK 에서는 trx.close(conn, "<pool_name">);
Hans Bersten 의 DBConnectionManager 의 경우는
connMgr.freeConnection(poolname, conn); 등등 서로 다릅니다. 이러한 제품들까지
모두 지원하려면 "release()" 라는 우리들(!)만의 예약된 메소드가 꼭 필요하게 됩니다.
물론, java.sql.Connection Interface를 implements 한 별도의 MyConnection 을 만들어
두고, 실제 java.sql.Connection 을 얻은 후 MyConnection의 생성자에 그 reference를
넣어준 후, 이를 return 시에 넘기도록 하게 할 수 있습니다. 이때, MyConnection의
close() 함수를 약간 개조하여 DB Connection Pool로 돌아가게 할 수 있으니까요.
PS: 하나 이상의 DB 를 필요로 한다면, 다음과 같은 생성자를 추가로 만들어서 구분케
할 수도 있습니다.
-----------------------------------------------------------
....
private String poolname = "default_pool_name";
private String userid = "scott";
private String password = "tiger";
public ConnectionResource() throws Exception {
initialize();
}
public ConnectionResource(String poolname) throws Exception {
this.poolname = poolname;
initialize();
}
public ConnectionResource(String poolname,String userid, String passwrod)
throws Exception
{
this.poolname = poolname;
this.userid = userid;
this.password = password;
initialize();
}
private void initialize() throws Exception {
....
}
...
-----------------------------------------------------------
-------------------------------------------------------------------------------
2001.03.20 추가
2001.10.22 수정
2001.11.09 수정(아래 설명을 추가함)
실 운영 사이트의 장애진단 및 튜닝을 다녀보면, 장애의 원인이 DataBase 연결개수가
지속적으로 증가하고, Connection Pool 에서 더이상 가용한 연결이 남아 있지 않아
발생하는 문제가 의외로 많습니다. 이 상황의 십중팔구는 개발자의 코드에서 Pool로
부터 가져온 DB연결을 사용하고 난 후, 이를 여하한의 Exception 상황에서도 다시
Pool로 돌려보내야 한다는 Rule 를 지키지 않아서 발생한 문제가 태반입니다.
이름만 말하면 누구나 알법한 큼직한 금융/뱅킹사이트의 프로그램소스에서도 마찬가지
입니다.
문제는 분명히 어떤 특정 응용프로그램에서 DB Connection 을 제대로 반환하지 않은
것은 분명한데, 그 "어떤 특정 응용프로그램"이 꼭집어 뭐냐 라는 것을 찾아내기란
정말 쉽지 않습니다. 정말 쉽지 않아요. 1초당 수십개씩의 Request 가 다양하게 들어
오는 상황에서, "netstat -n"으로 보이는 TCP/IP 레벨에서의 DB연결수는 분명히 증가
하고 있는데, 그 수십개 중 어떤 것이 문제를 야기하느냐를 도저히 못찾겠다는 것이지요.
사용자가 아무도 없는 새벽에 하나씩 컨텐츠를 꼭꼭 눌러 본들, 그 문제의 상황은
대부분 정상적인 로직 flow 에서는 나타나지 않고, 어떤 특별한 조건, 혹은 어떤
Exception 이 발생할 때만 나타날 수 있기 때문에, 이런 방법으로는 손발과 눈만 아프게
되곤 합니다.
따라서, 애초 부터, 이러한 상황을 고려하여, 만약, 개발자의 코드에서 실수로 DB연결을
제대로 Pool 에 반환하지 않았을 때, 그 개발자 프로그램 소스의 클래스 이름과 함께
Warnning 성 메세지를 남겨 놓으면, 되지 않겠느냐는 겁니다.
Java 에서는 java.lang.Object 의 finalize() 라는 기막힌 메소드가 있습니다. 해당
Object instance의 reference 가 더이상 그 어떤 Thread 에도 남아 있지 않을 경우
JVM의 GC가 일어날 때 그 Object 의 finalize() 메소드를 꼭 한번 불러주니까요.
계속 반복되는 얘기인데, 아래 글에서 또한번 관련 의미를 찾을 수 있을 것입니다.
Re: DB Connection Pool: Orphan and Idle Timeout
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=1005294960
아래는 이것을 응용하여, Hans Bergsten 의 DBConnectionManager 를 사용하는
ConnectionAdapter 클래스를 만들어 본 샘플입니다. Trace/Debuging 을 위한 몇가지
기능이 첨가되어 있으며, 다른 ConnectionPool을 위한 Connection Adapter 를 만들때도
약간만 수정/응용하여 사용하실 수 있을 것입니다.
/**
* Author : Lee WonYoung, javaservice@hanmail.net
* Date : 2001.03.20, 2001.10.22
*/
import java.util.*;
import java.sql.*;
import java.io.*;
import java.text.SimpleDateFormat;
public class ConnectionResource
{
private boolean DEBUG_MODE = true;
private String DEFAULT_DATASOURCE = "idb"; // default db name
private long GET_CONNECTION_TIMEOUT = 2000; // wait only 2 seconds if no
// avaiable connection in the pool
private long WARNNING_MAX_ELAPSED_TIME = 3000;
private SimpleDateFormat df = new SimpleDateFormat("yyyyMMdd/HHmmss");
private db.DBConnectionManager manager = null;
private Connection conn = null;
private String datasource = DEFAULT_DATASOURCE;
private String caller = "unknown";
private long starttime = 0;
private static int used_conn_count = 0;
private static Object lock = new Object();
// detault constructor
public ConnectionResource()
{
manager = db.DBConnectionManager.getInstance();
}
// For debugging, get the caller object reference
public ConnectionResource(Object caller_obj)
{
this();
if ( caller_obj != null )
caller = caller_obj.getClass().getName();
}
public Connection getConnection() throws Exception
{
return getConnection(DEFAULT_DATASOURCE);
}
public Connection getConnection(String datasource) throws Exception
{
if ( conn != null ) throw new Exception
("You must release the connection first to get connection again !!");
this.datasource = datasource;
// CONNECTION_TIMEOUT is very important factor for performance tuning
conn = manager.getConnection(datasource, GET_CONNECTION_TIMEOUT);
synchronized( lock ) { ++used_conn_count; }
starttime = System.currentTimeMillis();
return conn;
}
// you don't have to get "connection reference" as parameter,
// because we already have the referece as the privae member variable.
public synchronized void release() throws Exception {
if ( conn == null ) return;
// The following is needed for some DB connection pool.
boolean mode = true;
try{
mode = conn.getAutoCommit();
}catch(Exception e){}
if ( mode == false ) {
try{conn.rollback();}catch(Exception e){}
try{conn.setAutoCommit(true);}catch(Exception e){}
}
manager.freeConnection(datasource, conn);
conn = null;
int count = 0;
synchronized( lock ) { count = --used_conn_count; }
if ( DEBUG_MODE ) {
long endtime = System.currentTimeMillis();
if ( (endtime-starttime) > WARNNING_MAX_ELAPSED_TIME ) {
System.err.println(df.format(new java.util.Date()) +
":POOL:WARNNING:" + count +
":(" + (endtime-starttime) + "):" +
"\t" + caller
);
}
}
}
// finalize() method will be called when JVM's GC time.
public void finalize(){
// if "conn" is not null, this means developer did not release the "conn".
if ( conn != null ) {
System.err.println(df.format(new java.util.Date()) +
":POOL:ERROR connection was not released:" +
used_conn_count + ":\t" + caller
);
release();
}
}
}
-------------------------------------------------------------------------------
위의 Connection Adaptor 클래스를 Servlet 이나 JSP에서 사용할 때는 다음과 같이
사용할 수 있습니다.
사용법: DB Transaction 처리가 필요치 않을 때...
ConnectionResource resource = null;
Connection conn = null;
Statement stmt = null;
try{
// For debugging and tracing, I recommand to send caller's reference to
// the adapter's constructor.
resource = new ConnectionResource(this); // <--- !!!
//resource = new ConnectionResource();
conn = resource.getConnection();
// or you can use another database name
//conn = resource.getConnection("other_db");
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select ...");
while(rs.next()){
.....
}
rs.close();
}
//catch(Exception e){
// // error handling if you want
// ....
//}
finally{
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) resource.release(); // <--- !!!
}
-------------------------------------------------------------------------------
사용법: Transaction 처리가 필요할 때.
ConnectionResource resource = null;
Connection conn = null;
Statement stmt = null;
try{
// For debugging and tracing, I recommand to send caller's reference to
// the adapter's constructor.
resource = new ConnectionResource(this);
//resource = new ConnectionResource();
conn = resource.getConnection();
// or you can use another database name
//conn = resource.getConnection("other_db");
conn.setAutoCommit(false); // <--- !!!
stmt = conn.createStatement();
stmt.executeUpdate("update ...");
stmt.executeUpdate("insert ...");
stmt.executeUpdate("update ...");
stmt.executeUpdate("insert ...");
stmt.executeUpdate("update ...");
int affected = stmt.executeUpdate("update....");
// depends on your business logic,
if ( affected == 0 )
throw new Exception("NoAffectedException");
else if ( affected > 1 )
throw new Exception("TooManyAffectedException:" + affected);
conn.commit(); //<<-- commit() must locate at the last position in the "try{}"
}
catch(Exception e){
// if error, you MUST rollback
if ( conn != null ) try{conn.rollback();}catch(Exception e){}
// another error handling if you want
....
throw e; // <--- throw this exception if you want
}
finally{
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) resource.release(); // <-- NOTE: autocommit mode will be
// initialized
}
-----------------------------------------------------------------
PS: 더 깊이있는 내용을 원하시면 다음 문서들을 참조 하세요...
JDF 제4탄 - DB Connection Pool
http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945156790
JDF 제5탄 - DB Connection Resource Framework
http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945335633
JDF 제6탄 - Transactional Connection Resource Framework
http://www.javaservice.net/~java/bbs/read.cgi?m=jdf&b=framework&c=r_p&n=945490586
PS: IBM WebSphere의 JDBC Connection Pool 에 관심이 있다면 다음 문서도 꼭 참조
하세요...
Websphere V3 Connection Pool 사용법
http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=970209527
WebSphere V3 DB Connection Recovery 기능 고찰
http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=967473008
------------------------
NOTE: 2004.04.07 추가
그러나, 2004년 중반을 넘어서는 이 시점에서는, ConnectionResource와 같은 Wapper의
중요성이 별로 높이 평가되지 않는데, 그 이유는 Tomcat 5.0을 비롯한 대부분의 WAS(Web
Application Server)가 자체의 Connection Poolinig기능을 제공하며, 그 사용법은 다음과
같이 JDBC 2.0에 준하여 모두 동일한 형태를 띠고 있기 때문입니다.
InitialContext ctx = new InitialContext();
DataSource ds = (DataSoruce)ctx.lookup("java:comp/env/jdbc/ds");
Connection conn = ds.getConnection();
...
conn.close();
결국 ConnectionResource의 release()류의 메소드는 Vendor별로, JDBC Connection Pool의
종류가 난무하던 3-4년 전에는 어플리케이션코드의 Vendor종속성을 탈피하기 위해 의미를
가졌으나, 지금은 굳이 필요가 없다고 보여 집니다.
단지, caller와 callee의 정보, 즉, 응답시간을 추적한다거나, close() 하지 않은
어플리케이션을 추적하는 의미에서의 가치만 남을 수 있습니다. (그러나, 이것도,
IBM WebSphere v5의 경우, 개발자가 conn.close() 조차 하지 않을 지라도 자동을 해당
Thread의 request ending시점에 "자동반환"이 일어나게 됩니다.)
---------------------
2005.01.21 추가
JDBC Connection/Statement/ResultSet을 close하지 않은 소스의 위치를 잡아내는 것은
실제 시스템 운영 중에 찾기란 정말 모래사장에서 바늘찾기 같은 것이었습니다.
그러나, 제니퍼(Jennifer2.0)과 같은 APM을 제품을 적용하시면, 운영 중에, 어느 소스의
어느 위치에서 제대로 반환시키지 않았는지를 정확하게 찾아줍니다.
뿐만 아니라, 모든 SQL의 수행통계 및 현재 수행하고 있는 어플리케이션이 어떤 SQL을
수행중인지 실시간으로 확인되니, 성능저하를 보이는 SQL을 튜닝하는 것 등, 많은 부분들이
명확해져 가고 있습니다. 이젠 더이상 위 글과 같은 문서가 필요없는 세상을 기대해 봅니다.
-------------------------------------------------------
본 문서는 자유롭게 배포/복사 할 수 있으나 반드시
이 문서의 저자에 대한 언급을 삭제하시면 안됩니다
================================================
자바서비스넷 이원영
E-mail: javaservice@hanmail.net
PCS:011-898-7904
================================================
1.1 서블렛에서 instance variable 의 공유 - PrintWriter -
다음과 같은 코드를 생각해 보겠습니다.
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class CountServlet extends HttpServlet {
private PrintWriter out = null; // <-------------- (1)
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
res.setContentType("text/html");
out = res.getWriter();
for(int i=0;i<20;i++){
out.println("count= " + (i+1) + "<br>"); // <---- (2)
out.flush();
try{Thread.sleep(1000);}catch(Exception e){}
}
}
}
위의 CountServlet.java 를 컴파일하여 돌려 보면, 1초간격으로 일련의 숫자가 올라가는
것이 보일 겁니다.(서블렛엔진의 구현방식에 따라 Buffering 이 되어 20초가 모두 지난
후에서 퍽 나올 수도 있습니다.)
혼자서 단일 Request 를 날려 보면, 아무런 문제가 없겠지만, 이제 브라우져 창을 두개 이상
띄우시고 10초의 시간 차를 두시면서 동시에 호출해 보세요... 이상한 증상이 나타날
겁니다. 먼저 호출한 창에는 10 까지 정도만 나타나고, 10초 뒤에 호출한 창에서는 먼저
호출한 창에서 나타나야할 내용들까지 덤으로 나타나는 것을 목격할 수 있을 겁니다.
이는 서블렛의 각 호출은 Thread 로 동작하여, 따라서, 각 호출은 위의 (1) 에서 선언한
instance variable 들을 공유하기 때문에 나타나는 문제입니다.
위 부분은 다음과 같이 고쳐져야 합니다.
public class CountServlet extends HttpServlet {
//private PrintWriter out = null;
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
PrintWriter out = null; // <--- 이 쪽으로 와야죠 !!!
res.setContentType("text/html");
out = res.getWriter();
for(int i=0;i<20;i++){
out.println("count= " + (i+1) + "<br>"); // <---- (2)
out.flush();
try{Thread.sleep(1000);}catch(Exception e){}
}
}
}
국내 몇몇 Servlet 관련 서적의 일부 예제들이 위와 같은 잘못된 형태로 설명한
소스코드들이 눈에 띕니다. 빠른 시일에 바로 잡아야 할 것입니다.
실제 프로젝트 환경에서 개발된 실무시스템에서도, 그러한 책을 통해 공부하신듯, 동일한
잘못된 코딩을 하고 있는 개발자들이 있습니다. 결과적으로 테스트 환경에서는 나타나지
않더니만, 막상 시스템을 오픈하고나니 고객으로 부터 다음과 같은 소리를 듣습니다.
"내 데이타가 아닌데 남의 데이타가 내 화면에 간혹 나타나요. refresh 를 누르면 또,
제대로 되구요" .....
1.2 서블렛에서 instance variable 의 공유
앞서의 경우와 의미를 같이하는데, 다음과 같이 하면 안된다는 얘기지요.
public class BadServlet extends HttpServlet {
private String userid = null;
private String username = null;
private int hitcount = 0;
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
res.setContentType("text/html");
PrintWriter out = res.getWriter();
userid = request.getParameter("userid");
username = request.getParameter("username");
hitcount = hitcount + 1;
....
}
}
새로운 매 HTTP 요청마다 userid/username변수는 새롭게 할당됩니다. 문제는 그것이 특정
사용자에 한하여 그러한 것이 아니라, BadServlet의 인스턴스(instance)는 해당
웹컨테이너(Web Container)에 상에서 (예외경우가 있지만) 단 하나만 존재하고, 서로 다른
모든 사용자들의 서로 다른 모든 요청들에 대해서 동일한 userid/username 및 count 변수를
접근하게 됩니다. 따라서, 다음과 같이 메소드 안으로 끌어들여 사용하여야 함을 강조합니다.
public class BadServlet extends HttpServlet {
//private String userid = null; // <---- !!
//private String username = null; // <---- !!
private int hitcount = 0;
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
res.setContentType("text/html");
PrintWriter out = res.getWriter();
String userid = request.getParameter("userid"); // <---- !!
String username = request.getParameter("username"); // <---- !!
//또한, instance 변수에 대한 접근은 적어도 아래처럼 동기화를 고려해야...
synchronized(this){ hitcount = hitcount + 1; }
....
}
}
1.3 서블렛에서 instance variable 의 공유 - DataBase Connection -
public class TestServlet extends HttpServlet {
private final static String drv = "oracle.jdbc.driver.OracleDriver";
private final static String url = "jdbc:orache:thin@210.220.251.96:1521:ORA8i";
private final static String user = "scott";
private final static String password = "tiger";
private ServletContext context;
private Connection conn = null; <--- !!!
private Statement stmt = null; <------ !!!
private ResultSet rs = null; <------ !!!
public void init(ServletConfig config) throws ServletException {
super.init(config);
context = config.getServletContext();
try {
Class.forName(drv);
}
catch (ClassNotFoundException e) {
throw new ServletException("Unable to load JDBC driver:"+ e.toString());
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
String id = req.getParameter("id");
conn = DriverManager.getConnection(url,user,password); ---- (1)
stmt = conn.createStatement(); ---------- (2)
rs = stmt.executeQuery("select .... where id = '" + id + "'"); ----- (3)
while(rs.next()) { ----------- (4)
...... --------- (5)
}
rs.close(); -------- (6)
stmt.close(); ---- (7)
conn.close(); --- (8)
.....
}
}
위에서 뭐가 잘못되었죠? 여러가지가 있겠지만, 그 중에 하나가 java.sql.Connection과
java.sql.Statment, java.sql.ResultSet을 instance variable 로 사용하고 있다는 것입니다.
이 서블렛은 사용자가 혼자일 경우는 아무런 문제를 야기하지 않습니다. 그러나 여러사람이
동시에 이 서블렛을 같이 호출해 보면, 이상한 증상이 나타날 것입니다.
그 이유는 conn, stmt, rs 등과 같은 reference 들을 instance 변수로 선언하여 두었기
때문에 발생합니다. 서블렛은 Thread로 동작하며 위처럼 instance 변수 영역에 선언해 둔
reference 들은 doGet(), doPost() 를 수행하면서 각각의 요청들이 동시에 공유하게 됩니다.
예를 들어, 두개의 요청이 약간의 시간차를 두고 비슷한 순간에 doGet() 안으로 들어왔다고
가정해 보겠습니다.
A 라는 요청이 순차적으로 (1), (2), (3) 까지 수행했을 때, B 라는 요청이 곧바로 doGet()
안으로 들어올 수 있습니다. B 역시 (1), (2), (3) 을 수행하겠죠...
이제 요청 A 는 (4) 번과 (5) 번을 수행하려 하는데, 가만히 생각해 보면, 요청B 로 인해
요청A에 의해 할당되었던 conn, stmt, rs 의 reference 들은 바뀌어 버렸습니다.
결국, 요청 A 는 요청 B 의 결과를 가지고 작업을 하게 됩니다. 반면, 요청 B 는
요청 A 의 의해 rs.next() 를 이미 수행 해 버렸으므로, rs.next() 의 결과가 이미 close
되었다는 엉뚱한 결과를 낳고 마는 거죠...
다른 쉬운 얘기로 설명해 보면, A, B 두사람이 식탁에 앉아서 각자 자신이 준비해 온 사과를
하나씩 깎아서 식탁 위의 접시에 올려 놓고 나중에 먹어려 하는 것과 동일합니다. A 라는
사람이 열심히 사과를 깎아 접시에 담아둘 때, B 라는 사람이 들어와서 A가 깎아둔 사과를
버리고 자신이 깎은 사과를 대신 접시에 담아 둡니다. 이제 A라는 사람은 자신이 깎아서
담아 두었다고 생각하는 그 사과를 접시에서 먹어버립니다. 곧이어 B라는 사람이 자신의
사과를 접시에서 먹어려 하니 이미 A 가 먹고 난 후 였습니다. 이는 접시를 두 사람이
공유하기 때문에 발생하는 문제잖습니까.
마찬가지로 서블렛의 각 Thread는 instance variable 를 공유하기 때문에 동일한 문제들을
발생하게 됩니다.
따라서 최소한 다음처럼 고쳐져야 합니다.
public class TestServlet extends HttpServlet {
private final static String drv = "...";
private final static String url = "....";
private final static String user = "...";
private final static String password = "...";
private ServletContext context;
public void init(ServletConfig config) throws ServletException {
super.init(config);
context = config.getServletContext();
try {
Class.forName(drv);
}
catch (ClassNotFoundException e) {
throw new ServletException("Unable to load JDBC driver:"+ e.toString());
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
Connection conn = null; <----- 이곳으로 와야죠..
Statement stmt = null; <-------
ResultSet rs = null; <---------
String id = req.getParameter("id");
conn = DriverManager.getConnection(url,user,password);
stmt = conn.createStatement();
rs = stmt.executeQuery("select ..... where id = '" + id + "'");
while(rs.next()) {
......
}
rs.close();
stmt.close();
conn.close();
.....
}
}
1.4 JSP에서 Instance Variable 공유
JSP에서 아래처럼 사용하는 경우가 위의 경우와 동일한 instance 변수를 공유하는 경우가
됩니다.
---------------------------------------------------------
<%@ page session=.... import=.... contentType=........ %>
<%!
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
String userid = null;
%>
<html><head></head><body>
<%
........
conn = ...
stmt = .....
uesrid = ......
%>
</body></html>
---------------------------------------------------------
마찬가지로 위험천만한 일이며, 여러 Thread 에 의해 그 값이 변할 수 있는 변수들은
<%! ... %> 를 이용하여 선언하시면 안됩니다. 이처럼 instance 변수로 사용할 것은
다음 처럼, 그 값이 변하지 않는 값이거나, 혹은 공유변수에 대한 특별한 관리를
하신 상태에서 하셔야 합니다.
<%! private static final String USERID = "scott";
private static final String PASSWORD = "tiger";
%>
JSP에서의 이와 같은 잘못된 유형도, 앞선 서블렛의 경우처럼 일부 국내 JSP관련
서적에서 발견됩니다. 해당 서적의 저자는 가능한 빨리 개정판을 내셔서 시정하셔야
할 것입니다. 해당 책은 출판사나 책의 유형, 그리고 글자체로 추정건데, 초보자가
쉽게 선택할 법한 책인 만큼 그 파급력과 영향력이 너무 큰 듯 합니다.
이와 같은 부분이 실 프로젝트에서 존재할 경우, 대부분 시스템 오픈 첫날 쯤에
문제를 인식하게 됩니다. Connection reference 가 엎어쳐지므로 Pool 에 반환이
일어나지 않게 되고, 이는 "connection pool"의 가용한 자원이 부하가 얼마 없음에도
불구하고 모자라는 현상으로 나타나며, 때론 사용자의 화면에서는 엉뚱한 다른
사람의 데이타가 나타나거나, SQLException 이 나타납니다.
NOTE: 어떻게하란 말입니까? 각 호출간에 공유되어서는 안될 변수들은 <%! ...%> 가
아니라, <% ... %> 내에서 선언하여 사용하란 얘깁니다. JSP가 Pre-compile되어
Servlet형태로 변환될 때, <%! ... %>는 서블렛의 instance variable로 구성되는 반면,
<% ... %>는 _jspService() 메소드 내의 method variable로 구성됩니다.
2. 하나의 Connection을 init()에서 미리 연결해 두고 사용하는 경우.
public class TestServlet extends HttpServlet {
private final static String drv = "oracle.jdbc.driver.OracleDriver";
private final static String url = "jdbc:orache:thin@210.220.251.96:1521:ORA8i";
private final static String user = "scott";
private final static String password = "tiger";
private ServletContext context;
private Connection conn = null; <--- !!!
public void init(ServletConfig config) throws ServletException {
super.init(config);
context = config.getServletContext();
try {
Class.forName(drv);
conn = DriverManager.getConnection(url,user,password);
}
catch (ClassNotFoundException e) {
throw new ServletException("Unable to load JDBC driver:"+ e.toString());
}
catch (SQLException e) {
throw new ServletException("Unable to connect to database:"+ e.toString());
}
}
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
Statement stmt = null;
ResultSet rs = null;
String id = req.getParameter("id");
stmt = conn.createStatement();
rs = stmt.executeQuery("select ..... where id = '" + id + "'");
while(rs.next()) {
......
}
rs.close();
stmt.close();
.....
}
public void destroy() {
if ( conn != null ) try {conn.close();}catch(Exception e){}
}
}
위는 뭐가 잘못되었을 까요? 서블렛당 하나씩 java.sql.Connection 을 init()에서 미리
맺어 두고 사용하는 구조 입니다.
얼핏 생각하면 아무런 문제가 없을 듯도 합니다. doGet() 내에서 별도의 Statement와
ResultSet 을 사용하고 있으니, 각 Thread는 자신만의 Reference를 갖고 사용하게 되니까요.
이 구조는 크게 세가지의 문제를 안고 있습니다. 하나는 DB 연결자원의 낭비를 가져오며,
두번째로 수많은 동시사용자에 대한 처리한계를 가져오고, 또 마지막으로 insert, update,
delete 와 같이 하나 이상의 SQL문장을 수행하면서 단일의 Transaction 처리를 보장받을
수 없다는 것입니다.
1) DB 자원의 낭비
위의 구조는 서블렛당 하나씩 java.sql.Connection 을 점유하고 있습니다. 실 프로젝트에서
보통 서블렛이 몇개나 될까요? 최소한 100 개에서 400개가 넘어 갈 때도 있겠죠?
그럼 java.sql.Connection에 할당 되어야 할 "DB연결갯수"도 서블렛 갯수 많큼 필요하게
됩니다. DB 연결 자원은 DB 에서 설정하기 나름이지만, 통상 maximum 을 셋팅하기
마련입니다. 그러나 아무런 요청이 없을 때도 400 여개의 DB연결이 연결되어 있어야 한다는
것은 자원의 낭비입니다.
2) 대량의 동시 사용자 처리 불가.
또한, 같은 서블렛에 대해 동시에 100 혹은 그 이상의 요청이 들어온다고 가정해 보겠습
니다. 그럼 같은 java.sql.Connection 에 대해서 각각의 요청이 conn.createStatement() 를
호출하게 됩니다.
문제는 하나의 Connection 에 대해 동시에 Open 할 수 있는 Statement 갯수는 ( 이 역시
DB 에서 셋팅하기 나름이지만 ) maximum 제한이 있습니다. Oracle 의 경우 Default는 50
입니다. 만약 이 수치 이상을 동시에 Open 하려고 하면 "maximum open cursor exceed !"
혹은 "Limit on number of statements exceeded"라는 SQLExceptoin 을 발생하게 됩니다.
예를 들어 다음과 같은 프로그램을 실행시켜 보세요.
public class DbTest {
public static void main(String[] args) throws Exception {
Class.forName("jdbc driver...");
Connection conn = DriverManager.getConnection("url...","id","password");
int i=0;
while(true) {
Statement stmt = conn.createStatement();
System.out.println( (++i) + "- stmt created");
}
}
}
과연 몇개 까지 conn.createStement() 가 수행될 수 있을까요? 이는 DB에서 설정하기 나름
입니다. 중요한 것은 그 한계가 있다는 것입니다.
또한 conn.createStatement() 통해 만들어진 stmt 는 java.sql.Connection 의 자원이기
때문에 위처럼 stmt 의 reference 가 없어졌다고 해도 GC(Garbage Collection)이 되지
않습니다.
3) Transaction 중복현상 발생
예를 들어 다음과 같은 서비스가 있다고 가정해 보겠습니다.
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
Statement stmt = null;
String id = req.getParameter("id");
try {
conn.setAutoCommit(false);
stmt = conn.createStatement();
stmt.executeUpdate("update into XXXX..... where id = " + id + "'");
stmt.executeUpdate("delete from XXXX..... where id = " + id + "'");
stmt.executeUpdate(".... where id = " + id + "'");
stmt.executeUpdate(".... where id = " + id + "'");
conn.commit();
}
catch(Exception e){
try{conn.rollback();}catch(Exception e){}
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
conn.setAutoCommit(true);
}
.....
}
아무런 문제가 없을 듯도 합니다. 그러나 위의 서비스를 동시에 요청하게 되면, 같은
java.sql.Connection 을 갖고 작업을 하고 있으니 Transaction 이 중첩되게 됩니다.
왜냐면, conn.commit(), conn.rollback() 과 같이 conn 이라는 Connection 에 대해서
Transaction 이 관리되기 때문입니다. 요청 A 가 총 4개의 SQL문장 중 3개를 정상적으로
수행하고 마지막 4번째의 SQL문장을 수행하려 합니다. 이 때 요청 B가 뒤따라 들어와서
2개의 SQL 문장들을 열심히 수행했습니다. 근데, 요청 A에 의한 마지막 SQL 문장
수행중에 SQLException 이 발생했습니다. 그렇담 요청 A 는 catch(Exception e) 절로
분기가 일어나고 conn.rollback() 을 수행하여 이미 수행한 3개의 SQL 수행들을 모두
rollback 시킵니다. 근데,,, 문제는 요청 B 에 의해 수행된 2개의 SQL문장들도 같이
싸잡아서 rollback() 되어 버립니다. 왜냐면 같은 conn 객체니까요. 결국, 요청B 는
영문도 모르고 마지막 2개의 SQL문장만 수행한 결과를 낳고 맙니다.
따라서 정리하면, Connection, Statement, ResultSet 는 doGet() , doPost() 내에서
선언되고 사용되어져야 합니다.
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
Connection conn = null; <----- 이곳으로 와야죠..
Statement stmt = null; <-------
ResultSet rs = null; <---------
.....
}
3. Exception 이 발생했을 때도 Connection 은 닫혀야 한다 !!
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
String id = req.getParameter("id");
Connection conn = DriverManager.getConnection("url...","id","password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("ssselect * from XXX where id = '" + id + "'");
while(rs.next()) {
......
}
rs.close();
stmt.close();
conn.close();
.....
}
위에선 뭐가 잘못되었을까요? 네, 사실 특별히 잘못된 것 없습니다. 단지 SQL문장에 오타가
있다는 것을 제외하곤.... 근데, 과연 그럴까요?
SQLException 이라는 것은 Runtime 시에 발생합니다. DB 의 조건이 맞지 않는다거나
개발기간 중에 개발자의 실수로 SQL문장에 위처럼 오타를 적을 수도 있죠.
문제는 Exception 이 발생하면 마지막 라인들 즉, rs.close(), stmt.close(), conn.close()
가 수행되지 않는다는 것입니다.
java.sql.Connection 은 reference 를 잃더라도 JVM(Java Virtual Machine)의 GC(Garbage
Collection) 대상이 아닙니다. 가뜩이나 모자라는 "DB연결자원"을 특정한 어플리케이션이
점유하고 놓아 주지 않기 때문에 얼마안가 DB Connection 을 더이상 연결하지 못하는
사태가 발생합니다.
따라서 다음처럼 Exception 이 발생하든 발생하지 않든 반드시 java.sql.Connection 을
close() 하는 로직이 꼭(!) 들어가야 합니다.
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
String id = req.getParameter("id");
try {
conn = DriverManager.getConnection("url...","id","password");
stmt = conn.createStatement();
rs = stmt.executeQuery("sselect * from XXX where id = '" + id + "'");
while(rs.next()) {
......
}
rs.close();
stmt.close();
}
finally {
if ( conn != null ) try {conn.close();}catch(Exception e){}
}
.....
}
참고로, 실프로젝트의 진단 및 튜닝을 나가보면 처음에는 적절한 응답속도가 나오다가
일정한 횟수 이상을 호출하고 난 뒤부터 엄청 응답시간이 느려진다면, 십중팔구는 위처럼
java.sql.Connection 을 닫지 않아서 생기는 문제입니다.
가용한 모든 Connection 을 연결하여 더이상 연결시킬 Connection 자원을 할당할 수 없을
때, 대부분 timewait 이 걸리기 때문입니다. 일단 DB관련한 작업이 들어오는 족족
timewait에 빠질 경우, "어플리케이션서버"에서 동시에 처리할 수 있는 최대 갯수만큼
호출이 차곡차곡 쌓이는 건 불과 몇분 걸리지 않습니다. 그 뒤부터는 애궂은 dummy.jsp
조차 호출이 되지 않게 되고, 누군가는 "시스템 또 죽었네요"라며 묘한 웃음을 짓곤
하겠죠....
4. Connection 뿐만 아니라 Statement, ResultSet 도 반드시 닫혀야 한다 !!
4.1 3번의 예제에서 Connection 의 close() 만 고려하였지 Statement 나 ResultSet 에 대한
close는 전혀 고려 하지 않고 있습니다. 무슨 문제가 있을까요? Statement 를 닫지 않아도
Connection 을 닫았으니 Statement 나 ResultSet 은 자동으로 따라서 닫히는 것 아니냐구요?
천만의 말씀, 만만의 콩깎지입니다.
만약, DB Connection Pooling 을 사용하지 않고 직접 JDBC Driver 를 이용하여 매번 DB
연결을 하였다가 끊는 구조라면 문제가 없습니다.
그러나 DB Connection Pooling 은 이젠 보편화되어 누구나 DB Connection Pooling 을 사용
해야한다는 것을 알고 있습니다. 그것이 어플리케이션 서버가 제공해 주든, 혹은 작은
서블렛엔진에서 운영하고 있다면 직접 만들거나, 인터넷으로 돌아다니는 남의 소스를 가져다
사용하고 있을 겁니다.
이처럼 DB Connection Pooling 을 사용하고 있을 경우는 Conneciton 이 실제 close()되는
것이 아니라 Pool에 반환되어 지게 되는데, 결국 reference가 사리지지 않기 때문에 GC시점에
자동 close되지 않게 됩니다.
특정 Connection 에서 열어둔 Statement 를 close() 하지 않은채 그냥 반환시켜 놓게 되면,
언젠가는 그 Connection 은 다음과 같은 SQLException 을 야기할 가능성을 내포하게 됩니다.
Oracle :
java.sql.SQLException : ORA-01000: maximum open cursor exceeded !!
(최대열기 커서수를 초과했습니다)
UDB DB2 :
COM.ibm.db2.jdbc.DB2Exception: [IBM][JDBC 드라이버] CLI0601E 유효하지 않은
명령문 핸들 또는 명령문이 닫혔습니다. SQLSTATE=S1000
COM.ibm.db2.jdbc.DB2Exception: [IBM][CLI Driver] CLI0129E 핸들(handle)이
더이상 없습니다. SQLSTATE=HY014
COM.ibm.db2.jdbc.DB2Exception: [IBM][CLI Driver][DB2/NT] SQL0954C 응용프로그램
힙(heap)에 명령문을 처리하기 위해 사용 가능한 저장영역이 충분하지 않습니다.
SQLSTATE=57011
이유는 앞 2)번글에서 이미 언급드렸습니다. 보다 자세한 기술적 내용은 아래의 글을
참고하세요.
Connection/Statement 최대 동시 Open 수
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=jdbc&c=r_p&n=972287002
따라서 또다시 3번의 소스는 다음과 같은 유형으로 고쳐져야 합니다.
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
String id = req.getParameter("id");
try {
conn = ...<getConnection()>...; // (편의상 생략합니다.)
stmt = conn.createStatement();
rs = stmt.executeQuery("sselect * from XXX where id = '" + id + "'");
while(rs.next()) {
......
}
// rs.close();
// stmt.close();
}
finally {
if ( rs != null ) try {rs.close();}catch(Exception e){}
if ( stmt != null ) try {stmt.close();}catch(Exception e){} // <-- !!!!
if ( conn != null ) ...<releaseConnection()>...; // (편의상 생략)
}
.....
}
4.2 사실 위와 같은 구조에서, java.sql.Statement의 쿼리에 의한 ResultSet의 close()에
대한 것은 그리 중요한 것은 아닙니다. ResultSet은 Statement 가 close() 될 때 함께
자원이 해제됩니다. 따라서 다음과 같이 하셔도 됩니다.
public void doGet(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException, SQLException
{
Connection conn = null;
Statement stmt = null;
String id = req.getParameter("id");
try {
conn = ...<getConnection()>...;
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("sselect * from XXX where id = '" + id + "'");
while(rs.next()) {
......
}
rs.close(); //<--- !!!
}
finally {
// if ( rs != null ) try {rs.close();}catch(Exception e){}
if ( stmt != null ) try {stmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
.....
}
4.3 가장 대표적으로 잘못 프로그래밍하고 있는 예를 들라면 다음과 같은 유형입니다.
Connection conn = null;
try {
conn = ...<getConnection()>....;
Statement stmt = conn.createStatement();
stmt.executeUpdate("...."); <--- 여기서 SQLException 이 일어나면...
.....
.....
..... <-- 만약,이쯤에서 NullPointerException 이 일어나면 ?
.....
stmt.close(); <-- 이것을 타지 않음 !!!
}
finally{
if ( conn != null ) ...<releaseConnection()>...;
}
4.4 Statement 가 close() 되지 않는 또 하나의 경우가 다음과 같은 경우입니다.
Connection conn = null;
Statement stmt = null;
try {
conn = .....
stmt = conn.createStatement(); // ....(1)
rs = stmt.executeQuery("select a from ...");
.....
rs.close();
stmt = conn.createStatement(); // ....(2)
rs = stmt.executeQuery("select b from ...");
....
}
finally{
if ( rs != null ) try {rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ....
}
즉, 두번 stmt = conn.createStatement() 를 수행함으로써, 먼저 생성된 stmt 는
(2)번에 의해 그 reference 가 엎어쳐버리게 되어 영원히 close() 되지 않은채
남아 있게 됩니다.
이 경우, (2)번 문장을 주석처리하여야 겠지요. 한번 생성된 Statement 로 여러번
Query 를 수행하면 됩니다.
Connection conn = null;
Statement stmt = null;
try {
conn = .....
stmt = conn.createStatement(); // ....(1)
rs = stmt.executeQuery("select a from ...");
.....
rs.close();
// stmt = conn.createStatement(); // <--- (2) !!!
rs = stmt.executeQuery("select b from ...");
....
}
finally{
if ( rs != null ) try {rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ....
}
4.5 Statement 뿐만 아니라 PreparedStatement 를 사용할 때로 마찬가지 입니다.
....
PreparedStatement pstmt = conn.prepareStatement("select ....");
....
이렇게 만들어진 pstmt 도 반드시 pstmt.close() 되어야 합니다.
예를 들면, 다음과 같은 코드를 생각할 수 있습니다.
Connection conn = null;
try {
conn = ...<getConnection()>...;
PreparedStatement pstmt = conn.prepareStatement("select .... ?...?");
pstmt.setString(1,"xxxx");
pstmt.setString(2,"yyyy");
ResultSet rs = pstmt.executeQuery(); <--- 여기서 SQLException 이 일어나면
while(rs.next()){
....
}
rs.close();
pstmt.close(); <-- 이것을 타지 않음 !!!
}
finally{
if ( conn != null ) ...<releaseConnection()>...;
}
따라서 같은 맥락으로 다음과 같이 고쳐져야 합니다.
Connection conn = null;
PreparedStatement pstmt = null; // <-------- !!
ResultSet rs = null;
try {
conn = ...<getConnection()>...;
pstmt = conn.prepareStatement("select .... ?...?");
pstmt.setString(1,"xxxx");
pstmt.setString(2,"yyyy");
rs = pstmt.executeQuery(); <--- 여기서 SQLException 이 일어나더라도...
while(rs.next()){
....
}
//rs.close();
//pstmt.close();
}
finally{
if ( rs != null ) try {rs.close();}catch(Exception e){}
if ( pstmt != null ) try {pstmt.close();}catch(Exception e){} // <-- !!!!
if ( conn != null ) ...<releaseConnection()>...;
}
4.6 PreparedStatement 에 관련해서 다음과 같은 경우도 생각할 수 있습니다.
4.6.1 앞서의 4.4에서 Statement를 createStatement() 연거푸 두번 생성하는 것과
동일하게 PreparedStatement에서도 주의하셔야 합니다. 예를 들면 다음과 같습니다.
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = .....
pstmt = conn.prepareStatement("select a from ..."); // ....(1)
rs = pstmt.executeQuery();
.....
rs.close();
pstmt = conn.prepareStatement("select b from ..."); // <--- (2) !!!
rs = pstmt.executeQuery();
....
}
finally{
if ( rs != null ) try {rs.close();}catch(Exception e){}
if ( pstmt != null ) try{pstmt.close();}catch(Exception e){} // <--- (3)
if ( conn != null ) ...<releaseConnection()>...;
}
Statement의 인스턴스는 conn.createStatement() 시에 할당되어 지는 반면,
PreparedStatement는 conn.prepareStatement("..."); 시에 할당되어 집니다. 위의 경우에서
설령 마지막 finally 절에서 pstmt.close() 를 하고 있기는 하지만, (1)번에서 할당되어진
pstmt 는 (2)에서 엎어쳤기 때문에 영원히 close() 되지 않게 됩니다. 따라서, 다음과
같이 코딩되어야 한다는 것은 자명합니다.
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = .....
pstmt = conn.prepareStatement("select a from ..."); // ....(1)
rs = pstmt.executeQuery();
.....
rs.close();
pstmt.close(); // <------- !!!!! 이처럼 여기서 먼저 close() 해야지요.
pstmt = conn.prepareStatement("select b from ..."); // <--- (2)
rs = pstmt.executeQuery();
....
}
finally{
if ( rs != null ) try {rs.close();}catch(Exception e){}
if ( pstmt != null ) try{pstmt.close();}catch(Exception e){} // <--- (3)
if ( conn != null ) ...<releaseConnection()>...;
}
혹은 다음과 같이 서로 다른 두개의 PreparedStatement를 이용할 수도 있습니다.
Connection conn = null;
PreparedStatement pstmt1 = null;
ResultSet rs1 = null;
PreparedStatement pstmt2 = null;
ResultSet rs2 = null;
try {
conn = .....
pstmt1 = conn.prepareStatement("select a from ..."); // ....(1)
rs1 = pstmt1.executeQuery();
.....
pstmt2 = conn.prepareStatement("select b from ..."); // <--- (2)
rs2 = pstmt2.executeQuery();
....
}
finally{
if ( rs1 != null ) try {rs1.close();}catch(Exception e){}
if ( pstmt1 != null ) try{pstmt1.close();}catch(Exception e){} // <--- (3)
if ( rs2 != null ) try {rs2.close();}catch(Exception e){}
if ( pstmt2 != null ) try{pstmt2.close();}catch(Exception e){} // <--- (4)
if ( conn != null ) ...<releaseConnection()>...;
}
4.6.2 아래는 앞서의 4.6.1과 같은 맥락인데, for loop 안에서 사용된 경우입니다.
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = ...<getConnection()>...;
for(int i=0;i<10;i++){
pstmt = conn.prepareStatement("update .... ?... where id = ?"); //... (1)
pstmt.setString(1,"xxxx");
pstmt.setString(2,"id"+(i+1) );
int affected = pstmt.executeUpdate();
if ( affected == 0 ) throw new Exception("NoAffected");
else if ( affedted > 1 ) throw new Exception("TooManyAffected");
}
}
finally{
if ( pstmt != null ) try {pstmt.close();}catch(Exception e){} // ...(2)
if ( conn != null ) ...<releaseConnection()>...;
}
이 경우가 실제 프로젝트 performace 튜닝을 가 보면 종종 발견되는 잘못된
유형 중의 하나입니다. 핵심은 pstmt.prepareStatement("update..."); 문장을
수행할 때 마다 내부적으로 pstmt 가 새로 할당된다는 것에 있습니다.
결국, (1)번 문장이 for loop 을 돌면서 9개는 pstmt.close()가 되지 않고, 마지막
하나의 pstmt 만 finally 절의 (2)번에서 close 되지요...
따라서 다음처럼 고쳐져야 합니다.
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = ...<getConnection()>...;
pstmt = conn.prepareStatement("update .... ?... where id = ?");
for(int i=0;i<10;i++){
pstmt.clearParameters();
pstmt.setString(1,"xxxx");
pstmt.setString(2,"id"+(i+1) );
int affected = pstmt.executeUpdate();
if ( affected == 0 ) throw new Exception("NoAffected");
else if ( affedted > 1 ) throw new Exception("TooManyAffected");
}
}
finally{
if ( pstmt != null ) try {pstmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
PreparedStatement 라는 것이, 한번 파싱하여 동일한 SQL문장을 곧바로 Execution할 수
있는 장점이 있는 것이고, 궁극적으로 위와 같은 경우에 효과를 극대화 할 수 있는
것이지요.
어느 개발자의 소스에서는 위의 경우를 다음과 같이 for loop 안에서 매번
conn.prepareStatement(...)를 하는 경우를 보았습니다.
Connection conn = null;
try {
conn = ...<getConnection()>...;
for(int i=0;i<10;i++) {
PreparedStatement pstmt = null;
try{
pstmt = conn.prepareStatement("update .... ?... where id = ?");
pstmt.clearParameters();
pstmt.setString(1,"xxxx");
pstmt.setString(2,"id"+(i+1) );
int affected = pstmt.executeUpdate();
if ( affected == 0 ) throw new Exception("NoAffected");
else if ( affedted > 1 ) throw new Exception("TooManyAffected");
}
finally{
if ( pstmt != null ) try {pstmt.close();}catch(Exception e){}
}
}
}
finally{
if ( conn != null ) ...<releaseConnection()>...;
}
위 경우는 장애관점에서 보면 사실 별 문제는 없습니다. 적어도 닫을 건 모두 잘 닫고
있으니까요. 단지 효율성의 문제가 대두될 수 있을 뿐입니다.
4.7 생각해 보면, Statement 나 PreparedStatement 가 close() 되지 않는 유형은
여러가지가 있습니다. 그러나 열려진 Statement는 반드시 close()되어야 한다라는
단순한 사실에 조금만 신경쓰시면 어떻게 프로그래밍이 되어야 하는지 쉽게
감이 오실 겁니다. 단지 신경을 안쓰시니 문제가 되는 것이지요...
Statement 를 닫지 않는 실수를 한 코드가 400-1000여개의 전 어플리케이션을 통털어
한두군데에 숨어 있는 경우가 제일 찾아내기 어렵습니다. 한두번 Statement 를
close 하지 않는다고 하여 곧바로 문제로 나타나지 않기 때문입니다.
하루나 이틀, 혹은 며칠지나서야, Oracle 의 경우, "maximum open cursor exceed"
에러를 내게 됩니다. oracle 의 경우, 위와 같은 에러를 conn.createStatement()시에
발생하므로 (경우에 따라) 큰 문제로 이어지지는 않습니다. 찾아서 고치면 되니까요.
반면, DB2 의 경우는 메모리가 허용하는 한도까지 지속적인 메모리 증가를 야기합니다.
급기야 "핸들(handle)이 더이상 없습니다", "응용프로그램 힙(heap)에 명령문을
처리하기 위해 사용 가능한 저장영역이 충분하지 않습니다", "유효하지 않은 명령문
핸들 또는 명령문이 닫혔습니다" 등과 같은 에러를 내지만, 여기서 그치는 것이 아니라
새로운 connection 을 맺는 시점에 SIGSEGV 를 내면 crashing 이 일어납니다.
java.lang.OutOfMemoryError 가 발생하기 때문이지요.
Oracle 의 경우도 사실 상황에 따라 심각할 수 있습니다. 예를 들어, 어떤 개발자가
"maximum open cursor exceed"라는 Oracle SQL 에러 메세지를 만나고는 자신이
코딩을 잘못한 것은 염두에 두지 않고, 무조건 DBA에게 oraXXX.ini 파일에서
OPEN_CURSORS 값을 올려달라고 요청하고, DBA는 그 조언(?)을 충실히 받아들여
Default 50 에서 이를 3000 으로 조정합니다. 여기서 문제는 깊숙히(?) 숨겨집니다.
close() 안된 Statement 가 3000 회에 도달하지 전까진 아무도 문제를 인식하지
못하기 때문이죠. 그러나, Statement가 하나씩 close()되지 않은 갯수에 비례하여
Oracle 엔진의 메모리 사용은 자꾸만 증가하고, 전체적인 성능저하를 야기하지만,
이를 인식하기란 막상 시스템을 오픈하고 나서 3-4일이 지난 후에 "DB성능이 이상하네,
응답속도가 느리네" 하면서 또 한번의 "maximum open cursor exceed" 메세지를
확인하고 난 뒤의 일이 되곤 합니다.
에러가 없는 정상적인 로직 flow 에서는 대부분 Statement가 잘 닫힐 겁니다. 그러나,
어떤 이유에서건 아주 드물게 Runtime Exception 이 발생하여 exception throwing으로
인해 "stmt.close()"를 타지 않는 경우가 제일 무섭지요. 정말 무섭지요...
Statement, PreparedStatement, CallableStatement 모두 마찬가지 입니다.
5. close() 를 할 땐 제대로 해야 한다!!
5.1 다음과 같은 프로그램 형식을 생각할 수 있습니다.
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
conn = ...<getConnection()>...; //.......(1)
stmt = conn.createStatement(); //.............(2)
rs = stmt.executeQuery("select ....."); // .....(3)
while(rs.next()){
......
}
}
finally {
try {
rs.close(); //........(4)
stmt.close(); //......(5)
...<releaseConneciton()>...; //......(6)
}catch(Exception e){}
}
위에선 뭐가 잘못되었을까요? 다 제대로 한듯 한데....
finally 절에서 rs, stmt, conn 을 null check 없이, 그리고 동일한 try{}catch 절로
실행하고 있습니다.
예를 들어, (1), (2) 번을 거치면서 conn 과 stmt 객체까지는 제대로 수행되었으나
(3)번 Query문장을 수행하는 도중에 SQLException 이 발생할 수 있습니다. 그러면,
finally 절에서 (4) 번 rs.close()를 수행하려 합니다. 그러나, executeQuery()가
실패 했기 때문에 rs 의 reference 는 null 이므로 rs.close() 시에
NullPointerException 이 발생합니다. 결국 정작 반드시 수행되어야할 (5)번, (6)번이
실행되지 않습니다. 따라서 반드시(!) 다음과 같은 형식의 코딩이 되어야 합니다.
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try{
conn = ...<getConnection()>...;
stmt = conn.createStatement();
rs = stmt.executeQuery("select .....");
while(rs.next()){
......
}
}
catch(Exception e){
....
}
finally {
if ( rs != null ) try{rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
같은 맥락으로 PreparedStatement 를 사용할 경우도, 다음과 같은 코딩은 잘못되었습니다.
Connection conn = null;
PreparedStatement pstmt = null;
try{
conn = ...<getConnection()>...; //.......(1)
pstmt = conn.prepareStatement("ddelete from EMP where empno=7942"); //...(2)
int k = pstmt.executeUpdate(); // .....(3)
......
}
finally {
try {
pstmt.close(); //......(4)
...<releaseConneciton()>...; //......(5)
}catch(Exception e){}
}
왜냐면, SQL문장에 오타가 있었고, 이는 prepareStatement("ddelete..")시점에 에러가
발생하여 pstmt 가 여전히 null 인 상태로 finally 절로 분기되기 때문인 것이죠.
5.2.0 앞서 4.2에서도 언급했지만, java.sql.Statement의 executeQuery()에 의한 ResultSet은
Statement 가 close 될때 자원이 같이 해제되므로 다음과 같이 하여도 그리 문제가 되진
않습니다.
Connection conn = null;
Statement stmt = null;
//ResultSet rs = null;
try{
conn = ...<getConnection()>...;
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select .....");
while(rs.next()){
......
}
rs.close(); // <---- !!!
}
catch(Exception e){
....
}
finally {
//if ( rs != null ) try{rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
5.2.1 그러나 PreparedStatement에 의한 ResultSet close()는 얘기가 다를 수 있습니다.
(2002.06.11 추가 사항)
많은 분들이, "아니 설령 ResultSet를 close하지 않았더라도 Statement/PreparedStatement를
close하면 함께 ResultSet로 close되는 것 아니냐, JDBC 가이드에서도 그렇다고
나와 있다, 무슨 개뿔같은 소리냐?" 라구요.
그러나, 한가지 더 이해하셔야 할 부분은, 웹스피어, 웹로직과 같은 상용 웹어플리케이션
서버들은 성능향상을 위해 PreparedStatement 및 심지어 Statement에 대한 캐싱기능을
제공하고 있습니다. pstmt.close() 를 하였다고 하여 정말 해당 PreparedStatement가
close되는 것이 아니라, 해당 PreparedeStatement가 생겨난 java.sql.Connection당 지정된
개수까지 웹어플리케이션서버 내부에서 reference가 사라지지 않고 캐시로 남아 있게 됩니다.
결국, 연관된 ResultSet 역시 close()가 되지 않은 채 남아있게 되는 것이지요. 명시적인
rs.close()를 타지 않으면, 웹어플리케이션서버의 JVM내부 힙영역 뿐만아니라, 쿼리의 결과로
데이타베이스에서 임시로 만들어진 메모리데이타가 사라지지 않는 결과를 낳게 됩니다.
특히 ResultSet이 닫히지 않으면 DB에서의 CURSOR가 사라지지 않습니다.
또한, CURSOR를 자동으로 닫거나 닫지 않는 등의 옵션은 WAS마다 WAS의 DataSource설정에서
CORSOR 핸들링에 대한 별도의 옵션을 제공하게 되며 특히 아래 [항목6]에서 다시 언급할
nested sql query 처리시에 민감한 반응을 하게 됩니다.
따라서, 앞서의 코딩 방법을 반드시 다음처럼 ResultSet도 close하시기를 다시금 정정하여
권장 드립니다.
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null; // <---- !!!
try{
conn = ...<getConnection()>...;
pstmt = conn.prepareStatement("select .....");
rs = pstmt.executeQuery(); // <----- !!!
while(rs.next()){
......
}
//rs.close(); // <---- !!!
}
catch(Exception e){
....
}
finally {
if ( rs != null ) try{rs.close();}catch(Exception e){} // <---- !!!
if ( pstmt != null ) try{pstmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
PS: 웹어플리케이션서버에서의 PreparedStatement 캐싱기능에 관한 부분은 아래의 글들을
참조하세요.
Connection Pool & PreparedStatement Cache Size
http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=995572195
WebLogic에서의 PreparedStatement Cache -서정희-
http://www.javaservice.net/~java/bbs/read.cgi?m=dbms&b=jdbc&c=r_p&n=1023286823
5.3 간혹, 다음과 같은 코딩은 문제를 야기하지 않을 것이라고 생각하는 분들이 많습니다.
finally{
try{
if ( stmt != null ) stmt.close();
if ( conn != null ) conn.close();
}catch(Exception e){}
}
저명한 많은 책에서도 위처럼 코딩되어 있는 경우를 종종봅니다. 맞습니다. 특별히 문제성이
있어보이지는 않습니다. 그러나, 간혹 웹어플리케이션서버의 Connection Pool과 연계하여
사용할 땐 때론 문제가 될 때도 있습니다. 즉, 만약, stmt.close()시에 Exception이 throw
되면 어떻게 되겠습니까?
아니 무슨 null 체크까지 했는데, 무슨 Exception이 발생하느냐고 반문할 수도 있지만,
오랜 시간이 걸리는 SQL JOB에 빠져 있다가 Connection Pool의 "Orphan Timeout"이 지나
자동으로 해당 Connection을 Pool에 돌려보내거나 혹은 특별한 marking처리를 해 둘 수
있습니다. 이 경우라면 stmt.close()시에 해당 웹어플리케이션서버에 특화된 Exception이
발생하게 됩니다. 그렇게 되면 conn.close()를 타지 못하게 되는 사태가 벌어집니다.
따라서, 앞서 하라고 권장한 형식으로 코딩하세요.
6. Nested (Statemet) SQL Query Issue !!
6.1 아래와 같은 코딩을 생각해 보겠습니다.
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select deptno ...where id ='"+ id +"'");
while(rs.next()){
String deptno = rs.getString("deptno");
stmt.executeUpdate(
"update set dept_name = ... where deptno = '"+ deptno +"'"
);
......
}
rs.close();
}
catch(Exception e){
....
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
위 코드는 사실 실행하자 말자 다음과 같은 에러를 만나게 됩니다.
DB2 : -99999: [IBM][CLI Driver] CLI0115E 커서 상태가 유효하지 않습니다.
SQLSTATE=24000
Oracle :
에러는 두번째 while(rs.next()) 시에 발생합니다. 왜냐면, Statement가 nested하게
실행된 executeUpdate() 에 의해 엎어쳐 버렸기 때문입니다. ResultSet 은
Statement와 밀접하게 관련이 있습니다. ResultSet은 자료를 저장하고 있는 객체가
아니라, 데이타베이스와 step by step으로 상호 통신하는 Interface 일 뿐이기 때문입니다.
따라서 위 코드는 다음처럼 바뀌어져야 합니다.
Connection conn = null;
Statement stmt1 = null;
Statement stmt2 = null;
try{
conn = ...<getConnection()>...;
stmt1 = conn.createStatement();
stmt2 = conn.createStatement();
ResultSet rs = stmt1.executeQuery("select deptno ...where id ='"+ id +"'");
while(rs.next()){
String deptno = rs.getString("deptno");
stmt2.executeUpdate(
"update set dept_name = ... where deptno = '"+ deptno +"'"
);
......
}
rs.close();
}
catch(Exception e){
....
}
finally {
if ( stmt1 != null ) try{stmt1.close();}catch(Exception e){}
if ( stmt2 != null ) try{stmt2.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
즉, ResultSet에 대한 fetch 작업이 아직 남아 있는 상태에서 관련된 Statement를
또다시 executeQuery()/executeUpdate() 를 하면 안된다는 것입니다.
PS: IBM WebSphere 환경일 경우, 아래의 글들을 추가로 확인하시기 바랍니다.
349 Re: Function sequence error (Version 3.x)
http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=991154615
486 WAS4.0x: Function sequence error 해결
http://www.javaservice.net/~java/bbs/read.cgi?m=appserver&b=was&c=r_p&n=1015345459
7. executeUpdate() 의 결과를 비즈니스 로직에 맞게 적절히 활용하라.
7.1 아래와 같은 코딩을 생각해 보겠습니다.
public void someMethod(String empno) throws Exception {
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
stmt = conn.createStatement();
stmt.executeUpdate(
"UPDATE emp SET name='이원영' WHERE empno = '" + empno + "'"
);
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
}
사실 흔히들 이렇게 하십니다. 별로 잘못된 것도 없어보입니다. 근데, 만약
DB TABLE에 해당 empno 값이 없으면 어떻게 될까요? SQLException 이 발생
하나요? 그렇지 않죠? 아무런 에러없이 그냥 흘러 내려 옵니다. 그러면, Update를
하러 들어 왔는데, DB에 Update할 것이 없었다면 어떻게 해야 합니까? 그냥 무시하면
되나요? 안되죠.. 따라서, 다음 처럼, 이러한 상황을 이 메소드를 부른 곳으로
알려 줘야 합니다.
public void someMethod(String empno) throws Exception {
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
stmt = conn.createStatement();
int affected = stmt.executeUpdate(
"UPDATE emp SET name='이원영' WHERE empno = '" + empno + "'"
);
if ( affected == 0 ) throw new Exception("NoAffectedException");
else if ( affected > 1 ) throw new Exception("TooManyAffectedException");
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
}
물론 이러한 부분들은 해당 비즈니스로직이 뭐냐에 따라서 다릅니다. 그것을 무시케
하는 것이 비즈니스 로직이었다면 그냥 무시하시면 되지만, MIS 성 어플리케이션의
대부분은 이처럼 update 나 delete 쿼리의 결과에 따라 적절한 처리를 해 주어야
할 것입니다.
8. Transaction 처리를 할 땐 세심하게 해야 한다.
단, 아래는 웹어플리케이션서버의 JTA(Java Transaction API)기반의 트렌젝션처리가 아닌,
java.sql.Connection에서의 명시적인 conn.setAutoCommit(false) 모드를 통한 트렌젝션처리에
대해서 언급 하고 있습니다.
8.1 Non-XA JDBC Transaction(명시적인 java.sql.Connection/commit/rollback)
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
stmt = conn.createStatement();
stmt.executeUpdate("UPDATE ...."); // -------- (1)
stmt.executeUpdate("DELETE ...."); // -------- (2)
stmt.executeUpdate("INSERT ...."); // -------- (3)
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
if ( conn != null ) ...<releaseConnection()>...;
}
위와 같은 코딩은 아무리 비즈니스적 요구가 간소하더라도, 실 프로젝트에서는
있을 수가 없는 코딩입니다.(JTS/JTA가 아닌 명시적인 Transaction 처리시에..)
다들 아시겠지만, (1), (2) 번까지는 정상적으로 잘 수행되었는데, (3)번문장을
수행하면서 SQLException 이 발생하면 어떻게 되나요? (1),(2)은 이미 DB에 반영된
채로 남아 있게 됩니다. 대부분의 비즈니스로직은 그렇지 않았을 겁니다.
"(1),(2),(3)번이 모두 정상적으로 수행되거나, 하나라도 잘못되면(?) 모두 취소
되어야 한다"
가 일반적인 비즈니스적 요구사항이었을 겁니다. 따라서, 다음처럼 코딩 되어야
합니다.
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
conn.setAutoCommit(false);
stmt = conn.createStatement();
stmt.executeUpdate("UPDATE ...."); // -------- (1)
stmt.executeUpdate("DELETE ...."); // -------- (2)
stmt.executeUpdate("INSERT ...."); // -------- (3)
conn.commit(); // <-- 반드시 try{} 블럭의 마지막에 와야 합니다.
}
catch(Exception e){
if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
// error handling if you want
throw e; // <--- 필요한 경우, 호출한 곳으로 Exception상황을 알려줄
// 수도 있습니다
}
finally {
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
// in some connection pool, you have to reset commit mode to "true"
if ( conn != null ) ...<releaseConnection()>...;
}
8.2 auto commit mode 를 "false"로 셋팅하여 명시적인 Transaction 관리를 할 때,
정말 조심해야 할 부분이 있습니다. 예를 들면 다음과 같은 경우입니다.
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
conn.setAutoCommit(false);
stmt = conn.createStatement();
stmt.executeUpdate("UPDATE ...."); // ----------------------- (1)
ResultSet rs = stmt.executeQuery("SELECT ename ..."); // ---- (2)
if ( rs.next() ) {
conn.commit(); // ------------------- (3)
}
else {
conn.rollback(); // ----------------- (4)
}
}
finally {
if ( rs != null ) try{rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
// in some connection pool, you have to reset commit mode to "true"
if ( conn != null ) ...<releaseConnection()>...;
}
코드가 왜 위처럼 됐는지는 저도 모르겠습니다. 단지 비즈니스가 그러했나보죠..
문제는 만약, (2)번 executeQuery()문장을 수행하면서 SQLException 이나 기타의
RuntimeException 이 발생할 때 입니다.
commit() 이나 rollback()을 타지 않고, finally 절로 건너 뛰어 Statement를
닫고, connection 은 반환됩니다. 이때, commit() 이나 rollback()이 되지 않은채
(1)번 UPDATE 문이 수행된채로 남아 있게 됩니다. 이는 DB LOCK을 점유하게
되고, 경우에 따라 다르겠지만, 다음번 요청시에 DB LOCK으로 인한 hang현상을
초래할 수도 있습니다.
일단 한두개의 어플리케이션이 어떠한 이유였든, DB Lock 을 발생시키면, 해당
DB에 관련된 어플리케이션들이 전부 응답이 없게 됩니다. 이 상황이 조금만
지속되면, 해당 waiting 을 유발하는 요청들이 어플리케이션서버의 최대 동시
접속처리수치에 도달하게 되고, 이는 전체 시스템의 hang현상으로 이어지게
되는 것이죠..
따라서, 비즈니스 로직이 어떠했든, 반드시 지켜져야할 사항은 다음과 같습니다.
"conn.setAutoComm(false); 상태에서 한번 실행된 Update성 SQL Query는 이유를
막론하고 어떠한 경우에도 commit() 이나 rollback() 되어야 한다."
위의 경우라면 다음처럼 catch 절에서 rollback 시켜주는 부분이 첨가되면 되겠네요.
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
conn.setAutoCommit(false);
stmt = conn.createStatement();
stmt.executeUpdate("UPDATE ....");
ResultSet rs = stmt.executeQuery("SELECT ename ...");
if ( rs.next() ) {
conn.commit();
}
else {
conn.rollback();
}
}
catch(Exception e){ // <---- !!!!!
if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
throw e;
}
finally {
if ( rs != null ) try{rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
// in some connection pool, you have to reset commit mode to "true"
if ( conn != null ) ...<releaseConnection()>...;
}
8.3 모든 경우의 수를 생각하라.
다음과 같은 경우를 생각해 보겠습니다.
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
conn.setAutoCommit(false);
stmt = conn.createStatement();
stmt.executeUpdate("UPDATE ....");
String idx = name.substring(3);
ResultSet rs = stmt.executeQuery("SELECT ename ... where idx=" + idx);
if ( rs.next() ) {
.....
}
rs.close(); rs = null;
stmt.executeUpdate("UPDATE ....");
conn.commit();
}
catch(SQLException e){
if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
throw e;
}
finally {
if ( rs != null ) try{rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
// in some connection pool, you have to reset commit mode to "true"
if ( conn != null ) ...<releaseConnection()>...;
}
잘 찾아 보세요. 어디가 잘못되었습니까? 잘 안보이시죠?
Connection conn = null;
Statement stmt = null;
try{
conn = ...<getConnection()>...;
conn.setAutoCommit(false);
stmt = conn.createStatement();
stmt.executeUpdate("UPDATE ...."); //---- (1)
String idx = name.substring(3); //<------ (2) NullPointerExceptoin ?
ResultSet rs = stmt.executeQuery("SELECT ename ... where idx=" + idx);
if ( rs.next() ) {
.....
}
rs.close(); rs = null;
stmt.executeUpdate("UPDATE ....");
conn.commit();
}
catch(SQLException e){ //<------ (3) 이 부분을 탈까?
if ( conn != null ) try{conn.rollback();}catch(Exception ee){}
throw e;
}
finally {
if ( rs != null ) try{rs.close();}catch(Exception e){}
if ( stmt != null ) try{stmt.close();}catch(Exception e){}
// in some connection pool, you have to reset commit mode to "true"
if ( conn != null ) ...<releaseConnection()>...;
}
위 코드를 보듯, 만약 (1)을 수행 후 (2)번 에서 NullPointerException 이나
ArrayIndexOutOfBoundException이라도 나면 어떻게 되죠? catch(SQLException ...)에는
걸리지 않고 곧바로 finally 절로 건너띄어 버리네요. 그럼 (1)에서 update 된 것은
commit()이나 rollback() 없이 connection 이 반환되네요... ;-)
어떻게 해야 합니까? SQLException만 잡아서 되는 것이 아니라, catch(Exception ...)과
같이 모든 Exception 을 catch해 주어야 합니다.
8.4 위 주석문에서도 언급해 두었지만, Hans Bergsteins 의 DBConnectionManager.java
와 같은 Connection Pool 을 사용할 경우에, 개발자의 코드에서 transaction auto
commit mode 를 명시적으로 "false"로 한 후, 이를 그냥 pool 에 반환하시면,
그 다음 사용자가 pool 에서 그 connection 을 사용할 경우, 여전히 transaction
mode 가 "false"가 된다는 것은 주지하셔야 합니다. 따라서, DBConnectionManger의
release method를 수정하시든지, 혹은 개발자가 명시적으로 초기화한 후 pool 에
반환하셔야 합니다. 그렇지 않을 경우, DB Lock 이 발생할 수 있습니다.
반면, IBM WebSphere 나 BEA WebLogic 과 같인 JDBC 2.0 스펙에 준하는 Connection
Pool을 사용할 경우는 반환할 당시의 transaction mode 가 무엇이었든 간에,
pool 에서 꺼내오는 connection 의 transaction mode는 항상 일정합니다.
(default 값은 엔진의 설정에 따라 달라집니다.)
그렇다고 commit 시키지 않은 실행된 쿼리가 자동으로 commit/rollback 되는 것은
아닙니다. 단지 auto commit 모드만 자동으로 초기화 될 뿐입니다.
PS:WAS의 JTS/JTA 트렌젝션 기반으로 운영될 경우는 명시적으로 commit/rollback되지
않은 트렌젝션은 자동으로 rollback되거나 commit됩니다. default는 WAS 설정에 따라
다를 수 있습니다.
---------------
NOTE: 자바서비스컨설팅의 WAS성능관리/모니터링 툴인 제니퍼(Jennifer 2.0)를 적용하면,
어플리케이션에서 명시적으로 commit/rollback시키지 않고 그대로 반환시킨 어플리케이션의
소스 위치를 실시간으로 감지하여 알려줍니다. 이를 만약 수작업으로 한다면, 수많은 코드
중 어디에서 DB lock을 유발 시키는 코드가 숨어있는지를 찾기가 경우에 따라 만만치 않은
경우가 많습니다.
8.5 XA JDBC Driver, J2EE JTS/JTA
JDBC 2.0, 표준 javax.sql.DataSource를 통한 JDBC Connection을 사용할 경우에,
대부분의 상용WAS제품들은 J2EE의 표준 JTS(Java Transaction Service)/JTA(Java Transaction
API)를 구현하고 있습니다. 특별히, 하나 이상의 데이타베이스에서 2 phase-commit과
같은 XA Protocol를 지원하고 있지요(사실 WAS에서 2PC가 지원되기 시작한 것은 몇년
되지 않습니다. 2PC를 사용하려면 반드시 XA-JDBC Driver가 WAS에 설치되어야 합니다)
샘플 예제는 다음과 같습니다.
...
javax.transaction.UserTransaction tx = null;
java.sql.Connection conn1 = null;
java.sql.Statement stmt1 = null;
java.sql.Connection conn2 = null;
java.sql.Statement stmt2 = null;
java.sql.CallableStatement cstmt2 = null;
try {
javax.naming.InitialContext ctx = new javax.naming.InitialContext();
tx = (javax.transaction.UserTransaction) ctx.lookup("java:comp/UserTransaction");
// 트렌젝션 시작
tx.begin();
// -------------------------------------------------------------------------
// A. UDB DB2 7.2 2PC(XA) Test
// -------------------------------------------------------------------------
javax.sql.DataSource ds1 =
(javax.sql.DataSource)ctx.lookup("java:comp/env/jdbc/DB2");
conn1 = ds1.getConnection();
stmt1 = conn1.createStatement();
stmt1.executeUpdate(
"insert into emp(empno,ename) values(" + empno + ",'Name" + empno + "')"
);
stmt1.executeUpdate(
"update emp set ename = 'LWY" + count + "' where empno = 7934"
);
java.sql.ResultSet rs1 = stmt1.executeQuery("select empno,ename from emp");
while(rs1.next()){
...
}
rs1.close();
// -------------------------------------------------------------------------
// B. Oracle 8.1.7 2PC(XA) Test
// -------------------------------------------------------------------------
javax.sql.DataSource ds2 =
(javax.sql.DataSource)ctx.lookup("java:comp/env/jdbc/ORA8i");
conn2 = ds2.getConnection();
stmt2 = conn2.createStatement();
stmt2.executeUpdate(
"update emp set ename = 'LWY" + count + "' where empno = 7934"
);
java.sql.ResultSet rs2 = stmt2.executeQuery("select empno,ename from emp");
while(rs2.next()){
...
}
rs2.close();
// -------------------------------------------------------------------------
// 트렌젝션 commit
tx.commit();
}
catch(Exception e){
// 트렌젝션 rollback
if ( tx != null ) try{tx.rollback();}catch(Exception ee){}
...
}
finally {
if ( stmt1 != null ) try { stmt1.close();}catch(Exception e){}
if ( conn1 != null ) try { conn1.close();}catch(Exception e){}
if ( stmt2 != null ) try { stmt2.close();}catch(Exception e){}
if ( conn2 != null ) try { conn2.close();}catch(Exception e){}
}
NOTE: 위에서 설명한 하나하나가 제 입장에서 보면 너무나 가슴깊이 다가오는
문제들입니다. 개발하시는 분의 입장에서 보면, 위의 가이드에 조금 어긋났다고
뭐그리 문제겠느냐고 반문하실 수 있지만, 수백본의 소스코드 중에 위와 같은 규칙을
준수하지 않은 코드가 단 하나라도 있다면, 잘 운영되던 시스템이 며칠에 한번씩
에러를 야기하거나 응답이 느려지고 급기야 hang 현상을 초래하는 결과를 가져 옵니다.
정말(!) 그렇습니다.
NOTE: 위에서 사용한 코딩 샘플들은 JDBC Connection Pooling 은 전혀 고려치 않고
설명드렸습니다. 그러했기 때문에 <getConnection()>, <releaseConnection()> 이란
Pseudo 코드로 설명했던 것입니다.
반드시 "서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-" 을 읽어 보세요.
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=968522077
-------------------------------------------------------
본 문서는 자유롭게 배포/복사 할 수 있으나 반드시
이 문서의 저자에 대한 언급을 삭제하시면 안됩니다
================================================
자바서비스넷 이원영
E-mail: javaservice@hanmail.net
PCS:010-6239-6498
================================================
1 | session객체 |
session객체는 세션에 관련된 정보를 핸들하는 객체입니다. 웹서버와 클라이언트간의 세션 데이터를 저장하고 있는 객체죠. 그런데 세션이라는 것이 왜 필요할까요? HTTP프로토콜은 Stateless을 기본으로 합니다. ftp는 지속적인 연결설정이 되어 있는 반면 http는 순간적으로 연결하고 연결설정을 바로 끊어 버립니다. 이것을 개선하기 위해서 나온것이 session이죠. 그래서 Stateless의 극복이라고도 하죠. |
2 | 클라이언트의 웹브라우져와 언제까지 세션이 유지될까? |
클라이언트에서 하나의 jsp파일에 접근하고 그리고 jsp에서 session설정이 true로 되어 있다면 세션이 끝나는 지점은 jsp에서 세션시간설정이 끝났을 때 세션은 마무리되고 그리고 웹브라우져를 닫았다가 다시 오픈하면 즉시 세션이 사라지게 된다. |
☞디폴트세션타임은 30분입니다. |
3 | 세션의 멤버 메서드를 알아볼까요 |
public String getID() 세션이 한번 열리면 고유한 ID를 갖게 됩니다. 이 아이디를 얻겠다는 것이죠 public long getCreationTime() 세션이 처음 생성된 시간을 밀리 초로 계산하여 long형 정수로 리턴합니다. 기준은 70년1월1일 00시 00분 00초입니다. public long getLastAccessedTime() 클라이언트 요청이 마지막으로 시도된 시간을 밀리초로 반환합니다. public int getMaxInactiveInterval() 클라이언트의 요구가 없을 때 서버가 현재의 세션을 언제까지 유지할지를 정수로 리턴합니다. 이때 기본 디폴트 세션마감시간은 30분으로 지정되어 있습니다. public void invalidate() 현재의 세션을 마감해 버립니다. 세션의 속성값들이 사라지는거죠 public boolean isNew() 서버측에서 새로운 session객체를 생성하고 아직 클라이언트에게 세션ID를 할당하지 않은 경우 true를 리턴하고 기존의 세션이 유지되고 있는 상태라면 false를 반환합니다. public void setMaxInactiveInterval(int seconds) 세션 시간을 설정합니다. 그리고 이 시간이 지나면 당연히 세션은 마감되겠죠(밀리세컨드단위입니다) |
key: 객체를 가리키는 이름
value: 실제 객체
session.setAttribute(key, value) => 저장
session.getAttribute(key) => 불러오기
String memberID1 = "ID1";
session.setAttribute("memID", memberID1);
(key: memID, value: String형 memberID1
ID1이란 값은 memberID1 객체 속에 담긴 정보라고 생각하세요.
로그인 세션의 경우
1. 입력받은 정보(ID, 패스워드)를 DB에서 찾고 유효한지 확인
2. ID, 패스워드와 함께 기타 유저 정보를 받아서 유저 정보 객체(ex:"Info) 저장
3. 그 객체를 다시 세션에 저장(ex: session.setAttribute("Info", Info);
4. 세션이 필요한 페이지는 세션에 저장된 정보를 받아서 알맞게 이용하면 됩니다.
(ex: Info info = (Info)session.getAttribute("Info"));
말씀하신 것으로 설명드리면
1. session.getAttribute("memID") memID라는 이름으로 저장된 객체를 불러오기.
2. session에 아무것도 저장이 안 되있으면 null값을 반환
3. 위와 같이 저장한 경우, String형 memberID1 객체를 불러오게 됩니다.
4. 불러온 객체는 Object형으로 변환되어있기 때문에 다시 형 변환을 시켜줘야 합니다.
ex) (String) <= 요 부분입니다.
EXAMPLE 1:
//세션 등록
session.setAttribute("userId", userId); //왼쪽파라미터는 이름을 명시, 두번째는 값을 저장.
session.setAttribute("passWd", passWd);
session.setAttribute("userName", userName);
session.setAttribute("mgrDiv", mgrDiv);
이는 세션에 적용된 페이지에서
session.getAttribute("userId"); 로 값을 가져옮
//세션 삭제
response.addHeader("Pragma", "No-cache");
response.addHeader("Cache-Control", "no-cache");
response.addDateHeader("Expires", 1);
session.invalidate();
세션 체크ex)
if(session != null && session.getAttribute("userId") != null && !session.getAttribute("userId").equals("")){
//session에 userId라는 값이 존재할 때
}else{
//userId를 사용하지 못할 때
}
EXAMPLE 2:
getCreationTime()을 사용하려다가 한번 만들어 봤습니다.
<%@ page contentType="text/html;" import="java.util.*,java.text.*" %>
<%
// javax.servlet.http Interface HttpSession
// session Creation / Last Access Time
long logonTime = session.getCreationTime(); //세션이 만들어진 시간
long lastAccessTime = session.getLastAccessedTime(); //최종접속시간
//날짜 타입으로 변환합니다.
SimpleDateFormat sFormat = new SimpleDateFormat("MM/dd/yyyy hh:mm:ss");
Date logonTimeF = new Date(logonTime);
Date lastAccessTimeF = new Date(lastAccessTime);
String logonTimeStr = sFormat.format(logonTimeF);
String lastAccessTimeStr = sFormat.format(lastAccessTimeF);
out.print("LOGON TIME : " + logonTimeStr + "<BR>");
out.print("LAST ACCESS TIME : " + lastAccessTimeStr + "<BR>");
out.print("SESSION MAX INACTIVE INTERVAL : " + (session.getMaxInactiveInterval() / 60) + " MIN <BR>");
//세션타임아웃 시간을 기본 30분에서 40분으로 늘립니다.
session.setMaxInactiveInterval(2400);
// session Attribute
String sName="";
Integer value=new Integer(1004);
session.setAttribute("bigjava", value);
Enumeration attEnum = session.getAttributeNames();
while(attEnum.hasMoreElements()){
sName=(String)attEnum.nextElement();
out.print( sName + " : " + session.getAttribute(sName) + "<BR>");
session.removeAttribute(sName);
}
// session ID
String mySessionID = session.getId();
out.print("SESSION ID : " + mySessionID + "<BR>");
%>
EXAMPLE 3:
Q:
[1번 페이지]
session생성 test
id [ ] 확인 <---text box에 값을 넣은 후 확인을 클릭 하면
--------------------------------------------------------------------
웹페이지가 뜨면서
[2번 페이지]
새로운 세션이 생성되었습니다.
세션ID : ALSJFLAJFLJASDLKJF
세션내용보기 <---링크
-------------------------------------------------------------
2페이지의 세션내용보기 클릭하면 웹페이지가 뜨면서
[3번 페이지]
Attribute id 값 :1234 <--1번페이지 텍스트박스에 넣었던값
isNew():false
세션id:
세션생성시간:
세센마지막접속시간
Session 삭제하기 <------링크
--------------------------------------------------------------------
3페이지의 세션 삭제하기 클릭하면 웹페이지 뜨면서
[4페이지]
로그인페이지로 <===클릭하면 1페이지
A:
---index.jsp---
[%@page language="java" contentType="text/html; charset=euc-kr"%]
[html]
[head]
[script]
function idSubmit(){
document.idForm.action = "test1.jsp";
document.idForm.submit();
}
[/script]
[/head]
[body]
[!-- Header --]
[table width="100%"]
[form name="idForm" method="post"]
[tr]
[td]ID : [input type="text" name="id" size="15"] [input type="button" value="확인" onClick="idSubmit()"][/td]
[/tr]
[/form]
[/table]
[/body]
[/html]
---test1.jsp---
[%@page language="java" contentType="text/html; charset=euc-kr"%]
[html]
[%
session = request.getSession(true);
String id = request.getParameter("id");
request.getSession().setAttribute("id", id);
%]
[body]
[h3]Session 생성 [/h3]
새로운 세션이 생성되었습니다.[BR]
세션ID : [%=session.getId()%][BR]
[a href="test2.jsp"]세션내용보기[BR]
[/body]
[/html]
---test2.jsp---
[%@page language="java" contentType="text/html; charset=euc-kr"%]
[html]
[body]
[h3]Session 정보 [/h3]
Attribute id 값 :[%=(String)request.getSession().getAttribute("id")%][br]
isNew():[%=session.isNew()%][br]
세션ID:[%=session.getId() %][br]
세션생성시간:[%=new java.util.Date(session.getCreationTime()).toString() %][br]
세션마지막접속시간:[%=new java.util.Date(session.getLastAccessedTime()).toString() %][br]
[a href="test3.jsp"]세션삭제하기[/a]
[/body]
[/html]
---test3.jsp---
[%@page language="java" contentType="text/html; charset=euc-kr"%]
[%
session = request.getSession(false);
if(session != null){
session.invalidate();
}
response.sendRedirect("index.jsp");
%]
session객체는 내장객체를 사용하였고 '['는 '<'로 바꾸어 사용하면 됩니다.
JSP 페이지를 실행할 때마다 커넥션을 생성해서 사용하게 되면 커넥션을 생성하고 닫는데 시간이 소모되기 때문에 동시 접속자 수가 많은 웹 사이트의 경우 전체 성능을 떨어뜨리는 원인이 된다. 성능문제를 해결하기 위해 사용하는 일반적인 방식으로 커넥션 풀 기법이 있는데, 이 글에서는 자카르타 프로젝트의 DBCP API를 이용하여 커넥션 풀을 사용하는 방법에 대해 살펴보도록 하겠다.
1. 커넥션 풀이란?
-- 커넥션 풀 기법이란, 데이터베이스와 연결된 커넥션을 미리 만들어서 풀(pool) 속에 저장해 두고 있다가 필요할 때에 커넥션을 풀에서 가져다 쓰고, 다시 풀에 반환하는 기법을 말한다.
[그림 1] 커넥션 풀 기법
풀 속에 데이터베이스와 연결된 커넥션을 미리 생성해 놓고 있다가, 커넥션이 필요할 경우 풀 속에 미리 생성되어 있는 커넥션을 가져다가 사용하고 다 사용한 커넥션은 다시 풀에 반환한다. 풀에 반환된 커넥션은 다음에 다시 사용된다.
◆ 커넥션 풀의 특징
-- 풀 속에 미리 커넥션이 생성되어 있기 때문에 커넥션을 생성하는데 드는 연결 시간이 소비되지 않는다.
-- 커넥션을 계속해서 재사용하기 때문에 생성되는 커넥션 수가 많지 않다.
커넥션을 생성하고 닫는데 필요한 시간이 소모되지 않기 때문에 그 만큼 어플리케이션의 실행 속도가 빨라지며, 또한 한번에 생성될 수 있는 커넥션 수를 제어하기 때문에 동시 접속자 수가 몰려도 웹 어플리케이션이 쉽게 다운되지 않는다. 커넥션 풀을 사용하면 전체적인 웹 어플리케이션의 성능 및 처리량이 높아지기 때문에 많은 웹 어플리케이션에서 커넥션 풀을 기본으로 사용하고 있다.
2. DBCP API의 사용 방법
자카르타 프로젝트의 DBCP API를 사용할 때에는 다음과 같은 과정을 거치면 된다.
① DBCP 관련 Jar 파일 및 JDBC 드라이버 Jar 파일 설치하기
② 커넥션 풀 관련 설정 파일 초기화하기
③ 커넥션 풀 관련 드라이버 로딩하기
④ 커넥션 풀로부터 커넥션 사용하기
이 네 가지 절차에 대해서 차례대로 살펴보도록 하자.
2.1 필요한 Jar 파일 복사
DBCP API를 사용하기 위해서는 다음과 같은 라이브러리가 필요하다.
- DBCP API 관련 Jar 파일 DBCP 1.2.2 Download
- DBCP API가 사용하는 자카르타 Pool API의 Jar 파일 Pool 1.3 Download
- Pool API가 사용하는 자카르타 Collection API의 Jar 파일 Collections 3.2 Download
이들 라이브러리의 최신 버전은 http://jakarta.apache.org/site/binindex.cgi 에서 다운로드 받을 수 있다.
파일의 압축을 풀면 다음과 같은 Jar 파일들을 발견할 수 있는데, 이들 Jar 파일들을 사용하면 된다.
- common-dbcp-1.2.2.jar
- common-pool-1.3.jar
- common-collections-3.2.jar
이 파일들을 WEB-INFlib 폴더에 복사해 넣는다.
2.2 설정 파일 작성하기
DBCP를 사용하는 방법에는 소스 코드 상에서 커넥션 풀을 설정하는 방법과 설정 파일을 통해서 커넥션 풀을 설정하는 방법 두 가지 존재하는데 여기서는 설정 파일을 이용한 커넥션 풀 설정 방법에 대해서 살펴보도록 하겠다.
DBCP Pool API에서 사용되는 커넥션 풀 설정 파일의 기본 골격은 다음과 같다.
[참고 1] WEB-INFclassespool1.jocl
---------------------------------------------------------------------------------------------------------------------------
01 <object class = "org.apache.common.dbcp.Poolable ConnectionFactory"
02 xmlns = "http://apache.org/xml/xmlsn/jakarta/commons/jocl">
03
04 // DBMS와 연결할 때 사용할 JDBC URL 및 사용자 계정, 암호
05 <object class = "org.apache.commons.dbcp.DriverManagerConnectionFactory">
06 <string value = "jdbc:mysql://localhost:3306/myRoot?.." />
07 <string value = "jspexam" />
08 <string value = "jspex" />
09 </object>
10
11 // 커넥션 풀과 관련된 추가 설정 정보 지정
12 <object class = "org.apache.commons.pool.implGenericObjectPool">
13 <object class = "org.apache.commons.pool.PoolableObjectFactory" null="true" />
14 </object>
15
16 <object class = "org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory" null="true" />
17
18 // 커넥션이 유효한지의 여부를 검사할 때 사용할 쿼리. 쿼리를 지정하고 싶은 경우에는
19 // <string value = "SELECT count(*) FROM member" /> 와 같은 코드를 입력해 주면 된다.
20 <string null="true" />
21
22 // 커넥션을 읽기 전용으로 생성할지의 여부를 지정한다.
23 // insert, update, delete 작업이 있다면 false로 지정해 주어야 한다.
24 <boolean value="false" />
25
26 // 커넥션을 자동 커밋 모드로 설정할 경우 true를, 그렇지 않을 경우 false를 지정한다. 일반적으로 true를 사용한다.
27 <boolean value="true" />
28 </object>
---------------------------------------------------------------------------------------------------------------------------
[참고1] 설정 파일에서 나머지 부분은 그대로 입력하고 라인 05~09 부분만 알맞게 변경하면 된다.
05 <object class = "org.apache.commons.dbcp.DriverManagerConnectionFactory">
06 <string value = "jdbc:mysql://localhost:3306/myRoot?.." />
07 <string value = "jspexam" />
08 <string value = "jspex" />
09 </object>
위 코드에는 세 개의 <string> 태그가 사용되는데, 이들 태그는 각각 순서대로 JDBC URF, 데이터베이스 사용자 계정, 암호를 나타낸다. JDBC URL 부분의 값이 길어서 생략해서 표시했는데 다음과 같이 사용할 수 있다.
jdbc:mysql://localhost:3306/myRoot?useUnicode=true&characterEncoding=euc-kr
*** NOTE
DBCP 커넥션 풀 설정 파일은 XML 문서로 처리된다. XML 문서에서는 엠퍼샌드 기호('&')를 표시할 때 &를 사용해야 하기 때문에 JDBC URL 입력 부분에 useUnicode=true&characterEncoding 대신 useUnicode=true&char...를 사용하였다.
하나의 설정 파일은 하나의 커넥션 풀에 해당한다. 새로운 커넥션 풀을 생성하고 싶다면 [참고 1]과 같은 파일을 하나 새롭게 생성하면 된다.
2.3 설정 파일의 위치
DBCP API는 클래스 패스로부터 설정 파일을 읽어 온다. 따라서 앞서 작성한 커넥션 풀 설정 파일은 클래스 패스에 위치해 있어야 한다. 웹 어플리케이션에서 DBCP API와 관련된 설정 파일의 위치로 가장 좋은 곳은 WEB-INFclasses 폴더이다.
2.4 커넥션 풀 초기화하기
DBCP API를 통해서 커넥션 풀을 사용하기 위해서는 커넥션 풀과 관련된 JDBC 드라이버를 로딩해 주어야 한다. DBCP API를 사용할 때에 로딩해 주어야 할 JDBC 드라이버는 다음과 같다.
- org.apache.commons.dbcp.PoolingDriver -- DBCP API의 JDBC 드라이버
- DBMS에 연결될 때 사용될 JDBC 드라이버
DBCP API를 사용할 때에는 웹 어플리케이션 시작할 때 위에서 언급한 두 가지 형태의 JDBC 드라이버를 로딩하도록 하면 편리하다.
DBCP API와 관련된 JDBC 드라이버를 로딩하는 코드는 다음 [참고 2]와 같다.
[참고 2] WEB-INFjdbcdriverDBCPInit.java
---------------------------------------------------------------------------------------------------------------------------
01 package jdbcdriver;
02
03 import javax.servlet.http.HttpServlet;
04 import javax.servlet.ServletConfig;
05 import javax.servlet.ServletException;
06 import java.util.StringTockenizer;
07
08 public class DBCPInit extends HttpServlet {
09
10 public void init (ServletConfig config) throws ServletException {
11 try {
12 // DBMS와 연결할 때 사용될 JDBC 드라이버 로딩
13 String drivers = config.getInitParameter("jdbcdriver");
14 StringTockenizer st = new StringTokenizer(drivers, ",");
15 while (st.hasMoreTokens()) {
16 String jdbcDriver = st.nextToken();
17 Class.forName(jdbcDriver);
18 }
19
20 // DBCP API에서 풀 기능을 제공하기 위해 사용되는 PoolingDriver 로딩
21 Class.forName("org.apache.commons.dbcp.PoolingDriver");
22
23 // 톰캣 5.0.19에서는 주석을 그대로 두어도 올바르게 실행되지만 톰캣 5.0.2x 버전부터는
24 // 주석을 없애고 SAX 파서를 지정해 주어야 올바르게 실행된다.
25 // System.setProperty("org.xml.sax.driver", "org.apache.crimson.parser.XMLReaderImpl");
26 } catch (Exception ex) {
27 throw new ServletException(ex);
28 }
29 }
30 }
---------------------------------------------------------------------------------------------------------------------------
[컴파일]
C:>cd jakarta-tomcat-5.0.19webappsmyRootWEB-INF
C:..WEB-INF> set PATH=c:j2sdk1.4.2_04bin;%PATH%
C:..WEB-INF> set CLASSPATH=c:jakarta-tomcat-5.0.19commonlibservlet-api.jar;c:jakarta-tomcat-5.0.19commonlibjsp-api.jar;classes
C:..WEB-INF> javac -d classes jdbcdriverDBCPInit.java
컴파일에 성공하면 WEB-INFclassesjdbcdriver 폴더에 DBCPInit.class 파일이 생성될 것이다.
WEB-INFweb.xml 파일에 DBCPInit 서블릿 클래스에 대한 설정 정보를 추가함으로써 웹 어플리케이션이 시작될 때 DBCPInit 서블릿 클래스가 시작될 수 있도록 할 수 있다. 예를 들면, 아래와 같은 코드를 web.xml 파일에 추가해 주면 된다.
---------------------------------------------------------------------------------------------------------------------------
<servlet>
<servlet-name>DBCPInit</servlet-name>
<servlet-class>jdbcdriver.DBCPInit</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>jdbcdriver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
<init-param>
</servlet>
---------------------------------------------------------------------------------------------------------------------------
위와 같이 코드를 추가해 주면 웹 어플리케이션이 시작할 때 DBCPInit 서블릿 클래스가 자동으로 시작되고 init() 메소드가 호출된다.
2.5 커넥션 사용하기
커넥션 풀을 위한 JDBC 드라이버 및 DBMS에 연결할 때 사용할 JDBC 드라이버를 로딩하면 커넥션 풀로부터 커넥션을 가져와 사용할 수 있다. 커넥션 풀로부터 커넥션을 가져오는 코드는 별반 다르지 않으며, 다음과 같은 형태의 코드를 사용하면 된다.
JSP 페이지를 실행할 때마다 커넥션을 생성해서 사용하게 되면 커넥션을 생성하고 닫는데 시간이 소모되기 때문에 동시 접속자 수가 많은 웹 사이트의 경우 전체 성능을 떨어뜨리는 원인이 된다. 성능문제를 해결하기 위해 사용하는 일반적인 방식으로 커넥션 풀 기법이 있는데, 이 글에서는 자카르타 프로젝트의 DBCP API를 이용하여 커넥션 풀을 사용하는 방법에 대해 살펴보도록 하겠다.
1. 커넥션 풀이란?
-- 커넥션 풀 기법이란, 데이터베이스와 연결된 커넥션을 미리 만들어서 풀(pool) 속에 저장해 두고 있다가 필요할 때에 커넥션을 풀에서 가져다 쓰고, 다시 풀에 반환하는 기법을 말한다.
[그림 1] 커넥션 풀 기법
풀 속에 데이터베이스와 연결된 커넥션을 미리 생성해 놓고 있다가, 커넥션이 필요할 경우 풀 속에 미리 생성되어 있는 커넥션을 가져다가 사용하고 다 사용한 커넥션은 다시 풀에 반환한다. 풀에 반환된 커넥션은 다음에 다시 사용된다.
◆ 커넥션 풀의 특징
-- 풀 속에 미리 커넥션이 생성되어 있기 때문에 커넥션을 생성하는데 드는 연결 시간이 소비되지 않는다.
-- 커넥션을 계속해서 재사용하기 때문에 생성되는 커넥션 수가 많지 않다.
커넥션을 생성하고 닫는데 필요한 시간이 소모되지 않기 때문에 그 만큼 어플리케이션의 실행 속도가 빨라지며, 또한 한번에 생성될 수 있는 커넥션 수를 제어하기 때문에 동시 접속자 수가 몰려도 웹 어플리케이션이 쉽게 다운되지 않는다. 커넥션 풀을 사용하면 전체적인 웹 어플리케이션의 성능 및 처리량이 높아지기 때문에 많은 웹 어플리케이션에서 커넥션 풀을 기본으로 사용하고 있다.
2. DBCP API의 사용 방법
자카르타 프로젝트의 DBCP API를 사용할 때에는 다음과 같은 과정을 거치면 된다.
① DBCP 관련 Jar 파일 및 JDBC 드라이버 Jar 파일 설치하기
② 커넥션 풀 관련 설정 파일 초기화하기
③ 커넥션 풀 관련 드라이버 로딩하기
④ 커넥션 풀로부터 커넥션 사용하기
이 네 가지 절차에 대해서 차례대로 살펴보도록 하자.
2.1 필요한 Jar 파일 복사
DBCP API를 사용하기 위해서는 다음과 같은 라이브러리가 필요하다.
- DBCP API 관련 Jar 파일 DBCP 1.2.2 Download
- DBCP API가 사용하는 자카르타 Pool API의 Jar 파일 Pool 1.3 Download
- Pool API가 사용하는 자카르타 Collection API의 Jar 파일 Collections 3.2 Download
이들 라이브러리의 최신 버전은 http://jakarta.apache.org/site/binindex.cgi 에서 다운로드 받을 수 있다.
파일의 압축을 풀면 다음과 같은 Jar 파일들을 발견할 수 있는데, 이들 Jar 파일들을 사용하면 된다.
- common-dbcp-1.2.2.jar
- common-pool-1.3.jar
- common-collections-3.2.jar
이 파일들을 WEB-INFlib 폴더에 복사해 넣는다.
2.2 설정 파일 작성하기
DBCP를 사용하는 방법에는 소스 코드 상에서 커넥션 풀을 설정하는 방법과 설정 파일을 통해서 커넥션 풀을 설정하는 방법 두 가지 존재하는데 여기서는 설정 파일을 이용한 커넥션 풀 설정 방법에 대해서 살펴보도록 하겠다.
DBCP Pool API에서 사용되는 커넥션 풀 설정 파일의 기본 골격은 다음과 같다.
[참고 1] WEB-INFclassespool1.jocl
---------------------------------------------------------------------------------------------------------------------------
01 <object class = "org.apache.common.dbcp.Poolable ConnectionFactory"
02 xmlns = "http://apache.org/xml/xmlsn/jakarta/commons/jocl">
03
04 // DBMS와 연결할 때 사용할 JDBC URL 및 사용자 계정, 암호
05 <object class = "org.apache.commons.dbcp.DriverManagerConnectionFactory">
06 <string value = "jdbc:mysql://localhost:3306/myRoot?.." />
07 <string value = "jspexam" />
08 <string value = "jspex" />
09 </object>
10
11 // 커넥션 풀과 관련된 추가 설정 정보 지정
12 <object class = "org.apache.commons.pool.implGenericObjectPool">
13 <object class = "org.apache.commons.pool.PoolableObjectFactory" null="true" />
14 </object>
15
16 <object class = "org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory" null="true" />
17
18 // 커넥션이 유효한지의 여부를 검사할 때 사용할 쿼리. 쿼리를 지정하고 싶은 경우에는
19 // <string value = "SELECT count(*) FROM member" /> 와 같은 코드를 입력해 주면 된다.
20 <string null="true" />
21
22 // 커넥션을 읽기 전용으로 생성할지의 여부를 지정한다.
23 // insert, update, delete 작업이 있다면 false로 지정해 주어야 한다.
24 <boolean value="false" />
25
26 // 커넥션을 자동 커밋 모드로 설정할 경우 true를, 그렇지 않을 경우 false를 지정한다. 일반적으로 true를 사용한다.
27 <boolean value="true" />
28 </object>
---------------------------------------------------------------------------------------------------------------------------
[참고1] 설정 파일에서 나머지 부분은 그대로 입력하고 라인 05~09 부분만 알맞게 변경하면 된다.
05 <object class = "org.apache.commons.dbcp.DriverManagerConnectionFactory">
06 <string value = "jdbc:mysql://localhost:3306/myRoot?.." />
07 <string value = "jspexam" />
08 <string value = "jspex" />
09 </object>
위 코드에는 세 개의 <string> 태그가 사용되는데, 이들 태그는 각각 순서대로 JDBC URF, 데이터베이스 사용자 계정, 암호를 나타낸다. JDBC URL 부분의 값이 길어서 생략해서 표시했는데 다음과 같이 사용할 수 있다.
jdbc:mysql://localhost:3306/myRoot?useUnicode=true&characterEncoding=euc-kr
*** NOTE
DBCP 커넥션 풀 설정 파일은 XML 문서로 처리된다. XML 문서에서는 엠퍼샌드 기호('&')를 표시할 때 &를 사용해야 하기 때문에 JDBC URL 입력 부분에 useUnicode=true&characterEncoding 대신 useUnicode=true&char...를 사용하였다.
하나의 설정 파일은 하나의 커넥션 풀에 해당한다. 새로운 커넥션 풀을 생성하고 싶다면 [참고 1]과 같은 파일을 하나 새롭게 생성하면 된다.
2.3 설정 파일의 위치
DBCP API는 클래스 패스로부터 설정 파일을 읽어 온다. 따라서 앞서 작성한 커넥션 풀 설정 파일은 클래스 패스에 위치해 있어야 한다. 웹 어플리케이션에서 DBCP API와 관련된 설정 파일의 위치로 가장 좋은 곳은 WEB-INFclasses 폴더이다.
2.4 커넥션 풀 초기화하기
DBCP API를 통해서 커넥션 풀을 사용하기 위해서는 커넥션 풀과 관련된 JDBC 드라이버를 로딩해 주어야 한다. DBCP API를 사용할 때에 로딩해 주어야 할 JDBC 드라이버는 다음과 같다.
- org.apache.commons.dbcp.PoolingDriver -- DBCP API의 JDBC 드라이버
- DBMS에 연결될 때 사용될 JDBC 드라이버
DBCP API를 사용할 때에는 웹 어플리케이션 시작할 때 위에서 언급한 두 가지 형태의 JDBC 드라이버를 로딩하도록 하면 편리하다.
DBCP API와 관련된 JDBC 드라이버를 로딩하는 코드는 다음 [참고 2]와 같다.
[참고 2] WEB-INFjdbcdriverDBCPInit.java
---------------------------------------------------------------------------------------------------------------------------
01 package jdbcdriver;
02
03 import javax.servlet.http.HttpServlet;
04 import javax.servlet.ServletConfig;
05 import javax.servlet.ServletException;
06 import java.util.StringTockenizer;
07
08 public class DBCPInit extends HttpServlet {
09
10 public void init (ServletConfig config) throws ServletException {
11 try {
12 // DBMS와 연결할 때 사용될 JDBC 드라이버 로딩
13 String drivers = config.getInitParameter("jdbcdriver");
14 StringTockenizer st = new StringTokenizer(drivers, ",");
15 while (st.hasMoreTokens()) {
16 String jdbcDriver = st.nextToken();
17 Class.forName(jdbcDriver);
18 }
19
20 // DBCP API에서 풀 기능을 제공하기 위해 사용되는 PoolingDriver 로딩
21 Class.forName("org.apache.commons.dbcp.PoolingDriver");
22
23 // 톰캣 5.0.19에서는 주석을 그대로 두어도 올바르게 실행되지만 톰캣 5.0.2x 버전부터는
24 // 주석을 없애고 SAX 파서를 지정해 주어야 올바르게 실행된다.
25 // System.setProperty("org.xml.sax.driver", "org.apache.crimson.parser.XMLReaderImpl");
26 } catch (Exception ex) {
27 throw new ServletException(ex);
28 }
29 }
30 }
---------------------------------------------------------------------------------------------------------------------------
[컴파일]
C:>cd jakarta-tomcat-5.0.19webappsmyRootWEB-INF
C:..WEB-INF> set PATH=c:j2sdk1.4.2_04bin;%PATH%
C:..WEB-INF> set CLASSPATH=c:jakarta-tomcat-5.0.19commonlibservlet-api.jar;c:jakarta-tomcat-5.0.19commonlibjsp-api.jar;classes
C:..WEB-INF> javac -d classes jdbcdriverDBCPInit.java
컴파일에 성공하면 WEB-INFclassesjdbcdriver 폴더에 DBCPInit.class 파일이 생성될 것이다.
WEB-INFweb.xml 파일에 DBCPInit 서블릿 클래스에 대한 설정 정보를 추가함으로써 웹 어플리케이션이 시작될 때 DBCPInit 서블릿 클래스가 시작될 수 있도록 할 수 있다. 예를 들면, 아래와 같은 코드를 web.xml 파일에 추가해 주면 된다.
---------------------------------------------------------------------------------------------------------------------------
<servlet>
<servlet-name>DBCPInit</servlet-name>
<servlet-class>jdbcdriver.DBCPInit</servlet-class>
<load-on-startup>1</load-on-startup>
<init-param>
<param-name>jdbcdriver</param-name>
<param-value>com.mysql.jdbc.Driver</param-value>
<init-param>
</servlet>
---------------------------------------------------------------------------------------------------------------------------
위와 같이 코드를 추가해 주면 웹 어플리케이션이 시작할 때 DBCPInit 서블릿 클래스가 자동으로 시작되고 init() 메소드가 호출된다.
2.5 커넥션 사용하기
커넥션 풀을 위한 JDBC 드라이버 및 DBMS에 연결할 때 사용할 JDBC 드라이버를 로딩하면 커넥션 풀로부터 커넥션을 가져와 사용할 수 있다. 커넥션 풀로부터 커넥션을 가져오는 코드는 별반 다르지 않으며, 다음과 같은 형태의 코드를 사용하면 된다.
젠장..
1. 먼저 library를 다운받는다.. 다운은 sun 홈페이지에서 손쉽게 찾을수 있다.
(http://java.sun.com/developer/technicalArticles/javaserverpages/rss_utilities/rss_utils_1.1.zip)
2. 이곳에 있는 jar파일은 WebContent/WEB-INF/lib 폴더에 복사해넣고 tld파일은 WebContent/WEB-INF 폴더에 복사해 넣는다.
3. WEB-INF/web.xml에
<taglib>
<taglib-uri>/WEB-INF/rssutils.tld</taglib-uri>
<taglib-location>/WEB-INF/rssutils.tld</taglib-location>
</taglib>
를 넣어준다.
4. 그리고 jsp파일에는 맨위에 <%@ taglib uri="/WEB-INF/rssutils.tld" prefix="rss" %>를 넣어주면 끝...
sample )
<%@ taglib uri="/WEB-INF/rssutils.tld" prefix="rss" %>
<rss:feed
url="http://static.userland.com/gems/backend/rssTwoExample2.xml"
feedId="example3"/>
<b>Image: </b><rss:channelImage feedId="example3"/><br>
<b>Title: </b><rss:channelTitle feedId="example3"/><br>
<b>Link: </b><rss:channelLink feedId="example3" asLink="true"/><br>
<b>Description: </b><rss:channelDescription feedId="example3"/><br>
<b>Copyright: </b><rss:channelCopyright feedId="example3"/><br>
<b>Docs: </b><rss:channelDocs feedId="example3"/><br>
<b>Generator: </b><rss:channelGenerator feedId="example3"/><br>
<b>Language: </b><rss:channelLanguage feedId="example3"/><br>
<b>Last Build Date: </b><rss:channelLastBuildDate
feedId="example3"/><br>
<b>Managing Editor: </b><rss:channelManagingEditor
feedId="example3"/><br>
<b>Pub Date: </b><rss:channelPubDate feedId="example3"/><br>
<b>Skip Days: </b><rss:channelSkipDays feedId="example3"/><br>
<b>Skip Hours: </b><rss:channelSkipHours feedId="example3"/><br>
<b>TTL: </b><rss:channelTTL feedId="example3"/><br>
<ul>
<rss:forEachItem feedId="example3" startIndex="2" endIndex="4">
<li><rss:itemDescription feedId="example3"/><br><br></li>
</rss:forEachItem>
</ul>
JSP 웹서버 셋팅 정리 문서
Ver.2007.09.11 (deprecated)
- 작성자: 화랑천(
- 이메일: gsun2@nate.com (nate)
- 구축환경: Apache2.2 + Tomcat6.0 + JK1.2 + MySql5 on Linux
- 차례
1. Apache 2.2 Install
2. Tomcat 6.0 Install
3. JK 1.2 Install
4. Apache Configuration
5. Tomcat Configuration
6. MySql 5 Install and Configuration
7. Environment variable Configuration And TIP
8. PHP5 And ZendOptimizer Install
- 참고사항
l Linux 는 SULinux(Redhat EL4) 버전에서 테스트 되었다.
l 이 문서는 최신(작성일기준) 버전들의 연동 셋팅방법을 자세히 설명한다.
l 기초적인 것부터 자세히 기술하여 초보자에게 확실한 도움을 주고자 한다.
l 이 단계를 벗어난 로드밸런싱, 클러스터링등은 다루지 않는다
l 다운로드 받거나 보관될 소스 원본 위치: /usr/local/src
l 원칙적으로 RPM 버전은 사용하지 않는다.
l 다운로드 주소는 마우스 오른 클릭 팝메뉴의 [바로가기 복사]로 얻을 수 있다.
l 관리의 편의성을 위하여 프로그램 설치 시 디렉토리명에 버전까지 명시한다.
Ex) apache1.3 apache2.0 둘중 실제 사용하는 것을 apache로 심볼릭링크
1. Apache 2.2 Install
Apache 란? – 정적 파일의 웹 서비스를 담당하는 웹서버
Tomcat 자체적으로도 정적 파일 웹 서비스 기능이 있으나 속도가 느리기 때문에 정적 파일만을 전문으로 서비스하는 Apache 와 연동하여 역할을 분담하는 방식으로 주로 사용한다.
문서 작성 기준 버전: 2.2.6 (lastUpdate 2007.09.07)
"새 버전이 아파치 중 최선이라고 생각하며, 모든 사용자가 업그레이드하기를 권장한다. 아파치 2.2는 2.0 코드베이스와 비교할 때 수많은 개선점과 성능 향상을 이뤘다" // 아파치 재단 성명서
l Apache2 Reference Doc: http://httpd.apache.org/docs-2.2/
l Apache2 Download from httpd.apache.org (wget)
l ]# tar xzvf httpd-2.2.6.tar.gz
l ]# cd httpd-2.2.6
l ]# ./configure --prefix=/usr/local/apache-2.2.6 --enable-so --with-mpm=worker
l ]# make
l ]# make install
l ]# cd /usr/local/
l ]# ln -s apache-2.2.6 apache
l ]# ln -s /usr/local/apache/htdocs /home/webRoot
l ]# ln -s /usr/local/apache/logs /logs
인터넷에서 접할 수 있는 여러 apache 설치 문서에서 -–bindir, --sbindir, --enable-layout 등을 사용하여 리눅스 공용 bin, sbin dir로 옮기는 경우가 있는데, 관리상 불편을 초래하므로 절대 권장하지 않는다. --prefix 설치경로에 몰아 넣는 디폴트 설정에 따르도록 하자.
--enable-so 옵션은 동적공유객체(DSO) 사용 옵션으로 설치 당시의 공유객체들을 정적으로 사용하고 나중에 추가될 공유객체들의 httpd.conf 에서 추가할 수 있게 한다. (httpd –l 로 확인)
--with-mpm=worker 옵션은 다중처리모듈을 worker 로 설정한다. worker 방식은 쓰레드를 사용하여 많은 사용자수에 유연하게 대응할 수 있는 높은 확장성(scalability)이 특징이다. 리눅스에서 설치시 기본값은 prepork 인데, 쓰레드를 사용하지 않으며 MaxClient 256 제한이라는 단점이 있다. worker 옵션을 적용하면 더 적은 메모리를 사용하여 더 빠른 속도를 낼 수 있다.
$APACHE_HOME/htdocs 디렉토리를 /home/webRoot 로 심볼릭링크 한 이유는 이후 가상호스트로 여러 사이트를 운영시 모든 각 사이트들의 DocumentRoot를 /home/SITE_ID 처럼 만들어서 관리의 편의성을 통일하고 권한설정의 편의를 위해서 설정한다.
l 기타 유용한 Apache configure option
Apache 의 configure 설정은 언제든지 재컴파일을 쉽게 할수 있다.(httpd.conf설정유지됨)
]# ./configure --prefix=/usr/local/apache-2.2.4 --enable-so --with-mpm=worker --enable-rewrite --enable-headers
--enable-rewrite 는 정규표현식을 사용하여 URI를 포워딩 해주는 유용한 모듈로써 파라미터를 숨겨서 깔끔한 URI 처리가 가능하다.
사용되는 각 사이트의 DocumentRoot(./htaccess 있는 Dir) 설정
<Directory "/home/DOCUMENT_ROOT">
AllowOverride all
</Directory>
ex) http://domain.com/USER_ID/1234 -> http://domain.com/index.jsp?id=$1&no=$2
]# vi DOCUMENT_ROOT/.htaccess
RewriteEngine On
RewriteRule ^([a-zA-Z0-9_]+)/([[:digit:]]+)$ ./index.jsp?id=$1&no=$2
--enable-headers 는 IE6 버전에서 각각의 프레임에서 쿠키를 구울 수 없는것을 헤더를 직접 컨트롤함으로써 사용가능하게 하는 목적으로 쓰인다.
ex) Header set P3P "CP=\"ALL CURa ADMa DEVa TAIa OUR BUS IND PHY ONL UNI PUR FIN COM NAV INT DEM CNT STA POL HEA PRE LOC OTC\""
--enable-cache 는 튜닝관련하여 민감한 부분으로 확실한 정보가 부족하기 때문에 Reference Doc 링크를 참조하여 사용할 수 있다면 큰 성능향상을 기대할 수 있다.
http://httpd.apache.org/docs/2.2/mod/mod_cache.html
2. Tomcat 6.0 Install
Tomcat 이란? – 가장 유명한 JSP, Servlet Container
문서 작성 기준 버전: 6.0.14 (lastUpdate 2007.08.09)
Tomcat 을 설치하기 전에 반드시 먼저 JDK 가 설치되어 있어야 한다.
l JDK Download from java.sun.com (wget) *Linux self-extracting version (not RPM)
l wget으로 그냥 받을 경우 파일명이 너무 길기 때문에 -O 옵션을 써준다.
l ]# wget http://192.18.108.208/~~~/jdk-6u2-linux-i586.bin -O jdk-6u2-linux-i586.bin
l ]# chmod +x jdk-6u2-linux-i586.bin
l ]# ./ jdk-6u2-linux-i586.bin
l q (--more-- ,skip)
l yes (agree)
l ]# mv jdk1.6.0_02 /usr/local/
l ]# cd /usr/local/
l ]# ln –s jdk1.6.0_02 jdk
Tomcat 설치는 압축을 푸는 것 만으로 완료된다.
l Tomcat5.5 Reference Doc: http://jakarta.apache.org/tomcat/tomcat-6.0-doc/
l Tomcat5.5 Download from tomcat.apache.org (wget)
l ]# tar xzvf apache-tomcat-6.0.14.tar.gz –C /usr/local/
l ]# cd /usr/local
l ]# mv apache-tomcat-6.0.14 tomcat-6.0.14
l ]# ln -s apache-tomcat-6.0.14 tomcat
l ]# ln -s /usr/local/tomcat/logs/catalina.out /logs/catalina.out
3. JK 1.2 Install
JK Connector 란? - Apache + Tomcat 연동 프로그램
[참고1] JK2 Connector 는 2004.11. 이후 개발 및 지원 중단되어 더이상 사용되지 않으므로 JK2 가 최신버전인 것으로 오해하지 않도록 주의. (deprecated)
[참고2] JK1.2.21 버전과 JK1.2.23 버전에서 중요한 보안 업데이트가 이루어 졌으므로 반드시 1.2.24이상의 최신버전을 사용해야 한다. 최신버전을 사용하고 있지 않다면 반드시 업데이트 해야한다.
문서 작성기준 버전: 1.2.25 (lastUpdate 2007.08.07)
JK 설치에는 두 가지 방법이 있는데, 하나는 일반적인 컴파일로 jk_mod.so 생성 후 아파치 모듈 디렉토리에 복사하고 설정파일에도 추가하는 동적모듈방식이고, 다른 하나는 아파치를 설치하기 이전에 아파치 소스에 포함하여 아파치 설치 때 자동으로 설치되는 정적모듈방식이다. 이중 두번째 방식이 성능이 좋으나 Apache2.2 에서 아직 지원되지 않고 있기 때문에 동적모듈방식만 설명한다.
l JK Connector Reference Doc: http://tomcat.apache.org/connectors-doc/
l JK Connector Download from http://tomcat.apache.org/connectors-doc/ (wget)
l ]# tar xzvf tomcat-connectors-1.2.25-src.tar.gz
l ]# cd tomcat-connectors-1.2.25-src/native
l ]# ./configure --with-apxs=/usr/local/apache/bin/apxs
l ]# make
l ]# cp ./apache-2.0/mod_jk.so /usr/local/apache/modules/
4. Apache Configuration
l ]# vi /usr/local/apache/conf/httpd.conf
l 접근권한 디렉토리 변경 (웹서비스 디렉토리 : /home) (line:128)
<Directory “/home”>
l 웹에서 디렉토리 파일리스트 정보 출력막기 (Indexes 삭제) (line:141)
Option Indexes FollowSymLinks
l 디렉토리 인덱스파일 리스트에 index.jsp 추가 (line:163)
DirectoryIndex index.html index.jsp
l 에러로그 파일 설정 - 10메가 초과시 새로운 로그파일 생성 (cronolog 대체추천) (line:183)
ErrorLog "|/usr/local/apache/bin/rotatelogs /logs/errorlog.%y%m 10M"
l 기본 엑세스로그 주석처리 (가상호스트 사이트마다 따로 로깅함) (line:212)
CustomLog logs/access_log common
l 로그 필터링 - 불필요한 이미지파일등의 로그는 하지않음 (line:205)
SetEnvIfNoCase Request_URI "\.(gif|jpg|png|css|js|java|swf|ico)$" do_not_log
l JK Connection 설정부분 추가 (line:마지막)
# JK Connection
LoadModule jk_module modules/mod_jk.so
JkWorkerProperty worker.list=ajp13
JkWorkerProperty worker.ajp13.type=ajp13
JkWorkerProperty worker.ajp13.host=localhost
JkWorkerProperty worker.ajp13.port=8009
JkLogFile "logs/mod_jk.log"
JkLogLevel info
JkLogStampFormat "[%a %b %d %H:%M:%S %Y]"
JkMount /*.jsp ajp13
JkMount /*.XXX ajp13
마지막줄은 원하는 JSP 파일 확장자를 추가하는 부분으로 $CATALINA_HOME/conf/web.xml (line:368) 위치에 다음과 같이 같은 확장자를 설정해주면 된다. (필수 아닌 선택)
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.XXX</url-pattern>
</servlet-mapping>
l 가상호스트 설정 방법
NameVirtualHost * (line:마지막) 추가
<VirtualHost *>
ServerAdmin admin@server.domain
DocumentRoot /home/webRoot
ServerName default.server.domain
CustomLog "|/usr/local/sbin/cronolog /logs/SITE_ID/%y%m%d.log" combined env=!do_not_log
</VirtualHost>
- 처음설정은 디폴트 호스트(DocumentRoot)로써 주로 비워두거나 테스트용도로 설정한다.
- [주의] 가상호스트로 사용하기 위해선 반드시 위에서 설정한 <Directory “/home”> 을 확인해야한다. 이를 설정하지 않는다면 모든 가상호스트들 접속시 403에러(접근권한에러)가 난다.
- 이후 추가되는 가상호스트는 같은 형식으로 추가 후 DocumentRoot와 ServerName만 수정
l Cronolog - 위에서 설정한 날짜 별 로그기록 유틸
- http://cronolog.org/download/cronolog-1.6.2.tar.gz
- cronolog 설치 ( ./configure > make > make install )
l 아파치의 접속자수 제한 튜닝 (동시접속자수 수백명이상일경우)
#Include conf/extra/httpd-mpm.conf (line:367) 주석 제거
]# vi /usr/local/apache/conf/extra/httpd-mpm.conf (worker 부분만 수정)
MaxClient 값의 제한은 ServerLimit*ThreadsPerChild 이므로 기본값에 의한 최대치는 현재 400 이다.(16*25) 최대치를 1024 정도로 맞추자면 다음과 같이 설정한다.
StartServers 16
MaxClients 1024
MinSpareThreads 128
MaxSpareThreads 256
ThreadsPerChild 64
MaxRequestsPerChild 0
5. Tomcat Configuration
- server.xml
l Reference Doc: http://jakarta.apache.org/tomcat/tomcat-5.5-doc/config/
l <Connector port="8080"> 모두 주석처리 (아파치와의 커넥터(ajp13)만 사용)
l <Connector port="8009"> URIEncoding="UTF-8" 추가. (GET request 한글처리)
l 가상호스트 설정(아파치와 똑같이 설정)
<Host name="virtual.host.domain" appBase="/home/virtualHomeDir" autoDeploy="true">
<Context path="" docBase="" reloadable="true" allowLinking="true"></Context>
</Host>
l DBCP JNDI 설정
- <GlobalNamingResources>내에 다음과 같은 형식으로 추가.
<Resource name="jdbc/name" auth="Container" type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://xxx.xxx.xxx.xxx:3306/dbname?autoReconnect=true&useUnicode=true&min-connections=2"
username="dbid" password="dbpasswd" maxActive="25" maxIdle="10" maxWait="-1" removeAbandoned="true" removeAbandonedTimeout="60" logAbandoned="true"/>
- 사용할 <Context></Context> 내에 다음과 같이 ResourceLink 추가
<Context path="" docBase="" reloadable="true">
<ResourceLink name="jdbc/name" global="jdbc/name" type="javax.sql.DataSource"/>
</Context>
- web.xml
l “$CATALINA_HOME/conf/web.xml”과 “appBase/docBase/WEB-INF/web.xml”의 이해
모든 application에 두 설정 모두 적용되며, 해당 application에 따라 conf/web.xml이 WEB-INF/web.xml에 include 되는 개념이다. 따라서 두 파일에 중복 설정은 불가능하며, 기본적인 공용 설정부분을 conf/web.xml에 쓰고, 해당 application에서만 쓰이는 설정을 WEB-INF/web.xml에 설정하면 된다. 보통 WEB-INF/web.xml 다음과 같이 만들어주면 된다.
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
version="2.4"></web-app>
l POST request 한글처리
]# cd /usr/local/tomcat/webapps/examples/WEB-INF/classes
]# jar cvf filters.jar ./filters
]# mv filters.jar /usr/local/tomcat/lib/
]# vi conf/web.xml (맨하단 </web-app> 바로윗줄에 추가)
<!-- Charactor Encoding(Only POST method Request -->
<filter>
<filter-name>Set Character Encoding</filter-name>
<filter-class>filters.SetCharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Set Character Encoding</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
6. MySql Install and Configuration
문서 작성 기준 버전: 5.0.45 (last update 2007.07.04)
설치 전 mysql 계정이 있는지 반드시 확인하고, 없으면 생성한다. (useradd mysql)
l MySql5.0 Download from dev.mysql.com (wget)
l ]# tar xzvf mysql-5.0.45-linux-i686.tar.gz -C /usr/local/
l ]# cd /usr/local
l ]# ln -s mysql-5.0.45-linux-i686 mysql
l ]# cd mysql
l ]# ./scripts/mysql_install_db
l ]# chown –R mysql.mysql .
l ]# cp support-files/my-huge.cnf /etc/my.cnf
l ]# cp support-files/mysql.server /etc/rc.d/init.d/mysqld
l ]# ln –s ../init.d/mysqld /etc/rc0.d/K01mysqld
l ]# ln -s ../init.d/mysqld /etc/rc3.d/S99mysqld
l ]# service mysqld {start|stop}
l ]# vi /etc/my.cnf
[client]
default-character-set = utf8
[mysqld]
user = mysql
wait-timeout = 300
max-connections = 200
default-character-set = utf8
language = /usr/local/mysql/share/mysql/korean/
[mysqldump]
default-character-set = utf8
[mysql]
default-character-set = utf8
l Connector/J Install
Connector/J Download from dev.mysql.com (wget) (/usr/local/src)
]# tar zxvf mysql-connector-java-5.0.7.tar.gz
]# cd mysql-connector-java-5.0.7
]# mv mysql-connector-java-5.0.7-bin.jar /usr/local/jdk/jre/lib/ext/
l 계정생성 및 권한설정
grant select,insert,update,delete,create,drop on databasename.* to 'id'@'lo
calhost' identified by 'pass';
7. Environment variable Configuration And TIP
l 다음과 같이 /etc/profile 에 환경변수를 추가할때 alias 도 추가해주자.
]# vi /etc/profile (맨하단에 추가)
# user alias
alias tl='tail -f /usr/local/tomcat/logs/catalina.out'
alias ac='vi /usr/local/apache/conf/httpd.conf'
alias tc='vi /usr/local/tomcat/conf/server.xml'
# user environment
export APACHE_HOME=/usr/local/apache
export JAVA_HOME=/usr/local/jdk
export CATALINA_HOME=/usr/local/tomcat
export CATALINA_OPTS="-server"
export MYSQL_HOME=/usr/local/mysql
export PATH=$PATH:$JAVA_HOME/bin
export PATH=$PATH:$CATALINA_HOME/bin
export PATH=$PATH:$APACHE_HOME/bin
export PATH=$PATH:$MYSQL_HOME/bin
l 서버 부팅 후에 바로 서비스가 실행되게 하는 설정
리눅스도 윈도우와 마찬가지로 MYSQL 설치 부분 에서 설정 한것과 같은 서비스방식과 윈도우의 시작프로그램과 같은 방식이 있다. 이중 TOMCAT 과 APACHE 는 서비스방식보다 윈도우의 시작프로그램과 같은 방식이 더 편하기 때문에 이를 설명한다.
]# vi /etc/rc.d/rc.local (맨하단에 추가)
source /etc/profile
catalina.sh start
apachectl start
l UTF-8 환경
UTF-8 인코딩은 완벽하게 한글을 지원할 뿐만 아니라 유니코드로써 다국어 지원 코드이다. 따라서 현재까지 많이 사용되어왔던 EUC-KR 인코딩 방식의 미지원한글 깨짐 문제를 해결할수 있다. 하지만 UTF-8 을 사용하기 위해서는 서버환경설정 뿐만아니라 작성되는 문서도 UTF-8 인코딩 저장되어야 한다. 따라서 개발할 때 사용되는 에디터에서 반드시 저장옵션을 UTF-8 로 해야한다.
8. PHP5 And ZendOptimizer Install
PHP 란? - JSP와 같은 웹서버 스크립트 언어를 뜻함과 동시에 Tomcat같은 웹서버컨테이너 프로그램.
기본적인 JSP 셋팅이 완료된 시점에서 PHP 설치는 선택사항으로써 필요한 경우에만 설치한다. 이는 JSP와 PHP를 동시에 사용하고자 할 때 필요한 경우로써 매력적인 공개소스 중 하나인 ‘제로보드’ 가 PHP 환경에서 실행되기 때문이다.
[참고] Tomcat 의 경우 Apache 와의 연동이 필수가 아닌 선택이지만 PHP 는 반드시 Apache 설치가 필수이다.
l PHP5 Reference Doc: http://doc.php.net/php/dochowto/
l PHP5 Download from http://www.php.net/downloads.php (wget)
l ]# tar zxvf php-5.2.4.tar.gz
l ]# cd php-5.2.4
l ]#./configure --prefix=/usr/local/php-5.2.4--with-apxs2=/usr/local/apache/bin/apxs --with-mysql=/usr/local/mysql--with-zlib --with-gd --with-ttf --with-png-dir
l ]# make
l ]# make test
l ]# make install
l ]# cp php.ini-dist /usr/local/php-5.2.4/lib/php.ini
l ]# cd /usr/local/
l ]# ln -s php-5.2.4 php
Zend Optimizer 란? - PHP의 성능을 약40%가량 향상시켜 주는 최적화 유틸
l Zend Optimizer Download from : http://www.zend.com/free_download/optimizer
[참고] 가입필수이며, wget 사용이 불가함. 다운로드 후 ftp 등을 통하여 업로드.
l ]# tar zxvf ZendOptimizer-3.3.0a-linux-glibc21-i386.tar.gz
l ]# cd ZendOptimizer-3.3.0a-linux-glibc21-i386
l ]# ./install
l [TEXT GUI] 보통 OK YES EXIT 등의 단일선택으로 넘어가고 php.ini 경로만 적어주면 된다.
Apache httpd.conf 설정 (ac)
l DirectoryIndex index.htm index.jsp index.php - index.php 추가
l LoadModule php5_module modules/libphp5.so - 확인
l AddType application/x-httpd-php .php .phtml - 추가
[출처] JSP 웹서버 셋팅 정리 문서 Ver.2007.09.11|작성자 화랑천
velocity 좋군요..