달력

122024  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

[SQL튜닝]EXISTS 와 DISTINCT

DB 2006. 7. 16. 02:01

SELECT 문장에서 DISTINCT를 사용하는 이유는 중복된 행을 제거 하기 위해서 입니다. 이를 위해 오라클은 SORT를 수행 하며 당연히 소트를 위한 시간, 메모리 또는 디스크 공간이 필요 할 수도 있습니다. 그러니깐 가급적이면 사용 안 하는 것이 좋습니다. 추출되는 데이터가 순서에 의해 출력되지 않아도 된다면 아래의 경우처럼 EXISTS를 사용하는 것이 훨씬 효율적 입니다. 예제를 통해 이해 하도록 하겠습니다. 실제 비용(COST)의 경우도 1/10 정도로 줄게 됩니다.

SQL> set autotrace on
SQL> select distinct c.name
  2  from s_customer c, s_ord o
  3  where c.id = o.customer_id;

NAME
--------------------------------------------------
Beisbol Si!
Big John's Sports Emporium
Delhi Sports
Futbol Sonora
Hamada Sport
Kam's Sporting Goods
Kuhn's Sports
Muench Sports
OJ Atheletics
Ojibway Retail
Sportique

NAME
--------------------------------------------------
Unisports
Womansport

13 개의 행이 선택되었습니다.

경   과: 00:00:00.03

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (UNIQUE)
   2    1     NESTED LOOPS
   3    2       TABLE ACCESS (FULL) OF 'S_ORD'
   4    2       TABLE ACCESS (BY INDEX ROWID) OF 'S_CUSTOMER'
   5    4         INDEX (UNIQUE SCAN) OF 'S_CUSTOMER_ID_PK' (UNIQUE)

이 경우엔 S_ORD 테이블을 전체 스캔 한 데이터와 S_CUSTOMER 테이블의 데이터를 UNIQUE 인덱스를 이용하여 가져온 후 ROWIDD로 찾은 데이터와 비교하여 같은 아이디의 데이터가 있으면 추출하고 아니면 반복하는 구조를 가집니다. 그런 다음 c.name으로 SORT를 하게 되는 거죠…


Statistics
----------------------------------------------------------
         80  recursive calls
          0  db block gets
         57  consistent gets
          0  physical reads
          0  redo size
        651  bytes sent via SQL*Net to client
        503  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         13  rows processed

이번엔 EXISTS를 이용하는 예 입니다.

SQL> select c.name
  2  from s_customer c
  3  where exists (select 1 from s_ord o
  4                where o.customer_id = c.id);

NAME
--------------------------------------------------
Unisports
OJ Atheletics
Delhi Sports
Womansport
Kam's Sporting Goods
Sportique
Muench Sports
Beisbol Si!
Futbol Sonora
Kuhn's Sports
Hamada Sport

NAME
--------------------------------------------------
Big John's Sports Emporium
Ojibway Retail

13 개의 행이 선택되었습니다.

경   과: 00:00:00.03

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     TABLE ACCESS (FULL) OF 'S_CUSTOMER'
   3    1     TABLE ACCESS (FULL) OF 'S_ORD'

이 경우엔 S_CUSTOMER를 전체 스캔하고 S_ORD도 전체 스캔하여 필터링(ID가 같은 데이터가 있는지) 하므로 SORT를 이용하지는 않습니다. 그러므로 앞의 쿼리와 추출되는 데이터의 개수는 같지만 순서는 달리 나오는 겁니다.

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
         49  consistent gets
          0  physical reads
          0  redo size
        651  bytes sent via SQL*Net to client
        503  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
         13  rows processed
Posted by 알 수 없는 사용자
|

WHERE versus HAVING

DB 2006. 7. 16. 02:01

예문을 통해 WHERE를 쓰는 경우와 HAVING을 쓰는 경우의 차이에 대해 알아 보도록 하겠습니다. 어느 방법이 효율이 좋을지 미리 추측을 해보시죠^^;;;
저의 경우 통계 정보를 보기 위해 set autotrace 를  사용했으며 이 부분에 대한 내용은 Oracle 튜닝 강좌를 참고하시기 바랍니다.
참고로 아래 myemp 테이블은 10만건 정도의 레코드를 가지고 있으며 deptno는 외래키로서 인덱스가 걸려 있습니다.

SQL> set autotrace on
SQL> set timing on

아래 질의는 MTEMP 테이블의 데이터를 DEPTNO 별로 그룹핑 하는데 DEPTNO가 10번인 부서원들의 수를 COUNT하는 질의 입니다.

SQL> select deptno, count(*)
  2  from myemp
  3  group by deptno
  4  having deptno = 10;

선택된 레코드가 없습니다.

경   과: 00:00:00.05

Execution Plan
-----------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     SORT (GROUP BY)
   3    2       TABLE ACCESS (FULL) OF 'MYEMP'


이 경우에는 Table을 FULL SCAN한 후 GROUP BY를 한 후 deptno가 10인 것을 찾기 위해 FILTER를 사용 했습니다. 통계정보는 아래와 같습니다.

Statistics
-----------------------------------------------------
          0  recursive calls
          0  db block gets
        304  consistent gets
        275  physical reads
          0  redo size
        282  bytes sent via SQL*Net to client
        372  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          0  rows processed


이 번에는 having 대신 where절을 이용하여 같은 결과를 만들어 보도록 하겠습니다.

SQL> select deptno, count(*)
  2  from myemp
  3  where deptno = 10
  4  group by deptno;

선택된 레코드가 없습니다.

경   과: 00:00:00.04

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   SORT (GROUP BY)
   2    1     INDEX (RANGE SCAN) OF 'IDX_MYEMP_DEPTNO' (NON-UNIQUE)

이 경우에는 DEPTNO의 인덱스를 이용하여 부분 검색을 통해 deptno가 10인 데이터를 꺼낸 후 GROUP BY를 시켜 count(*)를 구하였습니다.

Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
         59  consistent gets
         30  physical reads
       1748  redo size
        282  bytes sent via SQL*Net to client
        372  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          0  rows processed


두 번째의 경우(where를 이용하는 경우)가 deptno에 인덱스가 걸려 있는 경우 약간 효율이 앞선 다고 할 수 있습니다. 시간도 쬐금 앞서구요… 하지만 무조건은 절대 아닙니다. 때에 따라서는 데이터를 먼저 집계를 낸 후 집계된 데이터를 조건을 주어 비교하는 경우도 있으니까요.. 특히 집계된 값이 어떠하다라고 비교 할려면 having구만을 사용해야 합니다. 아래처럼 말입니다.

SQL> select deptno, count(*)
  2  from myemp
  3  group by deptno
  4  having sum(sal) > 2000;

    DEPTNO   COUNT(*)
---------- ----------
        30      49002
        40      16533

2 개의 행이 선택되었습니다.

경   과: 00:00:00.07

Execution Plan
----------------------------------------------------------
   0      SELECT STATEMENT Optimizer=CHOOSE
   1    0   FILTER
   2    1     SORT (GROUP BY)
   3    2       TABLE ACCESS (FULL) OF 'MYEMP'


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
        304  consistent gets
        300  physical reads
          0  redo size
        482  bytes sent via SQL*Net to client
        503  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
          2  rows processed
Posted by 알 수 없는 사용자
|

우선 돌리고자 하는 프로시져를 작성한다.
DBMS_JOBS_TEST 라고 만들었다.

그렇담
내일새벽 5시부터 매일 새벽5시에 DBMS_JOBS_TEST 프로시져를
수행하고자 하는 스케쥴러를 등록하고자 한다면

begin
    dbms_job.submit(:JOBSID,
                             `DBMS_JOBS_TEST;`,
                              trunc(sysdate) + 1 + 5/24 ,
                              `sysdate + 1`);
   commit;
end;
/
// :JOBSID 는 숫자로 아무거나 넣으면 됨

그리고 나서 등록이 되었는지 안되었는지 조회해보자
SELECT * FROM user_jobs
여기 등록되었다고 다 실행되는 것이 아니다.

뭐 이 DBMS_JOB package를 이용하기 위해선 SNP background process가 start되어 있어야 하고.... 이걸 뛰울려면

init<SID>.ora file 파일에
job_queue_processes = 1
-> 이 파라미터는 snp process를 몇 개 띄울지를 결정한다.
default=0
job_queue_interval = 60
-> 이 파라미터는 snp process가 깨어나는 간격을 초로 설정한다.
이런게 세팅되어 있어야 한다고 한다.

'DB' 카테고리의 다른 글

[SQL튜닝]EXISTS 와 DISTINCT  (0) 2006.07.16
WHERE versus HAVING  (0) 2006.07.16
nls_database 세팅보기(캐릭터 셋 보기)  (0) 2006.01.06
캐릭터셋 변경  (0) 2006.01.06
MySQL 캐릭터셋 설정  (0) 2005.12.10
Posted by 알 수 없는 사용자
|

Job Scheduling in Java

java 2006. 7. 16. 01:40

by Dejan Bosanac
번역 허태명
03/10/2004


어떤 프로젝트에서 정확히 정해진 시간이나 일정한 시간 간격으로 실행되는 작업이 필요할 수 있다. 이 글에서 우리는 자바 개발자가 표준 Java Timer API를 사용하여 어떻게 이러한 요구사항을 구현할 수 있는지 살펴볼 것이다. 그리고, Java Timer API가 제공하는 기본적인 스케쥴링 시스템 외에 추가적인 기능을 필요로 하는 사람을 위해 오픈 소스 라이브러리인 Quartz에 대해 살펴볼 것이다.

먼저 스케쥴링 작업을 필요로 할 때, 이러한 상황을 인식하는데 도움을 줄 수 있는 일반적인 유스케이스에 대해서 몇 가지 살펴보자. 그리고 나서 우리는 여러분의 기능적 요구에 가장 알맞는 최선의 해결책을 찾아 볼 것이다.

대부분의 비지니스 애플리케이션은 유저들이 필요로 하는 보고서와 통계를 가지고 있다. 시스템에 투자하는 사람들의 일반적인 목적은 대량의 데이터를 수집하고 그것을 미래의 비지니스 계획을 세우는데 도움이 되는 방식으로 보는 것이기 때문에, 이러한 일을 가능하게 해주는 보고서가 없는 시스템은 상상하기 힘들다. 이러한 보고서를 생성하는데 있어서의 문제점은 처리해야할 데이터의 양이 대량이기 때문에, 일반적으로 데이터베이스 시스템에 큰 부하가 걸린다는 것이다. 이러한 부하는 시스템이 보고서를 생성하는 동안, 전체적인 애플리케이션의 성능을 떨어뜨리고 단지 데이터 수집을 위하여 시스템을 사용하는 유저에게도 영향을 끼친다. 또한 유저입장에서 생각한다면, 생성하는데 10분이 걸리는 보고서는 좋은 응답시간의 예가 아니다.

우리는 먼저 실시간으로 실행될 필요가 없는, 캐쉬될 수 있는 종류의 보고서에 대해 살펴볼 것이다. 기쁘게도 대부분의 보고서가 이러한 부류에 들어간다. -- 작년 6월의 어떤 제품 판매에 관한 통계, 또는 1월의 회사 소득 등. 이러한 캐쉬가 가능한 보고서는 간단한 방법으로 해결할 수 있다 : 시스템이 한가할 때 또는 데이터베이스 시스템의 부하가 최소일 때, 보고서를 생성하도록 스케쥴링하면 된다. 여러분이 많은 사무실(모두 같은 시간대에 있는)을 가지고 있는 지역 책 판매자를 위한 애플리케이션을 만든다고 해보자. 여러분은 주당 소득에 대한 보고서(아마도 대량의)를 생성할 필요가 있다. 매주 일요일 밤과 같이 시스템이 사용되지 않는 시간에 데이터베이스에 캐쉬해서 이러한 작업을 스케쥴링 할 수 있다. 이러한 방식으로 구현하면, 판매 담당자는 여러분의 애플리케이션에서 성능상의 문제점을 찾지 못할 것이다. 그리고 회사 관리부는 필요한 모든 데이터를 빠른 시간에 구할 수 있을 것이다.

다음으로 다룰 두번째 예제는 계정 사용기한 만료와 같은 일련의 공지(notification)를 애플리케이션 유저에게 보내는 작업에 관한 것이다. 이것은 유저 데이터에서 날짜 필드를 사용하여 유저의 조건을 검사하는 쓰레드를 생성함으로써 행해질 수 있다. 그러나 이러한 경우 스케쥴러를 사용하는 것이 명백하게 더욱 우아한 해결책이고, 전체적인 시스템 아키텍쳐(아키텍쳐는 중요하다. 그렇지 않은가?)의 측면에서도 더 좋다. 복잡한 시스템에서 여러분은 이러한 종류의 공지를 많이 가지고 있을 것이고, 이외의 다른 많은 경우에도 또한 스케쥴러 시스템이 필요할 것이다. 따라서, 스케쥴링 작업을 필요로 하는 각각의 경우에 대해 해결책을 구현하는 것은 시스템을 변경하고 유지보수하는 것을 더욱 어렵게 할 것이다. 일일이 스케쥴링 작업을 구현하는 대신 여러분은 애플리케이션의 모든 스케쥴링을 담당하는 API를 사용해야만 한다. 이것이 이 글의 나머지 부분에서 다루는 주제이다.

간단한 해결책
자바 애플리케이션에 기본적인 스케쥴러를 구현하기 위해 여러분은 어떤 외부 라이브러리도 필요 없다. J2SE 1.3 이후로 자바는 이러한 목적으로 사용될 수 있는 java.util.Timer, java.util.TimerTask 두 개의 클래스를 포함하고 있다. 먼저 이 API로 가능한 모든 것을 설명해주는 간단한 예제를 만들어보자.

package net.nighttale.scheduling;

import java.util.Calendar;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;

public class ReportGenerator extends TimerTask {

  public void run() {
    System.out.println("Generating report");
    //TODO generate report
  }

}

class MainApplication {

  public static void main(String[] args) {
    Timer timer  new Timer();
    Calendar date = Calendar.getInstance();
    date.set(
      Calendar.DAY_OF_WEEK,
      Calendar.SUNDAY
    );
    date.set(Calendar.HOUR, 0);
    date.set(Calendar.MINUTE, 0);
    date.set(Calendar.SECOND, 0);
    date.set(Calendar.MILLISECOND, 0);
    // Schedule to run every Sunday in midnight
    timer.schedule(
      new ReportGenerator(),
      date.getTime(),
      1000 * 60 * 60 * 24 * 7
    );
  }
}
이 글의 모든 예제 코드는 글 말미의 링크에서 다운로드 받을 수 있다.

위의 코드는 서두에서 언급한, 시스템이 한가한 시간(예제의 경우 일요일 밤)에 보고서를 생성하도록 스케쥴링하는 예제를 구현하고 있다.

먼저 우리는 실제로 스케쥴링된 작업을 수행하는 "worker" 클래스를 구현해야 한다. 우리의 예제에서 이것은 ReportGenerator 이다. 이 클래스는 java.lang.Runnable을 구현하는 java.util.TimerTask 클래스를 상속받아야 한다. 그리고 나머지 할 일은 보고서를 생성하는 코드를 run() 메소드 안에 오버라이드하는 것 밖에 없다.

우리는 이 객체의 실행을 Timer 클래스의 스케쥴링 메소드 중의 하나를 이용해서 스케쥴링한다. 예제의 경우 최초 실행 날짜와 밀리세컨드 단위의 실행 주기를 인자로 받아들이는 schedule() 메소드를 사용한다.(왜냐하면 우리는 이 보고서를 매주 생성할 것이기 때문이다.)

스케쥴링 기능을 사용할 때, 우리는 스케쥴링 API가 제공하는 리얼타임에 대한 보증을 알아야만 한다. 불행하게도 자바의 특성과 다양한 플랫폼에서의 구현때문에, 각각의 JVM에서의 쓰레드 스케쥴링의 구현은 일치하지 않는다. 그러므로, Timer는 우리의 스케쥴링 작업이 정확한 규정된 시간에 실행될 것이라고 보증할 수 없다. 우리의 스케쥴링 작업은 Runnable 객체로 구현되어 있고 때때로 일정 시간동안 sleep 상태가 된다. 그러면 Timer는 규정된 순간에 이들을 깨운다. 그러나 정확한 실행 시간은 JVM의 스케쥴링 정책과 현재 얼마나 많은 쓰레드가 프로세서를 기다리고 있느냐에 따라 달라진다. 우리의 스케쥴링 작업 실행을 지연시킬 수 있는 두 가지 일반적인 경우가 있다. 첫째, 많은 수의 쓰레드가 실행되기를 기다리고 있는 경우이다; 둘째, 가비지 콜렉션의 활동에 의해 지연되는 경우가 있다. 이러한 영향들은 다른 JVM에서 스케쥴러를 실행 또는 가비지 콜렉터의 옵션 튜닝과 같은 여러가지 기법을 사용함으로써 최소화될 수 있다. 그러나 그것은 이 글의 주제를 벗어나는 것이다.

다시 본론으로 돌아오자. Timer 클래스에는 두 개의 다른 스케쥴링 메소드 그룹이 있다: 고정된 딜레이로 스케쥴링하는 schedule() 메소드와 고정된 비율로 스케쥴링하는 scheduleFixedRate() 메소드가 그것이다. 첫번째 그룹의 메소드들을 사용할 때, 각 작업 실행의 딜레이는 다음 작업 실행으로 전달될 것이다. 후자 그룹의 경우 딜레이를 최소화하면서 모든 연속된 작업 실행은 최초 작업 실행의 시간에 맞춰 스케쥴링 될 것이다. 어떤 메소드를 사용하느냐는 여러분의 시스템에 어떤 파라미터가 더 중요하느냐에 달려 있다.

매우 중요한 것이 한 가지 더 있다: 각 Timer 객체는 쓰레드를 백그라운드로 시작한다. 이러한 방식은 J2EE 애플리케이션 서버와 같은 환경에서는 바람직하지 않을 것이다. 왜냐하면 이러한 쓰레드들이 컨테이너 영역 내에 있지 않기 때문이다.


평범한 것을 넘어서
지금까지 애플리케이션에서 어떻게 스케쥴링을 하는지 살펴 보았고, 이것은 간단한 요구사항에서는 충분하다. 그러나 고급 유저와 복잡한 요구사항을 위해서는 유용한 스케쥴링을 지원하기 위해 더 많은 기능들을 필요로 한다. 이러한 경우 두 가지 일반적인 해결책이 있다. 첫번째는 자신이 필요로 하는 기능을 가지고 있는 스케쥴러를 만드는 것이다; 두번째는 필요로 하는 요구사항을 충족하는 프로젝트를 찾아내는 것이다. 시간과 자원을 절약할 수 있고 다른 누군가의 노력을 중복해야 할 필요가 없기 때문에, 두번째 해결책이 대부분의 경우에 있어서 더욱 적합할 것이다.

이러한 요구사항은 우리를 단순한 Timer API 이상의 훨씬 뛰어난 장점들을 가진 오픈 소스 작업 스케쥴링 시스템인 Quartz 로 유도한다.

Quartz의 첫번째 장점은 영속성이다. 만약 여러분의 작업이 앞서의 예제와 같이 "정적"이라면 영속성 지원은 필요 없을 것이다. 그러나 종종 어떤 조건이 충족됐을 때 "동적"으로 수행되는 작업을 필요로 할 때도 있다. 그리고 이러한 작업들이 시스템 재시작(또는 시스템 다운) 사이에도 실행되야 할 때가 있다. Quartz는 비 영속성과 영속성 작업 모두를 제공한다. 영속성 작업의 상태는 데이터베이스에 저장될 것이다. 따라서 이러한 작업들이 실행되지 않는 경우가 없을 거라고 확신할 수 있다. 영속성 작업은 시스템에 추가적인 성능 감소를 유발하기 때문에 주의깊게 사용해야만 한다.

Timer API는 원하는 실행시간을 단순하게 설정할 수 있는 메소드가 부족하다. 위 예제에서 본 대로, 실행시간을 설정하기 위해서 할 수 있는 것은 시작일자와 반복주기를 설정하는 것 밖에 없다. 분명히, 유닉스의 cron을 사용해 본 사람들은 스케쥴러를 그와 유사하게 설정할 수 있기를 바랄 것이다. Quartz는 원하는 시간, 날짜를 유연한 방법으로 설정할 수 있게 해주는 org.quartz.CronTrigger를 정의하고 있다.

개발자는 자주 한 가지 이상의 기능을 필요로 한다: 작업의 이름에 의한 작업의 관리와 조직화. Quartz에서 당신은 이름이나 그룹에 의해 원하는 작업을 찾아낼 수 있다. 이것은 스케쥴된 작업이 많은 환경에서 큰 도움이 될 것이다.

이제 보고서 생성 예제를 Quartz를 사용하여 구현하고 라이브러리의 기본적인 기능들에 대해 설명할 것이다.

package net.nighttale.scheduling;

import org.quartz.*;

public class QuartzReport implements Job {

  public void execute(JobExecutionContext cntxt)
    throws JobExecutionException {
      System.out.println(
        "Generating report - " +
cntxt.getJobDetail().getJobDataMap().get("type")
      );
      //TODO Generate report
  }

  public static void main(String[] args) {
    try {
      SchedulerFactory schedFact
       new org.quartz.impl.StdSchedulerFactory();
      Scheduler sched  schedFact.getScheduler();
      sched.start();
      JobDetail jobDetail
        new JobDetail(
          "Income Report",
          "Report Generation",
          QuartzReport.class
        );
      jobDetail.getJobDataMap().put(
                                "type",
                                "FULL"
                               );
      CronTrigger trigger  new CronTrigger(
        "Income Report",
        "Report Generation"
      );
      trigger.setCronExpression(
        "0 0 12 ? * SUN"
      );
      sched.scheduleJob(jobDetail, trigger);
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}
Quartz는 Job, Trigger라는 두 개의 기본 추상 계층을 정의한다. Job은 실제 실행되는 작업의 추상 계층이고, Trigger는 언제 작업이 실행되어야 하는지를 나타내는 추상 계층이다.

Job은 인터페이스이다. 그래서 우리가 해야할 일은 클래스가 org.quartz.Job(또는 나중에 살펴보게 될 org.quartz.StatefulJob) 인터페이스를 구현하도록 하고 execute() 메소드를 오버라이드하는 것 뿐이다. 예제에서 java.util.Map의 변형된 구현인 jobDataMap 어트리뷰트를 통해 어떻게 Job에게 파라미터를 전달할 수 있는지 살펴 봤다. 상태가 있는 작업, 또는 비 상태 작업 중 어떤 것을 구현하느냐 결정하는 것은 스케쥴링 작업의 실행동안 이러한 파라미터들을 변경하기를 원하느냐 아니냐를 결정하는 문제이다. Job 인터페이스를 구현한다면 모든 파라미터들은 작업이 최초로 스케쥴링 되는 순간에 저장된다. 그리고 이후의 변경은 모두 버려진다. execute() 메소드 내에서 StatefulJob의 파라미터를 변경한다면, 작업이 다음에 새로 스케쥴링 될 때 이 새로운 값이 전달될 것이다. 고려해야할 한 가지 중요한 사항은 StatefulJob 인터페이스를 구현한 작업들은 실행되는 동안 인자들이 변할 수 있기 때문에 동시에 실행될 수 없다는 것이다.

Trigger에는 두 가지의 기본적인 Trigger가 있다: SimpleTrigger 와 CronTrigger. SimpleTrigger는 기본적으로 Timer API가 제공하는 것과 같은 기능을 제공한다. 작업이 시작된 이후에 정해진 간격으로 반복해서 실행되는 경우, SimpleTrigger를 써야 한다. SimpleTrigger는 시작일, 종료일, 반복횟수, 실행 주기를 정의할 수 있다.

위의 예제에서는 더욱 사실적인 바탕에서 작업을 스케쥴링할 수 있는 유연성때문에 CronTrigger를 사용했다. CrinTrigger 사용함으로써 "매주 평일 오후 7시" 또는 "토요일과 일요일 매 5분마다"와 같은 스케쥴링 작업을 할 수 있다. 더 이상 cron에 관해 설명하지는 않을 것이다. cron에 관한 세부사항은 CronTrigger의 Javadoc을 참조하기 바란다.

위의 예제를 실행하기 위해 클래스패쓰에 기본적인 Quartz의 설정을 하는 quartz.properties 파일을 필요로 한다. 만약 파일이름을 다르게 사용하기를 원한다면, 파일이름을 StdScheduleFactory 생성자에 인자로 전달해야만 한다. 아래에 최소한의 프로퍼티들만 설정한 파일의 예제가 있다:

#
# Configure Main Scheduler Properties
#

org.quartz.scheduler.instanceName = TestScheduler
org.quartz.scheduler.instanceId = one

#
# Configure ThreadPool
#

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount =  5
org.quartz.threadPool.threadPriority = 4

#
# Configure JobStore
#

org.quartz.jobStore.misfireThreshold = 5000

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
표준 Timer API에 비해 Quartz가 가진 또 다른 장점은 쓰레드 풀의 사용이다. Quartz는 작업 실행을 위한 쓰레드를 얻기 위해 쓰레드 풀을 사용한다. 쓰레드 풀의 크기는 동시에 실행될 수 있는 작업의 수에 영향을 미친다. 실행해야할 작업이 있지만 쓰레드 풀에 남아 있는 쓰레드가 없다면, 작업은 여분의 쓰레드가 생길 때까지 sleep 상태가 될 것이다. 시스템에서 얼마나 많은 쓰레드를 사용할 지는 매우 결정하기 어려운 문제이고, 실험적으로 결정하는 것이 가장 좋다. 쓰레드 풀 크기의 기본 값은 5이고 수천개의 작업을 다루지 않는다면 이것은 충분할 것이다. Quartz 자체에서 구현한 쓰레드 풀이 있지만, 다른 쓰레드 풀의 구현이 있다면 그것을 사용하는데 제약을 받지는 않는다.

이제 JobStore에 관해 살펴 보자.JobStore는 Job과 Trigger에 관한 모든 데이터를 보존한다. 따라서 작업에 영속성을 부여할 것인지 아닌지 결정하는 것은 어떤 JobStore를 사용하느냐에 달려 있다. 예제에서 우리는 org.quartz.simpl.RAMJobStore를 사용했다. 이것은 모든 데이터는 메모리에 저장될 것이고 그러므로 비 영속성이라는 것을 의미한다. 따라서 애플리케이션이 다운되면 스케쥴링 작업에 관한 모든 데이터는 사라질 것이다. 어떤 상황에서 이것은 바람직한 방식이다. 그러나 데이터를 보존하고 싶다면 애플리케이션이 org.quartz.simpl.JDBCJobStoreTX(또는 org.quartz.simpl.JDBCJobStoreCMP)를 사용하도록 설정해야 한다.JDBCJobStoreTX는 좀 더 많은 설정 파라미터를 필요로 하고 그것은 아래 예제에서 설명할 것이다.

org.quartz.jobStore.class = org.quartz.impl.jdbcjobstore.JobStoreTX
org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
org.quartz.jobStore.dataSource = myDS
org.quartz.jobStore.tablePrefix = QRTZ_

#
# Configure Datasources
#

org.quartz.dataSource.myDS.driver = org.postgresql.Driver
org.quartz.dataSource.myDS.URL = jdbc:postgresql:dev
org.quartz.dataSource.myDS.user = dejanb
org.quartz.dataSource.myDS.password =
org.quartz.dataSource.myDS.maxConnections  5
Quartz와 관계형 데이터베이스를 성공적으로 사용하기 위하여 먼저 Quartz가 필요로 하는 테이블을 생성할 필요가 있다. 적절한 JDBC 드라이버를 가지고 있다면 어떤 데이터베이스 서버도 사용 가능하다. docs/dbTables 폴더에서 필요한 테이블을 생성하는 초기화 스크립트를 찾을 수 있다.

테이블을 생성한 후에 표준 SQL 쿼리를 특정 RDBMS의 SQL 문법에 맞게 변경해주는 위임(delegate) 클래스를 선언해야 한다. 예제에서 우리는 PostgreSQL을 데이터베이스 서버로 선택했고 따라서 org.quartz.impl.jdbcjobstore.PostgreSQLDelegate 클래스를 위임 클래스로 설정했다. 당신이 사용하는 특정 데이터베이스 서버를 위해 어떤 위임 클래스를 사용해야 하는지에 관한 정보는 Quartz 문서를 참조하기 바란다.

tablePrefix 파라미터는 데이터베이스에서 Quartz 테이블이 사용할 접두어를 정의한다. 디폴트는 QRTZ_ 이다. 이런 방식으로 데이터베이스의 나머지 테이블과 구분할 수 있다.

사용하는 매 JDBC store마다 어떤 datasource를 사용할 것인지 정의할 필요가 있다. 이것은 일반적인 JDBC의 설정이기 때문에 여기서 더 이상 설명하지는 않을 것이다.

Quartz의 뛰어난 점은 이러한 설정을 변경한 후에 보고서 생성 예제의 코드를 단 한줄도 변경하지 않고, 데이터를 데이터베이스에 저장할 것이라는 것이다.


Advanced Quartz
지금까지 프로젝트에 Quartz를 사용하기 위한 좋은 기초가 될 수 있는 기본적인 것에 대해 살펴 보았다. 이외에도 Quartz 라이브러리는 당신의 수고를 크게 덜어줄 수 있는 뛰어난 아키텍쳐를 가지고 있다.

Quartz는 기본적으로 제공하는 것 외에 애플리케이션의 문제를 해결하는데 도움이 되는 뛰어난 아키텍쳐를 가지고 있다. 그 중 중요한 기능 중의 하나는 listener 이다: 이것은 시스템에 어떤 이벤트가 발생할 때 호출된다. 세 가지 종류의 리스너가 있다: JobListener, TriggerListener, SchedulerListener 이다.

리스너는 시스템에 무언가 이상이 생겨서 이에 대한 공지나 알림 기능을 원할 때 특히 유용하다. 예를 들어 보고서 생성 중에 에러가 발생하면 개발 팀에게 이를 알리는 우아한 방법은 E-mail이나 SMS를 보내는 JobListener 를 만드는 것이다.

JobListener 는 더 흥미로운 기능을 제공한다. 시스템 자원의 가용성에 크게 의존하는 일(그다지 안정적이지 못한 네트워크와 같은)을 다루는 작업을 상상해보라. 이러한 경우 작업이 실행될 때 자원이 사용 불가능하다면 이를 재실행 시키는 리스너를 만들 수 있다.

Quartz는 또한 Trigger 가 잘못 실행되거나 스케쥴러가 다운되서 실행되지 않았을 때의 상황을 다룰 수 있다. Trigger 클래스의 setMisfireInstruction() 메소드를 사용함으로써 오실행에 관한 처리를 설정할 수 있다. 이 메소드는 오실행 명령 타입을 인자로 받아들인고, 그 값은 다음중의 하나가 될 수 있다:

Trigger.INSTRUCTION_NOOP: 아무 일도 하지 않는다.
Trigger.INSTRUCTION_RE_EXECUTE_JOB: 즉시 작업을 실행한다.
Trigger.INSTRUCTION_DELETE_TRIGGER: 오실행한 작업을 삭제한다.
Trigger.INSTRUCTION_SET_TRIGGER_COMPLETE: 작업이 완료를 선언한다.
Trigger.INSTRUCTION_SET_ALL_JOB_TRIGGERS_COMPLETE: 작업을 위한 모든 trigger의 완료를 선언한다.
Trigger.MISFIRE_INSTRUCTION_SMART_POLICY: 특정 Trigger의 구현에 가장 알맞은 오실행 처리 명령을 선택한다.
CronTrigger와 같은 Trigger 구현은 유용한 오실행 처리 명령을 새로 정의할 수 있다. 이것에 관한 더 많은 정보는 이 클래스들의 Javadoc을 참고하기 바란다. TriggerListener를 사용함으로써 오실행이 발생했을 경우 취할 액션에 관해 더 많은 제어권을 가질 수 있다. 또한 트리거의 실행이나 종료와 같은 트리거 이벤트에 반응하기 위해 이것을 사용할 수 있다.

SchedulerListener 는 스케쥴러 종료나 작업과 트리거의 추가나 제거와 같은 전체적인 시스템 이벤트를 다룬다.

여기서 우리는 보고서 생성 예제를 위한 간단한 JobListener 를 보여줄 것이다. 먼저 JobListener 인터페이스를 구현하는 클래스를 작성해야 한다.

package net.nighttale.scheduling;

import org.quartz.*;


public class MyJobFailedListener implements JobListener {

  public String getName() {
    return "FAILED JOB";
  }

  public void jobToBeExecuted
    (JobExecutionContext arg0) {
  }


  public void jobWasExecuted(
    JobExecutionContext context,
    JobExecutionException exception) {

    if (exception != null) {
      System.out.println(
        "Report generation error"
      );
      // TODO notify development team
    }       
  }
}
그리고 예제의 main 메소드에 다음을 추가한다:

sched.addGlobalJobListener(new MyJobFailedListener());
이 리스너를 스케쥴러 작업 리스너의 전체 목록에 추가함으로써 리스너는 모든 작업의 이벤트에 대해 호출 될 것이다. 물론 리스너를 특정 작업에 대해서만 설정할 수도 있다. 그렇게 하기 위해 Scheduler의 addJobListeners() 메소드를 사용해야 한다. 그리고 JobDetail의 addJobListener() 메소드를 사용해서 등록된 리스너를 리스너의 작업 목록에 추가해라. 이 때 리스너 이름을 파라미터로 사용한다. 리스너 이름은 리스너의 getName() 메소드의 리턴값을 말한다.

sched.addJobListener(new MyJobFailedListener());
jobDetail.addJobListener("FAILED JOB");
리스너가 정말로 동작하는지 테스트하기 위해, 다음을 보고서 생성 작업의 execute() 메소드 안에 추가한다.

throw new JobExecutionException();
작업이 실행된 후 리스너의 jobWasExecuted() 메소드가 실행되고, 예외가 발생한다. 예외는 null 이 아니기 때문에 "Report generation error" 메세지를 화면에서 볼 수 있을 것이다.

리스너에 관한 마지막 사항은 애플리케이션의 성능을 떨어뜨릴 수 있기 때문에 시스템에서 사용되는 리스너의 갯수에 유의해야한다는 것이다.

Quartz의 기능을 확장할 수 있는 방법이 한 가지 더 있다. 그것은 플러그 인을 이용하는 것이다. 플러그 인은 실질적으로 여러분이 필요한 어떤 일도 할 수 있다; 단지 해야할 일은 org.quartz.spi.SchedulerPlugin 인터페이스를 구현하는 것 뿐이다. 이 인터페이스는 구현해야할 두 개의 메소드를 정의한다 -- 하나는 초기화(스케쥴러 객체를 파라미터로 받는)를 위한 것이고, 또 하나는 종료를 위한 것이다. 나머지는 모두 여러분에게 달려 있다. SchedulerFactory 가 플러그 인을 사용하도록 하기 위해 quartz.properties 파일에 플러그 인 클래스와 몇 가지 옵션 설정 파라미터(플러그인를에서 필요로 하는)를 추가한다. In order to make SchedulerFactory use a certain plug-in, all you have to do is to add a line in the properties file (quartz.properties) with the plug-in class and a few optional configuration parameters (which depend on the particular plug-in). There are a few plug-ins already in Quartz itself. One is the shutdownHook, which can be used to cleanly shut down the scheduler in case the JVM terminates. To use this plug-in, just add the following lines in the configuration file:

org.quartz.plugin.shutdownHook.class =
   org.quartz.plugins.management.ShutdownHookPlugin
org.quartz.plugin.shutdownHook.cleanShutdown = true

어떤 환경에서도 융통성 있는
지금까지 Standalone 애플리케이션에서 Quartz를 사용하는 것을 살펴 봤다. 이제 Quartz 인터페이스를 자바 개발자의 가장 보편적인 환경 하에서 어떻게 사용하는지 살펴 보자.

RMI
RMI를 사용하는 분산 애플리케이션에서 Quartz는 지금까지 본 것과 마찬가지로 간단하다. 차이점은 설정 파일 밖에 없다.

두 가지 필수 단계가 있다: 먼저 Quratz를 우리의 request를 다루는 RMI 서버로 설정하는 것이다. 그러고 나면 단지 일반적인 방식으로 사용하기만 하면 된다.

이 예제의 소스 코드는 실질적으로 첫번째 예제와 동일하지만, 우리는 이것을 두 부분으로 나눌 것이다: 스케쥴러 초기화와 스케쥴러 사용.

package net.nighttale.scheduling.rmi;

import org.quartz.*;

public class QuartzServer {

  public static void main(String[] args) {
       
    if(System.getSecurityManager() != null) {
      System.setSecurityManager(
        new java.rmi.RMISecurityManager()
      );
    }
          
    try {
      SchedulerFactory schedFact =
       new org.quartz.impl.StdSchedulerFactory();
      Scheduler sched = schedFact.getScheduler();
      sched.start();        
    } catch (SchedulerException se) {
      se.printStackTrace();
    }
  }
}
위에서 볼 수 있듯이, 스케쥴러 초기화 코드는 security manager를 설정하는 부분을 포함하고 있다는 것 외엔 다른 것과 다를 바 없다. 중요한 것은 설정 파일(quartzServer.properties)안에 있다. 그것은 다음과 같다:

#
# Configure Main Scheduler Properties
#
org.quartz.scheduler.instanceName = Sched1
org.quartz.scheduler.rmi.export = true
org.quartz.scheduler.rmi.registryHost = localhost
org.quartz.scheduler.rmi.registryPort = 1099
org.quartz.scheduler.rmi.createRegistry = true

#
# Configure ThreadPool
#

org.quartz.threadPool.class = org.quartz.simpl.SimpleThreadPool
org.quartz.threadPool.threadCount = 5
org.quartz.threadPool.threadPriority = 4

#
# Configure JobStore
#

org.quartz.jobStore.misfireThreshold = 5000

org.quartz.jobStore.class = org.quartz.simpl.RAMJobStore
강조된 부분을 주목하라. 이것은 이 스케쥴러가 RMI를 통하여 인터페이스를 반출하고 RMI registry를 실행하기 위해 파라미터들을 제공해야 한다는 것을 보여준다.

서버를 성공적으로 배치하기 위해 몇 가지 할 일이 더 있다. 그것들은 모두 RMI를 통하여 객체를 반출하기 위한 전형적인 작업이다. 먼저 rmiregistry 를 시작할 필요가 있다. 이것은 다음과 같이 호출함으로써 이루어진다: 유닉스 시스템에서는

   rmiregistry &
또는 윈도우 플랫폼에서는

   start rmiregistry


다음에 QuartzServer 클래스를 다음과 같은 옵션으로 시작한다:

java  -Djava.rmi.server.codebase
   file:/home/dejanb/quartz/lib/quartz.jar
   -Djava.security.policyrmi.policy
   -Dorg.quartz.propertiesquartzServer.properties
   net.nighttale.scheduling.rmi.QuartzServer
이제 이러한 파라미터들을 좀 더 자세하게 살펴보자. Quartz의 Ant 빌드 태스크는 필요한 RMI 클래스를 클라이언트가 codebase로 가리키도록 이 클래스들을 생성하는 rmic 호출을 포함하고 있다. 그러기 위해 빌드 파일에 -Djava.rmi.server.codebase 파라미터로 시작하도록 설정해야 한다:추가적으로 quartz.jar 의 완전한 경로도 포함되어야 한다.(물론 이것은 라이브러리의 URL이 될 수도 있다.)

분산 시스템에서 중요한 이슈는 보안이다; 그러므로 RMI는 보안 정책을 사용하도록 강제할 것이다. 예제에서는 모든 권한을 허용하는 기본 정책(rmi.policy)을 사용했다.

grant {
  permission java.security.AllPermission;
};
실제 적용할 때는 시스템의 보안 요구사항에 따라 정책을 변경해야 한다.

이제 스케쥴러는 RMI를 통해 Job을 받아들일 준비가 되었다. 이제 클라이언트 코드를 살펴보자.

package net.nighttale.scheduling.rmi;

import org.quartz.*;
import net.nighttale.scheduling.*;

public class QuartzClient {

  public static void main(String[] args) {
    try {
      SchedulerFactory schedFact =
       new org.quartz.impl.StdSchedulerFactory();
      Scheduler sched = schedFact.getScheduler();
      JobDetail jobDetail = new JobDetail(
        "Income Report",
        "Report Generation",
        QuartzReport.class
      );
               
      CronTrigger trigger = new CronTrigger(
        "Income Report",
        "Report Generation"
      );
      trigger.setCronExpression(
        "0 0 12 ? * SUN"
      );
      sched.scheduleJob(jobDetail, trigger);
    } catch (Exception e) {
      e.printStackTrace();
    }
   }
}
위에 나온 바와 같이 클라이언트 소스는 이전 예제와 동일하다. 물론 여기에도 quartzClient.properties 의 설정은 다르다. 추가해야 할 것은 이 스케쥴러가 RMI 클라이언트(proxy)이고, 서버를 찾을 registry의 위치를 설정하는 것 뿐이다.

# Configure Main Scheduler Properties  

org.quartz.scheduler.instanceName = Sched1
org.quartz.scheduler.rmi.proxy = true
org.quartz.scheduler.rmi.registryHost = localhost
org.quartz.scheduler.rmi.registryPort = 1099
나머지는 모두 서버 측에서 이루어지기 때문에 클라이언트 측에 이외의 다른 어떤 설정도 필요하지 않다. 사실 다른 설정이 있다해도 이것은 무시될 것이다. 중요한 것은 스케쥴러의 이름은 클라이언트와 서버의 설정이 일치해야 한다는 것이다.(예제의 경우 Shed1) 그러면 시작하기 위한 준비는 끝났다. 단지 리다이렉트된 프로퍼티 파일로 클라이언트를 시작하기만 하면 된다:

java -Dorg.quartz.properties
       quartzClient.properties
       net.nighttale.scheduling.rmi.QuartzClient
이제 서버 콘솔에서 첫번째 예제와 같은 결과를 볼 수 있을 것이다.

웹과 엔터프라이즈 환경
웹이나 엔터프라이즈 환경의 솔류션을 개발하고 있다면, 스케쥴러를 시작하기 위한 적절한 곳은 어디인지 의문이 생길 것이다. 이것을 위해 Quartz는 org.quartz.ee.servlet.QuartzInitializerServlet를 제공한다. 할 일은 단지 web.xml 파일에 다음 설정을 추가하는 것 뿐이다:


 
   QuartzInitializer
 
 
   Quartz Initializer Servlet
 
 
   org.quartz.ee.servlet.QuartzInitializerServlet
 
 
   1
 

Job을 EJB 메소드에서 호출하고 싶다면, org.quartz.ee.ejb.EJBInvokerJob 클래스를 JobDetail 에 전달해야 한다. 이러한 기법을 보여주기 위해, ReportGenerator를 세션빈으로 구현하고 서블릿으로부터 generateReport() 메소드를 호출할 것이다.

package net.nighttale.scheduling.ee;

import java.io.IOException;

import javax.servlet.*;
import net.nighttale.scheduling.Listener;
import org.quartz.*;
import org.quartz.ee.ejb.EJBInvokerJob;
import org.quartz.impl.StdSchedulerFactory;

public class ReportServlet implements Servlet {

  public void init(ServletConfig conf)
    throws ServletException {
    JobDetail jobDetail = new JobDetail(
      "Income Report",
      "Report Generation",
      EJBInvokerJob.class
    );
    jobDetail.getJobDataMap().put(
      "ejb",
      "java:comp/env/ejb/Report"
    );
    jobDetail.getJobDataMap().put(
      "method",
      "generateReport"
    );
    Object[] args = new Object[0];
    jobDetail.getJobDataMap().put("args", args);
    CronTrigger trigger = new CronTrigger(
      "Income Report",
      "Report Generation"
    );
    try {
      trigger.setCronExpression(
        "0 0 12 ? * SUN"
      );
      Scheduler sched =
       StdSchedulerFactory.getDefaultScheduler();
      sched.addGlobalJobListener(new Listener());
      sched.scheduleJob(jobDetail, trigger);
      System.out.println(
        trigger.getNextFireTime()
      );
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public ServletConfig getServletConfig() {
    return null;
  }

  public void service(ServletRequest request,
                      ServletResponse response)
    throws ServletException, IOException {
  }

  public String getServletInfo() {
    return null;
  }

  public void destroy() {
  }
}
위에 나온 바와 같이, job에 전달할 필요가 있는 파라미터가 3개가 있다.

ejb: 엔터프라이즈 빈의 JNDI 이름.
method: 실제 호출되야 할 메소드의 이름.
args: 메소드의 인자로 전달되야 할 객체의 배열.
나머지는 Quartz의 일반 사용법과 다른 것이 없다. 간단하게 하기 위해 이 예제를 서블릿의 초기화 메소드에 위치시켰지만, 물론 애플리케이션의 어떠한 위치에 놓아도 상관 없다. Quartz를 성공적으로 실행하기 위해 웹 애플리케이션에 이 EJB를 등록할 필요가 있다. 일반적으로 web.xml 파일에 다음을 추가하면 된다.


  ejb/Report
  Session
  net.nighttale.scheduling.ee.ReportHome
  net.nighttale.scheduling.ee.Report
  ReportEJB

어떤 애플리케이션 서버(Orion과 같은)는 유저 쓰레드를 생성하기 위한 권한을 설정해야 할 필요가 있다. 그러므로 -userThread와 같은 옵션으로 실행해야 할 것이다.

지금까지 살펴 본 것이 Quartz의 엔터프라이즈 기능의 전부는 아니다. 그러나 처음 시작할 때 참고할 만할 것이다. 지세한 정보를 원한다면 Quartz's Javadocs 를 참고하기 바란다.

Web Services
Quartz는 현재 웹 서비스를 위해 내장된 지원은 없지만, XML-RPC 을 통하여 스케쥴러 인터페이스를 반출하는 플러그 인을 찾을 수 있다. 설치 절차는 간단하다. 시작하기 위해 플러그 인 소스를 Quartz 소스 폴더에 압축을 풀고 다시 빌드해야만 한다. 플러그 인은 Jakarta XML-RPC 라이브러리에 의존하므로, 그것이 클래스패쓰에 잡혀 있는지 확인해야 한다. 다음에 프로퍼티 파일에 아래의 내용을 추가한다.

org.quartz.plugin.xmlrpc.class = org.quartz.plugins.xmlrpc.XmlRpcPlugin
org.quartz.plugin.xmlrpc.port = 8080
이제 스케쥴러는 XML-RPC를 통해 사용할 수 있는 준비가 되었다. 이러한 방식으로 PHP나 Perl같은 다른 언어, 또는 RMI가 적합한 해결책이 아닌 분산 환경에서, Quartz의 기능 중의 일부를 사용할 수 있다.

요약
지금까지 자바에서 스케쥴링을 구현하는 두 가지 방법에 대해 살펴 봤다. Quartz는 매우 강력한 라이브러리지만, 간단한 요구사항에 대해서는 Timer API가 시간을 절약하고 시스템에 불필요한 복잡성을 피할 수 있게 해 준다. 스케쥴링이 필요한 작업이 그다지 많지 않고, 프로세스에서 실행시간이 잘 알려져 있을 경우(그리고 불변일 경우) Timer API를 사용할 것을 고려해라. 이러한 상황에서 시스템 중지나 다운 때문에 스케쥴링 작업들을 잃어버릴 것에 대해 걱정할 필요는 없다. 더욱 복잡한 요구사항을 위해서는 Quartz가 우아한 해결책이다. 물론 언제든지 Timer API를 기반으로 당신 자신의 프레임워크를 만들 수 있다. 그러나 이미 당신이 필요로 하는 모든 기능(그리고 그 이상의 기능)을 제공하는 훌륭한 해결책이 존재하는데 왜 그런 귀찮을 일을 하려는가?

한 가지 주목할 만한 이벤트는 IBM과 BEA이 제출한 "Timer for Application Servers" JSR 236 이다. 이 스펙은 스케쥴링에 관련된 표준 API가 부족한 J2EE 환경에서 타이머를 위한 API를 만드는 데 촛점을 두고 있다. IBM's developerWorks site에서 스펙에 관련된 보다 자세한 사항을 찾을 수 있다. 이 스펙은 봄에 일반 사용자에게 공개될 것이다.

Dejan Bosanac은 DNS 유럽의 소프트웨어 개발자 이다.
Posted by 알 수 없는 사용자
|

http://www-128.ibm.com/developerworks/kr/library/j-ajax1/

Ajax를 이용한 웹 애플리케이션 구현

난이도 : 중급

Philip McCarthy, 소프트웨어 개발 컨설턴트 , Independent Consultant

2005 년 9 월 20 일

페이지 리로드 사이클은 웹 애플리케이션 개발에 있어서 가장 큰 사용 장애이자 자바 개발자들에게는 심각한 도전 과제이다. Philip McCarthy가 혁신적인 동적 웹 애플리케이션 구현 방법을 소개한다. Ajax (Asynchronous JavaScript and XML)는 자바, XML, JavaScript가 혼합된 프로그래밍 기술이다. 자바 기반 웹 애플리케이션의 페이지 리로드 패러다임을 과감히 바꾼다.

Ajax (Asynchronous JavaScript and XML)는 클라이언트 측 스크립팅을 사용하는 웹 애플리케이션 개발 방식으로서 데이터를 웹 서버와 교환한다. 따라서 웹 페이지는 동적으로 업데이트 될 수 있다. 전체 페이지를 리프레시 하여 인터랙션 플로우에 영향을 끼치지도 않는다. Ajax를 사용하여 보다 풍부하고 동적인 웹 애플리케이션 사용자 인터페이스를 만들 수 있다. 원시 데스크탑 애플리케이션의 가용성에 접근한 풍부하고 동적인 웹 애플리케이션 사용자 인터페이스이다.

Ajax는 기술이 아니다. 오히려 패턴에 더 가깝다. Ajax는 많은 개발자들에게는 생소할지 모르지만, Ajax 애플리케이션을 구현한 모든 컴포넌트들은 여러 해 동안 존재했다. 2004년과 2005년에 Ajax 기술에 기반한 동적 웹 UI가 생겨나면서 주목을 받게 되었다. 대표적인 예로 Google의 Gmail과 Maps 애플리케이션 그리고 사진 공유 사이트인 Flickr을 들 수 있다. 이러한 UI들은 몇몇 웹 개발자들에 의해 "Web 2.0"으로 불릴 만큼 혁신적이었다. 이에 따라 Ajax 애플리케이션에 대한 인기도 하늘로 치솟았다.

이 시리즈를 통해 Ajax를 사용하여 애플리케이션을 개발할 때 필요한 모든 툴들을 소개하겠다. 우선 이 첫 번째 글에서는 Ajax의 개념을 설명하고, 자바 기반 웹 애플리케이션에 Ajax 인터페이스를 구현하는 기본적인 단계들을 설명하겠다. 코드 예제를 사용하여 서버측 자바 코드와 클라이언트측 JavaScript를 설명하겠다. 이것이 바로 동적인 Ajax 애플리케이션의 핵심축을 구성한다. 마지막으로 Ajax 방식의 단점과 Ajax 애플리케이션을 구현할 때, 반드시 고려해야 할 가용성 및 접근성 문제들도 설명하도록 하겠다.

더 나은 쇼핑 카트

Ajax를 사용하여 전통적인 웹 애플리케이션을 만들 수 있다. 페이지 부하를 줄여서 인터랙션을 간소하게 할 수 있다. 아이템들이 추가될 때 마다 동적으로 업데이트 되는 쇼핑 카트 예제를 설명하겠다. 온라인 스토어와 결합된 이 방식을 사용하면, 사용자들은 전체 페이지가 업데이트 될 때까지 기다리지 않고도 카트에 아이템들을 검색 및 추가할 수 있다. 이 글에 소개한 몇몇 코드는 이 쇼핑 카트 예제에만 국한된 것이지만, 여기에 사용된 기술들은 Ajax의 어떤 애플리케이션에나 적용될 수 있다. Listing 1은 쇼핑 카트 예제에서 사용되는 HTML 코드이다.


Listing 1. 쇼핑 카트 예제
<!-- Table of products from store`s catalog, one row per item -->
<th>Name</th> <th>Description</th> <th>Price</th> <th></th>
...
<tr>
  <!-- Item details -->
  <td>Hat</td> <td>Stylish bowler hat</td> <td>$19.99</td>
  <td>
    <!-- Click button to add item to cart via Ajax request -->
    <button onclick="addToCart(`hat001`)">Add to Cart</button>
  </td>
</tr>
...

<!-- Representation of shopping cart, updated asynchronously -->
<ul id="cart-contents">

  <!-- List-items will be added here for each item in the cart -->
 
</ul>

<!-- Total cost of items in cart displayed inside span element -->
Total cost: <span id="total">$0.00</span>




위로


Ajax 작동

Ajax 인터랙션은 XMLHttpRequest라고 하는 JavaScript 객체로 시작한다. 이름만 보아도 알겠지만 이것은 클라이언트측 스크립트가 HTTP 요청을 수행하도록 하고 XML 서버 응답을 파싱한다. Ajax 연구의 첫 번째 단계는 XMLHttpRequest 인스턴스를 만드는 것으로 시작한다. 이 요청(GET 또는 POST)에 사용되는 HTTP 메소드와 도착지 URL은 XMLHttpRequest 객체에서 설정된다.

이제 Ajax의 첫 번째 단어 a가 비동기식(asynchronous)을 의미한다는 것을 기억하라. HTTP 요청을 보내면 서버가 응답할 때 까지 기다릴 것을 브라우저에 요청하지 않아도 된다. 대신 사용자와 페이지 간 인터랙션에 지속적으로 반응하고, 서버의 응답이 도착하면 이를 다루도록 요청한다. 이를 위해서 XMLHttpRequest로 콜백 함수를 등록하고, XMLHttpRequest를 비동기식으로 실행한다. 컨트롤이 브라우저에 리턴되지만, 콜백 함수는 서버 응답이 도착하면 호출될 것이다.

자바 웹 서버에 요청은 다른 HttpServletRequest와 마찬가지로 도착한다. 요청 매개변수를 파싱한 후에 서블릿은 필요한 애플리케이션 로직을 호출하고, 응답을 XML로 직렬화 하고, 이를 HttpServletResponse에 작성한다.

다시 클라이언트로 돌아가서, XMLHttpRequest에 등록된 콜백 함수가 서버에서 리턴된 XML 문서를 처리하도록 호출된다. 마지막으로, 사용자 인터페이스는 서버로부터 온 데이터에 대한 응답으로 업데이트된다. 이때 JavaScript를 사용하여 페이지의 HTML DOM을 조작한다. 그림 1은 Ajax의 시퀀스 다이어그램이다.


그림 1. Ajax 시퀀스 다이어그램 - Sequence diagram of the Ajax roundtrip

위 그림은 고급 시퀀스 다이어그램이다. 각 단계를 자세히 설명하도록 하겠다. 그림 1에서 보면 Ajax의 비동기식 방식 때문에 시퀀스가 단순하지 않다.




위로


XMLHttpRequest 실행

Ajax 시퀀스의 시작 부분부터 설명하겠다. 브라우저에서 XMLHttpRequest를 생성 및 실행하는 Ajax 시퀀스의 시작 부분부터 설명하겠다. 불행히도 XMLHttpRequest를 만드는 방식은 브라우저마다 다르다. Listing 2의 JavaScript 함수는 부드럽게 진행되면서 현재 브라우저에 맞는 정확한 방식을 찾고, 사용 준비가 된 XMLHttpRequest를 리턴한다. 이것을 JavaScript 라이브러리에 복사하여 XMLHttpRequest가 필요할 때 사용하도록 한다.


Listing 2. 크로스 브라우저 XMLHttpRequest 구현하기

/*
 * Returns a new XMLHttpRequest object, or false if this browser
 * doesn`t support it
 */
function newXMLHttpRequest() {

  var xmlreq = false;

  if (window.XMLHttpRequest) {

    // Create XMLHttpRequest object in non-Microsoft browsers
    xmlreq = new XMLHttpRequest();

  } else if (window.ActiveXObject) {

    // Create XMLHttpRequest via MS ActiveX
    try {
      // Try to create XMLHttpRequest in later versions
      // of Internet Explorer

      xmlreq = new ActiveXObject("Msxml2.XMLHTTP");

    } catch (e1) {

      // Failed to create required ActiveXObject

      try {
        // Try version supported by older versions
        // of Internet Explorer

        xmlreq = new ActiveXObject("Microsoft.XMLHTTP");

      } catch (e2) {

        // Unable to create an XMLHttpRequest with ActiveX
      }
    }
  }

  return xmlreq;
}
  

나중에 XMLHttpRequest를 지원하지 않는 브라우저를 다루는 방법을 설명하겠다. 지금은 Listing 2의 새로운 newXMLHttpRequest가 언제나 XMLHttpRequest를 리턴하는 것으로 간주한다.

쇼핑 카트 시나리오 예제로 돌아가서, 사용자가 해당 카탈로그 아이템에서 addToCart() 버튼을 누를 때 마다 Ajax 인터랙션을 호출하도록 할 것이다. addToCart()라는 onclick 핸들러 함수는 Ajax 호출 시 카트의 상태를 업데이트 한다.(Listing 1). Listing 3에서 보듯 addToCart()가 해야 하는 첫 번째 일은 Listing 2에서 newXMLHttpRequest()함수를 호출하여 XMLHttpRequest의 인스턴스를 획득하는 것이다. 다음에는 콜백 함수를 등록하여 서버의 응답을 받는 것이다. (Listing 6).

요청으로 인해 서버상의 상태가 변경되기 때문에 HTTP POST가 이 일을 수행하도록 할 것이다. POST 를 통해 데이터를 보내려면 세 단계를 거쳐야 한다. 우선, 통신하고 있는 서버 리소스에 대한 POST에 연결한다. 이 경우 URL cart.do에 매핑된 서블릿이다. 그런 다음 XMLHttpRequest에 헤더를 설정한다. 이때 요청 내용은 폼 인코딩(form-encoded) 데이터라는 것을 언급한다. 마지막으로 폼 인코딩 데이터를 가진 요청을 바디(body)로서 보낸다.

Listing 3은 그 세 단계를 나타낸다.


Listing 3. Add to Cart XMLHttpRequest 실행

/*
 * Adds an item, identified by its product code, 
 * to the shopping cart
 * itemCode - product code of the item to add.
 */
function addToCart(itemCode) {

  // Obtain an XMLHttpRequest instance
  var req = newXMLHttpRequest();

  // Set the handler function to receive callback notifications
  // from the request object
  var handlerFunction = getReadyStateHandler(req, updateCart);
  req.onreadystatechange = handlerFunction;
  
  // Open an HTTP POST connection to the shopping cart servlet.
  // Third parameter specifies request is asynchronous.
  req.open("POST", "cart.do", true);

  // Specify that the body of the request contains form data
  req.setRequestHeader("Content-Type", 
                       "application/x-www-form-urlencoded");

  // Send form encoded data stating that I want to add the 
  // specified item to the cart.
  req.send("action=add&item="+itemCode);
}


지금까지 Ajax 설정의 첫 번째 부분을 보았다. 주로 HTTP 요청을 클라이언트에서 생성하여 실행하는 것이다. 다음에는 이 요청을 핸들할 때 사용하는 자바 서블릿 코드이다.




위로


서블릿 요청 핸들링

서블릿으로 XMLHttpRequest를 핸들하는 것은 브라우저에서 일반적인 HTTP 요청을 핸들하는 것과 비슷하다. 포스트 바디(body)에 보내진 폼 인코딩 데이터는 HttpServletRequest.getParameter() 호출로 얻을 수 있다. Ajax 요청은 애플리케이션의 일반 웹 요청과 같은 HttpSession에서 발생한다. 이것은 쇼핑 카트 시나리오 예제에 유용하다. 왜냐하면 사용자의 쇼핑 카트 상태를 JavaBean에 캡슐화 하고, 그 상태를 요청들 간 세션에 지속시키기 때문이다.

Listing 4는 Ajax 요청을 핸들하여 쇼핑 카트를 업데이트 하는 간단한 서블릿의 일부이다. Cart빈은 사용자 세션에서 검색되고, 상태는 요청 매개변수에 따라 업데이트 된다. Cart는 XML로 직렬화 되고, 그 XML은 ServletResponse에 작성된다. 응답 내용 유형을 application/xml로 설정하는 것이 중요하다. 그렇지 않으면 XMLHttpRequest는 응답 내용을 XML DOM으로 파싱할 것이다.


Listing 4. Ajax 요청을 핸들하는 서블릿 코드

public void doPost(HttpServletRequest req, 
  HttpServletResponse res)
      throws java.io.IOException {

  Cart cart = getCartFromSession(req);

  String action = req.getParameter("action");
  String item = req.getParameter("item");
  
  if ((action != null)&&(item != null)) {

    // Add or remove items from the Cart
    if ("add".equals(action)) {
      cart.addItem(item);

    } else if ("remove".equals(action)) {
      cart.removeItems(item);

    }
  }

  // Serialize the Cart`s state to XML
  String cartXml = cart.toXml();

  // Write XML to response.
  res.setContentType("application/xml");
  res.getWriter().write(cartXml);
}


Listing 5는 Cart.toXml() 메소드에서 만들어진 XML 예제이다. 매우 단순하다. cart 엘리먼트의 generated 애트리뷰트에 주목하라. System.currentTimeMillis()에서 만들어진 타임스템프이다.


Listing 5. Cart 객체의 XML 직렬화 예제
<?xml version="1.0"?>
<cart generated="1123969988414" total="$171.95">
  <item code="hat001">
    <name>Hat</name>
    <quantity>2</quantity>
  </item>
  <item code="cha001">
    <name>Chair</name>
    <quantity>1</quantity>
  </item>
  <item code="dog001">
    <name>Dog</name>
    <quantity>1</quantity>
  </item>
</cart>

다운로드 섹션의, 애플리케이션 소스 코드에서 Cart.java를 보았다면, 스트링(strings)들을 함께 붙여서 XML이 만들어 졌다는 것을 알 수 있을 것이다. 이 예제에서는 가능하지만, 자바 코드에서 XML을 만드는 방법 중 최악의 방법이다. 다음 글에서는 더 나은 방법을 소개하겠다.

이제 CartServletXMLHttpRequest에 어떻게 응답하는지 알았다. 이제 클라이언트측으로 리턴할 차례 이다. 여기에서 XML 응답이 페이지 상태의 업데이트에 어떻게 사용되는지를 보게 된다.




위로


JavaScript를 이용한 응답 핸들링

XMLHttpRequestreadyState 속성은 요청의 주기 상태를 부여하는 숫자 값이다. "초기화되지 않은 것"에는 0 부터 "완료된 것"에는 4 까지 붙을 수 있다. readyState가 변경될 때 마다 readystatechange 이벤트가 실행되고, onreadystatechange 속성을 통해 붙은 핸들러 함수가 호출된다.

Listing 3에서, getReadyStateHandler()함수가 호출되어 핸들러 함수를 만드는 방법을 보았다. 이 핸들러 함수는 onreadystatechange속성으로 할당된다. getReadyStateHandler()는 함수들이 JavaScript의 첫 번째 객체라는 것을 활용한다. 다시 말해서 함수들은 다른 함수들에 대한 매개변수가 될 수 있고, 다른 함수들을 생성 및 리턴할 수 있다. XMLHttpRequest가 완료되었는지 여부를 검사하여 그 핸들러 함수에 대한 XML 응답으로 전달하는 함수를 리턴하는 것은 getReadyStateHandler()의 역할이다. Listing 6은 getReadyStateHandler()의 코드이다.


Listing 6. getReadyStateHandler() 함수

/*
 * Returns a function that waits for the specified XMLHttpRequest
 * to complete, then passes its XML response
 * to the given handler function.
 * req - The XMLHttpRequest whose state is changing
 * responseXmlHandler - Function to pass the XML response to
 */
function getReadyStateHandler(req, responseXmlHandler) {

  // Return an anonymous function that listens to the 
  // XMLHttpRequest instance
  return function () {

    // If the request`s status is "complete"
    if (req.readyState == 4) {
      
      // Check that a successful server response was received
      if (req.status == 200) {

        // Pass the XML payload of the response to the 
        // handler function
        responseXmlHandler(req.responseXML);

      } else {

        // An HTTP problem has occurred
        alert("HTTP error: "+req.status);
      }
    }
  }
}

HTTP 상태 코드

Listing 6에서, XMLHttpRequest의 상태 속성(status property)은 요청이 성공적으로 완료되었는지를 보기 위해 테스트된다. status에는 서버 응답에 대한 HTTP 상태 코드가 포함되어 있다. GETPOST 요청을 수행할 때 200(OK)이 아닌 어떤 코드도 에러이다. 서버가 리다이렉트 응답(예를 들어, 301 또는 302)을 보내면 브라우저는 리다이렉트를 따라 새로운 위치에서 리소스를 가져온다. XMLHttpRequest는 리다이렉트 상태 코드를 볼 수 없다. 또한 브라우저(Cache-Control: no-cache)는 헤더를 모든 XMLHttpRequest에 추가하여 클라이언트 코드가 (변경되지 않은) 304 서버 응답을 다루지 않도록 한다.

getReadyStateHandler()에 대하여

특히 JavaScript 읽기에 익숙하지 않다면 getReadyStateHandler()는 비교적 복잡한 코드이다. 대신 여러분의 JavaScript 라이브러리에 이 하수를 추가하여 XMLHttpRequest의 내부를 다루지 않고도 Ajax 서버 응답들을 핸들할 수 있다. 중요한 것은 자신의 코드에서 getReadyStateHandler()를 사용하는 방법을 이해하는 것이다.

Listing 3에서 getReadyStateHandler() 는 다음과 같이 호출된다. handlerFunction = getReadyStateHandler(req,updateCart). 이 경우 getReadyStateHandler()에 의해 리턴된 함수는 변수 req에 있는 XMLHttpRequest가 완료되었는지를 확인한 다음 응답 XML이 있는 updateCart 함수를 호출한다.

카트 데이터 추출하기

Listing 7은 updateCart()의 코드이다. 이 함수는 DOM 호출들을 사용하여 쇼핑 카트 XML 문서를 조사하여 새로운 카트 내용을 반영하여 웹 페이지를 업데이트 한다. (Listing 1) XML DOM에서 데이터를 추출하는데 사용된 호출에 집중해보자. cart 엘리먼트에 대한, generated 애트리뷰트는 Cart가 XML로 직렬화 되었을 때 만들어진 타임스템프이다. 이것은 새로운 카트 데이터가 옛 카트 데이터에 반영되지 않았는지를 확인한다. Ajax 요청은 근본적으로 비동기식이라서 시퀀스에서 도착하는 서버 응답에 대한 세이프가드를 체크한다.


Listing 7. 카트 XML 문서를 반영하여 페이지 업데이트하기

function updateCart(cartXML) {

 // Get the root "cart" element from the document
 var cart = cartXML.getElementsByTagName("cart")[0];

 // Check that a more recent cart document hasn`t been processed
 // already
 var generated = cart.getAttribute("generated");
 if (generated > lastCartUpdate) {
   lastCartUpdate = generated;

   // Clear the HTML list used to display the cart contents
   var contents = document.getElementById("cart-contents");
   contents.innerHTML = "";

   // Loop over the items in the cart
   var items = cart.getElementsByTagName("item");
   for (var I = 0 ; I < items.length ; I++) {

     var item = items[I];

     // Extract the text nodes from the name and quantity elements
     var name = item.getElementsByTagName("name")[0]
                                               .firstChild.nodeValue;
                                               
     var quantity = item.getElementsByTagName("quantity")[0]
                                               .firstChild.nodeValue;

     // Create and add a list item HTML element for this cart item
     var li = document.createElement("li");
     li.appendChild(document.createTextNode(name+" x "+quantity));
     contents.appendChild(li);
   }
 }

 // Update the cart`s total using the value from the cart document
 document.getElementById("total").innerHTML = 
                                          cart.getAttribute("total");
}


이것으로 Ajax 실행이 완료되었다. 물론 여러분은 웹 애플리케이션이 실행되는 것을 보고 싶을 것이다.(다운로드 참조). 이 예제는 매우 간단하고 환경 범위도 다양하다. 예를 들어 서버측 코드를 추가하여 카트에서 아이템을 제거했지만, UI에서 접근하는 방법은 없다. 애플리케이션의 기존 JavaScript 코드에 이러한 기능을 구현하기 바란다.




위로


Ajax를 사용할 때의 문제점

다른 기술과 마찬가지로 Ajax도 허점이 많이 있다 . 최근에는 쉬운 솔루션이 없다는 것이 문제되고 있지만 Ajax가 성숙해 가면서 개선될 전망이다. 개발자 커뮤니티가 Ajax 애플리케이션 개발에 경험을 많이 쌓고 최선의 방법과 가이드라인이 문서화 될 것이다.

XMLHttpRequest의 기능

Ajax 개발자들이 직면한 가장 큰 문제들 중 하나는 XMLHttpRequest를 사용할 수 없을 경우이다. 대다수의 브라우저들은 XMLHttpRequest를 지원하지만, 어떤 사람들은 그러한 브라우저를 사용하지 않거나 브라우저 보안 설정 때문에 XMLHttpRequest를 사용할 수 없는 경우도 있다. 기업 인트라넷에 전개할 웹 애플리케이션을 개발한다면 어떤 브라우저가 지원되는지를 지정하고, XMLHttpRequest를 언제나 사용할 수 있도록 해야 한다. 하지만 공용 웹상에 전개한다면 XMLHttpRequest를 사용할 수 있겠지만 오래된 브라우저, 장애인용 브라우저, 핸드헬드용 경량 브라우저에는 사용할 수 없다.

따라서 애플리케이션을 "부드럽게 강등시켜서" XMLHttpRequest지원이 안되는 브라우저에서 작동하도록 해야 한다. 이 쇼핑 카트 예제에서 애플리케이션을 강등시키는 최선의 방법은 Add to Cart 버튼이 폼 제출을 수행하도록 하는 것이다. 페이지를 리프레시하여 카트의 업데이트를 반영한다. 페이지가 로딩되면 Ajax 작동이 JavaScript를 통해 페이지에 추가될 수 있다. JavaScript 핸들러 함수를 각 Add to Cart 버튼에 붙인다. 이는 어디까지나 XMLHttpRequest가 가능할 때이다. 또 다른 방법은 사용자가 로그인할 때 XMLHttpRequest를 탐지하는 것이다. 그리고 나서 Ajax 버전의 애플리케이션을 실행하든지 아니면 폼 기반 버전을 실행한다.

가용성 문제

Ajax 애플리케이션에 관련한 몇 가지 가용성 문제들이 있다. 예를 들어, 인풋이 등록되었다는 것을 사용자에게 알리는 것이 중요할 수 있다. 왜냐하면 보통의 피드백 방식인 모래시계 커서와 돌아가는 브라우저 "쓰로버(throbber)"가 XMLHttpRequest에 적용되지 않기 때문이다. 한 가지 방법은 Submit 버튼을 "Now updating..."메시지로 대체시키는 것이다. 이렇게 해서 사용자들이 응답을 기다리는 동안 버튼을 반복적으로 클릭하지 않도록 한다.

또 다른 문제는 사용자가 그들이 보고 있는 페이지의 일부가 업데이트 되었다는 것을 인식하지 못하는 것이다. 다양한 시각 기술을 사용하여 업데이트된 페이지 부분을 그려서 이 문제를 해결할 수 있다. 페이지 업데이트와 관련한 또 다른 문제로는 브라우저의 뒤로가기(back) 버튼이 "비활성화(breaking)"되는 것, 주소 바의 URL이 페이지의 전체 상태를 반영하지 않는 문제, 북마킹이 안되는 경우 등이 있다. (참고자료)

서버 부하

폼 기반의 Ajax UI를 구현하면 서버에서 요청 수가 상당히 많이 늘어난다. 예를 들어, 사용자가 검색 폼을 제출할 때 Google 웹 검색이 서버에 한번의 히트를 일으킨다고 해보자. 하지만 사용자의 검색어를 자동 완성하는 Google Suggest는 서버에 여러 요청들을 보낸다. Ajax 애플리케이션을 개발할 때 서버에 얼마나 많은 요청들을 보낼 것인지, 그리고 서버 부하가 어떻게 될 것인지를 알아야 한다. 또한 클라이언트에 요청을 버퍼링하고 클라이언트에 서버 응답을 캐싱하여 서버 부하를 완화시킬 수 있다. 또한 Ajax 웹 애플리케이션을 설계하여 가능한 많은 로직들이 굳이 서버에 연결되지 않고도 클라이언트에서 수행될 수 있도록 한다.

비동기식 다루기

XMLHttpRequest가 발송되는 순서대로 완료될 것이라는 보장이 없다. 실제로 그것까지 염두해 가며 애플리케이션을 설계하지 않는다. 쇼핑 카트 예제에서 마지막으로 업데이트 된 타임스탬프는 새로운 카트 데이터가 오래된 데이터에 적용되었는지를 확인할 떄 사용되었다. (Listing 7). 이것은 쇼핑 카트 시나리오에서 매우 기본적인 방식이지만 다른 경우에는 그렇지 않다. 설계할 때 비동기식 서버 응답을 어떻게 처리할 것인지를 생각하라.




위로


결론

Ajax의 기본 원리와 Ajax 인터랙션에 참여하는 클라이언트와 서버측 컴포넌트에 대해서 설명했다. 이들 모두 자바 기반 Ajax 웹 애플리케이션을 만드는 요소들이다. 또한 Ajax 접근방식에 따르는 고급 디자인 문제들도 이해해야 한다. 성공적인 Ajax 애플리케이션을 만들기 위해서는 UI 디자인부터 자바 스크립트 디자인, 그리고 서버측 아키텍쳐에 이르기까지 유기적인 접근 방식이 필요하다. 하지만 이제 여러분도 Ajax에 대한 기본적인 지식이 있으니 문제없다.

이 글에서 설명한 기술들을 사용하여 대형 Ajax 애플리케이션을 작성할 때의 복잡함에 압도될지 모르겠다. 하지만 좋은 소식이 있다. Struts, Spring, Hibernate 같은 프레임웍이 저급의 Servlet API와 JDBC에서 탈피하여 추상 웹 애플리케이션으로 진화했기 때문에 툴킷들을 사용하면 Ajax 개발이 쉬워진다. 이들 중 몇몇은 클라이언트 측에만 해당된다. 페이지에 시각 효과를 쉽게 추가할 수 있고, XMLHttpRequest의 사용법도 간단해진다. 서버측 코드에서 Ajax 인터페이스를 자동으로 생성하는 방법도 제공한다. Ajax 개발 방식이 고급화 되었다. .

Ajax 커뮤니티는 빠르게 변화하고 있고 정보도 풍부하다. 이 글을 다 읽었다면 참고자료 섹션을 참조하기 바란다. Ajax 또는 클라이언트측 개발이 처음인 사람들에게 권한다. 또한 시간을 내어 샘플 코드도 공부하기 바란다.

다음에는 XMLHttpRequest API를 보다 자세히 설명하고 JavaBean에서 XML을 쉽게 생성하는 방법도 설명하겠다. JSON (JavaScript Object Notation)도 소개할 테니 기대하라.



필자소개

Philip McCarthy: Independent Consultant 소프트웨어 개발 컨설턴트

Posted by 알 수 없는 사용자
|

JSON-PRC-java 적용하기

java 2006. 2. 25. 22:35

http://kingori.egloos.com/1673534

ajax의 광풍이 몰아닥칠때, XMLHttpRequest 와 함께 이름이 여기저기서 보이던 JSON-RPC , 그 중에서도 JSON-RPC-java 를 직접 사용해보았다.

ajax 는 분명히 손이 많이 가지만 한번 적용해보고 싶었고, 또한 적용하지 않으면 상당히 우울한 부분이 마침 눈에 띄어서 옳다꾸나 하고 냉큼 써먹어봤다.

사용한 부분은 대량의 입력을 받아 submit 하기 직전에 대략 3개~4개의 column 들에 대한 validation 이었다. 당연히 server-side 에서의 validation 이다.
보통 이럴경우 struts의 form validation 을 쓰는데, 이런 경우 form 이 dynamic 하게 늘어나고 parameter 도 array 로 날라다니고 해서 아주 골치아프다.
사용자 입장에서는 대략 40개의 입력값을 줬는데, submit 했다가 validation에서 뻑나면 다시 일일이 입력해줘야 하고, 이걸 막기 위해선 개발자가 40개의 입력값을 받아다가 어디에 저장해놓고, validation 해서 뻑나면 다시 redirecting 시켜준 후 저장해 놓은 입력값을 다시 form 에 일일이 박아다 주고. 관두고 말지!
이때 ajax 를 통한 validation 을 쓰면 아주 깔끔하게 처리할 수 있다. 물론 iframe 을 사용해서 매우 아름답지 못한 방법으로 할 수도 있지만 느리고, 또 꼬이기 십상이다. 내 경우 단순결과를 얻기위해 iframe으로 10번정도의 request를 날려야하니 이것또한 우울하다.

XMLHttpRequest 를 쓸 수도 있는데 굳이 JSON-RPC 를 사용한 이유는, 단순 validation 이기 때문에 값만 받아오면 되는데 ( true|false ) , 요것때문에 xml 페이지 처리하고 값 받아오는것을 만드는것이 귀찮고, 또 귀파는데 삽자루 들이미는 격이라서 아얘 객체로 왔다리 갔다리 하는 JSON-RPC 로 눈을 돌렸다.

홈페이지에 나와있는 설명을 번역하자면

  • JSON (JavaScript Object Notation) 은 C, C++, C#, Java, JavaScript, Perl, TCL 등의 언어와 호환가능한 lightweight 한 데이타교환 format이다.
  • JSON-RPC 는 XML-RPC 과 유사한 단순한 remote procedure call protocol 이지만, XML 대신 JSON format 을 사용한다.
  • 페이지 reloading 없이 원격 method 호출을 위해 XMLHttpRequest object (IE의 경우는 MSXML ActiveX ) 가 쓰인다.
  • JSON-RPC-Java 는 JSON-RPC protocol 을 Java로 작성한 것이다

결국 XMLHttpRequest 위에 올라가서, 원격 메소드 호출 후 결과값을 JSON 으로 가져다 주는 것이니 나의 경우와 같이 결과값만 받는 경우 XMLHttpRequest 직접 구현보다 훨씬 편하다.

간단히 실제 적용내용을 정리하면
  1. web application 에 사용할 directory 를 만든다. ( 기존 application 에 적용할 때는 필요없음).
  2. web application root 밑에 WEB-INF 디렉토리를 만들고 아래와 같이 web.xml 을 작성.

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN" "http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
    <web-app>
    <directory-servlet>none</directory-servlet>
    <display-name>jsonrpc</display-name>
    <!-- Action Servlet Configuration -->
    <servlet>
    <servlet-name>JSONRPCServlet</servlet-name>
    <servlet-class>com.metaparadigm.jsonrpc.JSONRPCServlet</servlet-class>
    </servlet>
    <servlet-mapping>
    <servlet-name>JSONRPCServlet</servlet-name>
    <url-pattern>/json</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
    <welcome-file>index.html</welcome-file>
    </welcome-file-list>
    </web-app>

  3. library 디렉토리에 json.jar 복사
  4. js 위치한 곳에 jsonrpc.js 복사
  5. 환경설정끝! jsp 작성


내가 만든 테스트 페이지의 소스와 jsp 는
<%@ page contentType="text/html;charset=EUC-KR"
import
="kingori.MessageGetter" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<jsp:useBean id="JSONRPCBridge"
class="com.metaparadigm.jsonrpc.JSONRPCBridge" scope="session"/>
<%
JSONRPCBridge.registerObject("messageGetter", new MessageGetter() );
%>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<title>JSONRPCJAVA test</title>
<script type="text/javascript" src="jsonrpc.js"></script>
<script type="text/javascript">
jsonrpc = new JSONRpcClient("json");

function getMessage()
{
document.getElementById("messageSpan").innerHTML =
jsonrpc.messageGetter.getMessage();
}
</script>
</head>
<body>
<p>Please tell me something:
<input type="button" onclick="getMessage();" value="click"/></p>
<p>message:<span id="messageSpan"></span></p>
</body>
</html>

package kingori;

public class MessageGetter
{
public String getMessage()
{
return "hohoho!";
}
}


위와 같다.

jsp 에서의 호출부분이 매우 단순하다. 만약 object에 뭔가를 넣어줘야 할 경우 javascript 단에서 처리하면 된다. 위의 코드라면
jsonrpc.messageGetter.getMessage( value);

이런 식이 될 듯.

한가지 문제는 validator 자체가 http request , http response 를 거치지 않고 JSONRPC 가 그냥 불러버리기 때문에 "로긴한 사용자 이름을 가져와서 세팅하기" 등등의 부분에서 꼼수가 필요하지 않을까 생각한다. 이것은 jsp 단 코드에서 해결해줘야 하지 않을까 생각한다. (아직 해보진 않았지만.)

꽤 깔끔하고, XMLHttpRequest 일일이 구현하는것보다 간편하다. 대신 만약 xml document 전체를 뿌려준다거나 할 경우는 차라리 XMLHttpRequest 로 구현하는게 더 편하지 않을까 싶다.

내 결론은
  • 달랑 값만 필요 => JSONRPC-JAVA
  • 웹단 HTML 도 같이 가져오고싶다 => XMLHttpRequest


정도. 뭐가 되었던간에 예전같이 iframe 으로 장난하던 것보다 비할 데 없이 편하고, 보기도 좋다.
Posted by 알 수 없는 사용자
|

K사 프로젝트 시 발생했던 문제.
maxThread 의 갯수도 문제였지만 이제 생각해보면 가장 결정적인 문제는 fd limited 값이다.
솔라리스임에도 설정이 1024로 잡혀있던 걸로 기억한다. 계정마다 다를 수도 있겠지만 한 서버에 was, webserver, db 를 사용했었고 was 서버의 limited 가 작으니... thread 생성시에도 max 개수만큼 생성되지 않았으리라 생각된다.
또한 application 에서 connection close 처리를 잘못했었으니... dbconnection pool 개수도, db 세션수도 문제가 되었을 것이다.

http://cafe.naver.com/deve.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=482


안녕하십니까..
늘 좋은 자료 훔쳐만 가던 라자로 라고 합니다.. ^^;;
다들 더운 날씨에 코딩 하시느라 고생많으시죠?
점심시간도 지났으니.. 팥빙수라도 하나 사드세요.. ^^*

제가 요즘 톰캣과 사투를 벌이고 있는데요..
여기 저기 검색도 하고 자문도 구하면서..
사이트 장애분석 및 해결책과 서버의 성능을 높이기 위한 방안을 여러모로 검토중에 있습니다..

현재 웹서버가 2대, 파일 서버 1대, DB서버 1대 총 4대의 서버로 서비스 중이구요
현재 사이트 방문고객이 하루 평균 5만명 가량입니다.
우선 서버 구성은 다음과 같습니다.
Windows 2000 Server
oracle 9i
iis5.0 + tomcat4.1.29
jdk 1.4.2.03

가장 큰 문제점은 일정 시간이 지난후에
톰캣에서 다음과 같은 에러가 발생하면서 hang이 걸린다는 것이구요..
심각: All threads are busy, waiting. Please increase maxThreads or check the ser
vlet status150 150
(thread 수를 150으로 늘려봤는데도 여전하더군요..)
이상한 점은 웹서버가 2대인데 유독 한쪽 서버에서만 발생 한다는 것입니다..
똑같은 소스로 같은 서비스를 하고 있는데 말입니다..
(쿠키와 db를 이용해서 세션 클러스터링을 하고있습니다.)

그리고 apache DBCP를 사용하고 있는데..
다음과 같은 에러가 종종 발생을 하고 있습니다..
DBCP borrowObject failed: java.sql.SQLException: IO 예외 상황: Connection refuse
d(DESCRIPTION=(ERR=12518)(VSNNUM=153092352)(ERROR_STACK=(ERROR=(CODE=12518)(EMFI
=4))(ERROR=(CODE=12560)(EMFI=4))))
DBCP borrowObject failed: java.sql.SQLException: IO 예외 상황: Connection refuse
d(DESCRIPTION=(ERR=12518)(VSNNUM=153092352)(ERROR_STACK=(ERROR=(CODE=12518)(EMFI
=4))(ERROR=(CODE=12560)(EMFI=4))))


프로그램 자체에 버그가 있다면 이것을 찾는게 중요하겠지만..
혹시나 해서 레진으로 바꿔 볼까했는데..
대부분 model2 + jstl 로 짜여진 로직들이 resin 에서 돌릴려니..
조금씩 수정을 해야 하더군요.. ㅜㅜ
수많은 jsp page들을 바꿔 줄 생각하니..
차라리 톰캣을 잘 tuning 하는게 빠르겠단 생각이 들더군요

현재 jmeter + jprofiler 을 이용해서 stress test 도 진행중이지만..
뚜렷한 답은 안나오네요..

이러한 장애에 대해서 진단방법, 접근방법, 해결방법등 어떠한 형태이든지..
여하한의 조그마한 조언이라도 해주시면 감사하겠습니다.. ^^*


newoverguy  2대의 웹서버가 있는데, 한대는 iis+tomcat, 다른한대는 tomcat 이렇게 구성되어있는지요? 그리고, hang이 걸리는 서버는 iis+tomcat이 설치되어있는 서버인지 아니면 후자 인지 ? 궁금합니다. 주위분에게 조언을 구해볼려 했더니, 어떻게 구성되어 있는지 알아야 물어 볼꺼 같아서요~ 08/11 16:25 
 
 라자로  앗 우선 관심 가져 주셔서 너무나 감사합니다.. (__)
두대의 웹서버가 모두 iis+tomcat 으로 구성되어 있구요
네임 서버에서 로드 밸런싱을 하고 있습니다..
다시 한번 관심 가져주셔서 넘 감사합니다.. ^^* 08/11 18:43 
 
 아빠곰  제 생각에는 첫 번째 에러인 경우 접속 하고 있는 유저들이 150명을 초과하기 때문에 발생하는 문제인거 같아요... tomcat의 client max수를 좀 더 늘려주셔야 할 듯 하고요...


DBCP에서 나오는 에러인 경우 디비가 연결이 끊기면 저런 메시지가 나오는데, 디비가 죽지 않았다면 네트워크 상태를 함 체크해 보시면 되겠네요...
도움이 되었으면 합니다. 08/11 20:13 
 
 newoverguy  아빠곰회원님 좋은 답변감사합니다. ^^ 회원님 블로그도 둘러보았는데, 보아양채널이 좋던데요~ 보아짱ㅋ ^^ 08/11 21:53 
 
 라자로  아빠곰님 좋은 답변 감사합니다.. ^^;
우선 첫번째 에러인 경우에는 동접자하고는 상관이 없습니다.
접속자가 별로 없을때도 발생하거든요.
그리고 max 쓰레도 갯수를 기본75개에서 두배인 150개로 늘려줬는데..
오히려 에러 발생 빈도는 더 높아졌답니다..
내일은 호스팅 업체에 전화해서 네트웍상태에 대해서 논의를 좀 해봐야 하겠네요.. L4, 스위치 장비에 문제가 있을지도 모르겠군요.. 08/11 22:11 
 
 비실비실  장비에서 TCP/IP 관련 셋팅이 제대로 되어 있는지, 장애가 발생했을 경우 DB의 상황은 어떤지.. 락이 걸려 있는지.. 장애가 발생 했을 경우 페이지의 수행 속도를 체크 해 봐야 합니다. 이같은 경우 대부분 어플리케이션이 잘못 짜여져 있거나 DB의 이상으로 여겨집니다. Oracle의 Thread 설정은 어떻게 되어 있는지 체크 해 보시고 Oracle이 튜닝 되어 있는 지도 확인 해봐야 하구요..
max thread 변경 해 봐야 도움이 안될듯 합니다. 08/12 09:35 
 
 비실비실  전체적인 장비의 튜닝, 오라클 튜닝 후 개선이 안된다면 어플리케이션을 검토해 봐야 겠지요.. 윈도 장비를 어떻게 튜닝 해야 할 지는 잘 모르겠습니다. 아파치 사이트에 가면 있을 지도.. 모르겠네요.. 08/12 09:39 
 
 비실비실  서비스가 잘 되다가 요즘 갑자기 문제가 생긴 건지.. 아님 최근에 오픈했는지도 궁금하네요.. 08/12 09:47 
 
 라자로  비실비실님 관심 가져주셔서 넘 감사합니다.. ^^
첫번째 질문 : TCP/IP 관련 셋팅이 제대로 되어 있는지에 대한 질문은 잘 모르겠습니다.. ㅜㅜ 호스팅 업체에 서버 관리를 해주고 있거든요.. 전화해서 확인해 보겠습니다..
두번째 질문 : 장애 발생시 DB확인해 본 결과 락이 걸린 부분은 없었습니다.
세번째 질문 : 오라클 튜닝은 현재 진행중이며.. 아직은 튜닝이 완벽히 이루어 지지 않았습니다.
네번째 질문 : 서비스 시작한지 2달 가량 되어가고 있습니다. 초반에는 잦은 update 로 톰캣을 자주 재시작 햇었으므로.. 초반에는 서비스가 잘 이루어 졌다고 단언 할 수는 없는 입장입니다..


다시한번 관심 가져 주셔서 감사합니다.. ^^;; 08/12 10:04 
 
 라자로  아 참고로.. jai 를 사용하고 있는데 이부분에 버그가 있지 않을까 하는 의심이 가고 있어서 직중적으로 stress test를 해볼 생각입니다..
장애원인과 해결 방안이 찾아지는 대로 정리해서 공유해야겠죠..
이렇게 고생하는것도 다 내공이 싸여가는 걸로 생각하면서 오늘 하루도 톰캣과 사투를 벌여볼 생각입니다.. ^^* 08/12 10:08 
 
 tmhuh  DBCP에서 DB 커넥션이 끊어지는 현상은 저도 겪어본 바가 있는데 저와 같은 경우인지는 잘 모르겠지만.. 제가 해결한 방법을 적어보겠습니다.


일단 커넥션이 끊어지는 이유는 DB 서버를 재시작하거나 하면 DBCP에서 그걸 커넥션이 끊어진 걸로 생각하고 그 이후에는 DB 서버가 살아있음에도 불구하고 계속 connection refuse 에러가 나더라구요.
그래서 톰캣 서비스를 재시작해야만 다시 DB 커넥션이 되구요.


이에 대한 해결책은 DBCP 설정 프로퍼티에 validationQuery가 있는데 이걸 select 1 from dual 과 같이 더미 쿼리로 설정해 놓으면 이 문제가 해결되더군요.. 08/12 11:10 
 
 라자로  으음.. 설정 프로퍼티에 그런게 있었군요..
tmhuh님 정보주셔서 넘 감사합니다..
역시 배워갈게 많은분인것 같다던 제 예감이 딱 들어 맞았네요..
바로 적용해 보도록 하겠습니다.. ^^* 08/12 11:40 
 
 zmzizi  예전에 코스닥에 파견나가서 일할때, 코스닥도 L4 장비를 쓰던데, L4 장비의 로드밸런싱이 다소 좀 이상했던 걸로 기억나네요... 전체 접속인원/서버수 로 분배하는 방식이 아니라 특정 ip 대역으로 나누더라구요.
그게 코스닥에서의 L4 장비 설정의 문제인지 아니면 L4 장비 로드밸런싱의 특징인지는 잘 모르겠지만, 코스닥에서도 웹서버 3대 중 특정 1대만 커넥션 풀이 나는 경우가 잦았었습니다. 비슷한 현상인듯 싶어서 말씀드립니다. 08/12 14:43 

'WAS' 카테고리의 다른 글

AIX - was의 병목현상  (0) 2008.06.04
jeus 4.0 - DATA-SOURCE 패스워드 암호화  (0) 2008.06.04
Apache 가상호스트 설정  (0) 2006.02.21
tomcat4 가상호스트 설정  (0) 2006.02.21
Posted by 알 수 없는 사용자
|

Apache 가상호스트 설정

WAS 2006. 2. 21. 14:21

http://okjsp.pe.kr/lecture/lec01/vhost01.html

Apache 가상호스트 설정

kenu
2002-03-19 12:27오전

이 문서는 apache 서버에서 가상호스트를 사용해서 여러개의 도메인을 사용하도록 하는 방법을 설명합니다. 파일은 apache 의 conf 디렉토리에 있는 httpd.conf 파일만을 변경합니다. apache의 기본적인 설치는 잘 되어 있다고 가정하고 다른 도메인을 붙이는 법을 설명합니다. 또한 DNS 서버 설정에 관해서는 이 문서에서 설명하지 않겠습니다. 확장강좌는 tomcat 4.0.x 와 연동하는 강좌입니다.
강좌환경
OS:Solaris 2.8
Apache: 1.3.20
설치디렉토리: /usr/local/apache
		


주의할 점은 비단 아파치만 해당되는 것이 아닙니다만, 버전마다 환경설정하는 것이 차이가 있을 경우가 많기 때문에 가능하면 패키지에 포함된 Installation 문서를 표준으로 설치하시기 바랍니다.

Tomcat 과 연동하기 위해서는 DSO 모듈을 사용하는 mode 로 설치가 되어야 합니다. 1.3.22 버전부터는 이 모드로 설치가 되지만 1.3.21 이전 버전은 아래처럼 소스파일을 이용해서 설치를 해야됩니다. 가능하면 1.3.23 이후 버전을 다운받아서 설치하십시오. 아래 명령어를 skip 할 수 있으니까요. 아래 명령어는 unix 계열에서만 사용하는 명령어입니다. windows 의 경우 패키지에 포함된 문서를 보세요. \ 는 줄이 길어질 때 엔터를 쳐도 실행이 안되도록 하는 이음 문자입니다.
apache 컴파일 명령줄
./configure --prefix=/usr/local/apache \ 
            --enable-shared=max \
            --enable-rule=SHARED_CORE \ 
            --enable-module=so
		


conf 디렉토리에 있는 httpd.conf 파일을 열어서 아래 부분에 추가할 도메인과 그에 따른 부가적인 정보를 입력합니다.
설치경로
...
### Section 3: Virtual Hosts
#
# VirtualHost: If you want to maintain multiple domains/hostnames on your
# machine you can setup VirtualHost containers for them. Most configurations
# use only name-based virtual hosts so the server doesn't need to worry about
# IP addresses. This is indicated by the asterisks in the directives below.
#
# Please see the documentation at <URL:http://www.apache.org/docs/vhosts/>
# for further details before you try to setup virtual hosts.
#
# You may use the command line option '-S' to verify your virtual host
# configuration.

#
# Use name-based virtual hosting.
#
NameVirtualHost *

#
# VirtualHost example:
# Almost any Apache directive may go into a VirtualHost container.
# The first VirtualHost section is used for requests without a known
# server name.
#
#<VirtualHost *>
#    ServerAdmin webmaster@dummy-host.example.com
#    DocumentRoot /www/docs/dummy-host.example.com
#    ServerName dummy-host.example.com
#    ErrorLog logs/dummy-host.example.com-error_log
#    CustomLog logs/dummy-host.example.com-access_log common
#</VirtualHost>
    <VirtualHost *>
    ServerName kpanet.or.kr
    DocumentRoot /home/jakarta/kpanet402/webapps/ROOT
    ServerAlias kpanet.or.kr *.kpanet.or.kr
    </VirtualHost>

    <VirtualHost *>
    ServerName ycpa.or.kr
    DocumentRoot /home/vhost/ycpahome/ROOT
    ServerAlias ycpa.or.kr *.ycpa.or.kr
    </VirtualHost>

    <VirtualHost *>
    ServerName pharmsnet.com
    DocumentRoot /home/jakarta/pmstemp
    ServerAlias pharmsnet.com *.pharmsnet.com
    </VirtualHost>

Include /home/jakarta/kpanet402/conf/jk/mod_jk.conf
        


제가 사용하는 httpd.conf 파일의 일부입니다. httpd.conf 의 마지막 부분에 가상호스트 섹션이 있고, 이 부분에 여러개의 도메인을 세팅하면 됩니다. 위의 예에서는 kpanet.or.kr, ycpa.or.kr, pharmsnet.com 이렇게 3개의 도메인이 설정되어있습니다. 각각의 도메인마다 ROOT 디렉토리가 다르게 설정되어있습니다. 각각의 태그 안에 있는 DocumentRoot 의 값이 다름을 확인하실 수 있을 겁니다. 그리고 ServerAlias 는 2차 도메인을 포함합니다. 대표적인 것이 www.kpanet.or.kr 에서의 www 가 되겠죠. 모든 것을 통틀어 wildcard 문자인 * 으로 처리하도록 합니다. 2차 도메인마다 다르게 디렉토리를 지정하는 법은 설명하지 않겠습니다. 이에 관한 문서는 아래 링크에서 소개한 사이트를 찾아보시는 것이 더 나을 것이라 생각됩니다.

마지막줄의 Include 는 tomcat과 연동하는 모듈인 mod_jk.conf 파일의 경로를 적어놓은 것입니다. apache만으로 서비스할 경우에는 필요없습니다. 일단 이렇게 가상호스트 설정을 마쳤으면 /usr/local/apache/bin/apachectl restart 로 재시동합니다.

브라우저를 열어서 설정을 확인해보시기 바랍니다. 아래의 링크에서 더 자세한 정보를 찾아보시기 바랍니다.

관련 사이트
http://www.apache.org/docs/vhosts/
아파치 가상호스트 설정에 대한 공식문서
http://www.apache.kr.net/documents/vhost-story.html
정관진님의 아파치 가상호스트 설정에 대한 한글문서
http://www.superuser.co.kr/apache/virtualhost/page01.htm
아파치를 활용한 가상호스트 설정법
http://www.0u4u.com/linux/apache8.htm
공유포유의 가상호스트 설정법
http://www.apache.kr.net
한국 아파치 유저 그룹
http://okjsp.pe.kr/lecture/lec01/vhost02.xml
Tomcat4의 가상호스트 설정
xml-typed document
http://okjsp.pe.kr

'WAS' 카테고리의 다른 글

AIX - was의 병목현상  (0) 2008.06.04
jeus 4.0 - DATA-SOURCE 패스워드 암호화  (0) 2008.06.04
톰캣의 maxThreads, DBCP 장애  (0) 2006.02.24
tomcat4 가상호스트 설정  (0) 2006.02.21
Posted by 알 수 없는 사용자
|

tomcat4 가상호스트 설정

WAS 2006. 2. 21. 14:21

http://okjsp.pe.kr/lecture/lec01/vhost02.html

Tomcat4 가상호스트 설정

kenu
2002-03-19 1:37오전

이 문서는 Tomcat4 의 가상호스트를 사용해서 여러개의 도메인을 사용하도록 하는 방법을 설명합니다. 파일은 Tomcat4 의 conf 디렉토리에 있는 server.xml 파일만을 변경합니다. Tomcat4 의 기본적인 설치는 잘 되어 있다고 가정하고 다른 도메인을 붙이는 법을 설명합니다. 또한 DNS 서버 설정에 관해서는 이 문서에서 설명하지 않겠습니다. 관련강좌는 apache 가상호스트 강좌입니다.
강좌환경
OS:Solaris 2.8
Tomcat: 4.0.2
설치디렉토리: /home/jakarta/kpanet402
		


역시 이 문서도 Tomcat 4.0.2 를 기준으로 작성된 문서입니다. 버전이 틀릴 경우, 패키지에 포함된 문서를 참고하시기 바랍니다. 윈도우 사용자의 경우 경로를 \ 대신 / 로 사용하셔야 합니다. 상당히 혼동되는 부분입니다만 apache나 tomcat 모두 마찬가지 입니다.

conf 디렉토리에 있는 server.xml 파일을 열어서 아래 부분에 추가할 도메인과 그에 따른 부가적인 정보를 입력합니다. 이 때 주의 할 점은 도메인마다 <Host> 태그를 사용해서 설정합니다. 다른 <Host 태그 안에 들어가거나 태그의 열고 닫는 것이 얽혀서도 안됩니다. XML 특성상 태그의 대소문자를 구분하기 때문에 host 나 HOST 를 사용해도 안됩니다.
server.xml
 ...
        <!-- Define properties for each web application.  This is only needed
             if you want to set non-default properties, or have web application
             document roots in places other than the virtual host's appBase
             directory.  -->

        <!-- Tomcat Root Context -->
        <!--
          <Context path="" docBase="ROOT" debug="0"/>
        -->

        <!-- Tomcat Manager Context -->
        <Context path="/manager" docBase="manager"
         debug="0" privileged="true"/>

        <Context path="/fapa"
                  docBase="/home/fapa/public_html"
         debug="0" reloadable="true"/>

      </Host>

      <Host name="ycpa.or.kr" debug="0"
            appBase="/home/vhost/ycpahome" unpackWARs="true">
         <Context path=""
                  docBase="ROOT"
                  reloadable="true"/>
         <Alias>www.ycpa.or.kr</Alias>
      </Host>

      <Host name="pharmsnet.com" >
         <Context path=""
                  docBase="/home/jakarta/pmstemp"
                  reloadable="true"/>
         <Alias>www.pharmsnet.com</Alias>
      </Host>

    </Engine>

 


<Host> 태그 안에 Context 가 들어가고 www 와 같은 2차 도메인들도 Alias 에서 설정할 수 있습니다. 중요한 속성 몇 가지를 설명하자면 Host 태그의 appBase 속성은 웹 어플리케이션(Context)들의 기준 디렉토리가 됩니다. Context 에 있는 docBase 가 상대경로일 경우 기준위치가 된다는 뜻이죠. 예를 들면 ycpa.or.kr 의 ROOT Context의 루트디렉토리는 /home/vhost/ycpahome/ROOT 가 됩니다. unpackWARs 의 속성값이 "true" 일 경우 appBase 에서 정한 디렉토리에 WAR 파일이 있을 경우 Tomcat 이 자동으로 이 웹 어플리케이션의 압축을 풀고 Context를 설정하게 됩니다. 자동배치(auto deployment) 라고도 합니다.

<Context> 태그에서 주의할 점은 해당 도메인의 루트 경로를 지정할 때 path="/" 과 같이 쓰지 않고, path="" 를 사용하는 것입니다. docBase 에는 상대경로, 절대경로 모두 사용할 수 있습니다. reloadable="true" 는 servlet reloading 을 할 것인가 말 것인가인데, 개발중에는 true로 설정하고, 개발 완료되었을 경우 false로 놓는 것이 속도 향상에 도움이 됩니다. <Alias> 태그는 apache의 ServerAlias 와 같은 역할을 합니다. 여러개의 Alias 를 걸어줄 경우, 태그를 나란히 여러 번 사용하면 됩니다.

각 위치마다 테스트용 jsp 파일을 넣은 다음 브라우저를 열어서 설정을 확인해보시기 바랍니다. 속성에 대한 자세한 설명은 아래의 링크에서 찾아보시기 바랍니다.

잡설: 강좌를 써가면서 점점 무미건조해짐을 느낍니다. 그만큼 여유도 없어지고, 실력도 딸리는 것 같다는 생각이 많이 듭니다. 다시 한 번 강조하지만, 적어도 이 시대 프로그래머는 영어는 기본으로 해야합니다. 한글로 된 매뉴얼이 있다면 초보 개발자들이 쉽게 입문할 수 있겠지만, 인터넷에 산재한 문서들을 한글로 바꾸는 것도 한계가 있고, 일한번역기는 쓸만하지만 영한번역기로 영문서를 볼 수도 없기 때문에 자신의 기술을 빨리 진보시키고 싶은 분들은 영어공부를 따로 하시는 것을 추천합니다. free talk은 안되더라도 영문 매뉴얼은 읽어낼 수 있을 정도면 됩니다. 아울러 소망이 있다면 제 강좌들을 영문으로 다시 제작해서 올리고 싶습니다. 국내 제일의 jsp 사이트가 아니라 www.jspinsider.com 같은 세계적인 사이트가 되고 싶으니까요. 이상 Communication 수단을 넓히자는 kenu의 잡담이었습니다.

관련 사이트
http://okjsp.pe.kr/lecture/lec01/vhost01.xml
아파치 가상호스트 설정 강좌
http://jakarta.apache.org/tomcat/tomcat-4.0-doc/config/host.html
Tomcat 4 가상호스트 설정에 대한 매뉴얼
http://jakarta.apache.org/tomcat/tomcat-3.3-doc/tomcat-ug.html
Tomcat 3.3 가상호스트 설정에 대한 매뉴얼(하단)
http://jakarta.apache.org/tomcat/tomcat-3.2-doc/uguide/tomcat_ug.html
Tomcat 3.2 가상호스트 설정에 대한 매뉴얼(하단)
xml-typed document
http://okjsp.pe.kr


3. Tomcat4 가상호스트 설정에 대한 매뉴얼
4. Tomcat 3.3 가상호스트 설정에 대한 매뉴얼
5. Tomcat 3.2 가상호스트 설정에 대한 매뉴얼

'WAS' 카테고리의 다른 글

AIX - was의 병목현상  (0) 2008.06.04
jeus 4.0 - DATA-SOURCE 패스워드 암호화  (0) 2008.06.04
톰캣의 maxThreads, DBCP 장애  (0) 2006.02.24
Apache 가상호스트 설정  (0) 2006.02.21
Posted by 알 수 없는 사용자
|

http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=servlet&c=r_p&n=968522077

서블렛 + JDBC 연동시 코딩 고려사항 -제2탄-
[JDBC Connection Pooling]

최근수정일자 : 2001.01.19
최근수정일자 : 2001.03.20(샘플예제추가)
최근수정일자 : 2001.10.22(디버깅을 위한 로직추가)
최근수정일자 : 2001.10.29(Oracle JDBC2.0 샘플추가)
최근수정일자 : 2001.11.08(윤한성님 도움 OracleConnectionCacheImpl 소스수정)
최근수정일자 : 2001.11.09(Trace/Debugging을 위한 장문의 사족을 담)

5. JDBC Connection Pooling 을 왜 사용해야 하는가 ?

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
================================================





Posted by 알 수 없는 사용자
|