본문 바로가기

공부/컴퓨터

자바에서의 Object Pool 기법

Object Pool Control 기법은 JDBC Connection Pool 을 만들거나, TCP/IP Socket Pool을
만드는 등, 최근엔 많은 자바 개발자들이 즐겨 사용하는 기법 중의 하나 입니다.
특히 JDBC Connection Pool 이 소개 되면서, 너도 나도 직접 Connection Pool을
만드는 시도를 자연스럽게 하게 됩니다.

저 역시 그 중의 하나였고, Object Pooling 기법에 대해 너무 단순하고 쉽게 생각
했었습니다. 최근에야 저의 프로그래밍 기법에 엄청난 잘못이 있었다는 것을
하나은행과 한빛은행 인터넷뱅킹 프로젝트 튜닝작업을 하면서 깨달았습니다.
저와 같은 잘못을, 행여나 다른 분들이 똑같이 범하지 않았으면 하는 바램에서 저의
잘못을 아래 처럼 기술합니다.


아래의 3 개의 클래스는 Object Pool 을 테스트 하는 프로그램입니다. 뭐가 잘못
되었을까요? 퀴즈입니다.


----8><--------------------------------------------------------------------
package badpool;
/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class BadPoolTest extends Thread{
    private int client = 0;
    public BadPoolTest(int n){
        this.client = n;
    }
    public static void main(java.lang.String[] args) {
        // PoolObject 의 최대값인 20 보다 많은 요청을 보내어야 테스트가 됨.
        int max = 40;
        System.out.println("Calling " + max + " threads");

        Thread[] threads = new BadPoolTest[max];
        for(int i=0;i             threads[i] = new BadPoolTest(i);
            threads[i].start();
        }
    }
    public void run(){
        PoolManager manager = null;
        PoolObject obj = null;
        try {
            manager = PoolManager.getInstance();
            obj = manager.getPoolObject();
            obj.execute();
        }
        finally{
            if ( obj != null ) manager.release(obj);
        }
    }
}
----8><--------------------------------------------------------------------
package badpool;
/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class PoolManager {
    private static PoolManager manager = null;

    private int MAX = 20;
    private int count = 0;
    private java.util.Vector pool = new java.util.Vector();

    private PoolManager() {
        super();
        for(int i=0;i             pool.addElement(new PoolObject(new Integer(i)));
        }
    }
    public static synchronized PoolManager getInstance() {
        if ( manager == null ) {
            manager = new PoolManager();
        }
        return manager;    
    }
    public synchronized PoolObject getPoolObject() {
        PoolObject obj = null;
        while( count >= MAX ) {
            try{
                Thread.sleep(1000);
            }catch(Exception e){}
            System.out.println("PoolManager : sleep... current count=" + count);
        }
        obj = (PoolObject)pool.elementAt(0);
        pool.removeElementAt(0);
        count++;
        return obj;
    }
    public synchronized void release(PoolObject obj) {
        count--;
        pool.addElement(obj);
        System.out.println("PoolManager: released ... current count=" + count);
    }
}
----8><--------------------------------------------------------------------
package badpool;

/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class PoolObject {
        private static java.util.Random random = new java.util.Random();
        private Integer index = null;
    
    public PoolObject(Integer i) {
        this.index = i;
    }
    public void execute() {
        // 어떤 비즈니스 로직....
        System.out.println("Thread-" + index + ": started.");
        int second = 0;
        try{
            // 5 - 15 seconds random sleep.
            second = 5 + (int)(random.nextDouble()*10);
            Thread.sleep(second*1000);
        }catch(Exception e){}
        System.out.println("Thread-" + index + ": elapsed=" + second);
    }
}
----8><--------------------------------------------------------------------

위 클래스들을 컴파일 하여 돌리면, 40개의 Thread 가 모두 동작하는 것이 아니라
20개만 수행하고 dead-lock 에 빠집니다. 왜냐면, 한정된 20 개의 PoolObject 을
동시에 40개의 Client Thread 가 요청하게 되면, 그 중 20개는 getPoolObject()을
성공하게 되어 정상적인 수행을 진행하게 되고, 나머지 20개는 synchronized lock
으로 인해 대기하게 됩니다. 한참이 지난 후 수행중이었던 20개의 Thread 가
사용하고 난 PoolObject 을 PoolManager 에게 반환(release)하려 합니다.
문제는 여기서 발생하죠. 같은 lock 객체에 의해 synchronized 가 걸려 있으니,
앞에서 getPoolObject() 을 미처 수행하지 못하고 대기하던 20개의 Thread 가
해당 lock 을 미리 점유하고 있어서, release() 메소드 내의 synchronized(lock)
블록으로 들어가지를 못하게 되는 겁니다.
"다 사용했으나 반환을 못하고, 받지를 못했으니 나머지 20개의 Thread 는 마냥
기다리고만 있습니다."

물론 getPoolObject() 내에서 대부분 2-3 회 반복적으로 시도하다가 가용한 자원이
끝까지 생기지 않으면 튕켜 나가게 로직을 짜곤 합니다. 이 상황이라면 dead-lock 까지는
가지 않게 되겠지만, "끼적끼적" 겨우 하나씩 동작하게 되지요.

외견상으로는 "왜 이렇게 시스템이 갑자기 느려졌지?" 라는 형태로 나타나게 됩니다.
혹시 자신이 만든 Connection Pool 에서 Pool 최대값을 넘지 않을 때는
잘 동작하는데, 동시에 최대값 이상의 요청이 급작스럽게 들어오고 난 이후부터
시스템이 급격하게 느려지거나 hang 이 발생한 적이 없었던가요?



아래는 고민하고 고민하여 나름대로 최적화된 모습이라 생각되는 방식을 찾아
보았습니다. 보다 더 나은 알고리즘이 있으면 조언 부탁합니다.

프로그램 설명은 각자 개인 스스로의 몫으로 돌렸으면 합니다.
주석을 이래저래 풀어 가며 테스트를 해 보시면 어떤 서로다른 결과가 나타나는지
직접 목격할 수 있을 겁니다.


----8><--------------------------------------------------------------------

package pool;

/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class PoolTest extends Thread{
    private int client = 0;
    public PoolTest(int n){
        this.client = n;
    }
    public static void main(java.lang.String[] args) {
        // PoolObject 의 최대값인 20 보다 많은 요청을 보내어야 테스트가 됨.
        int max = 40;
        System.out.println("Calling " + max + " threads");

        Thread[] threads = new PoolTest[max];
        for(int i=0;i             threads[i] = new PoolTest(i);
            threads[i].start();
        }
    }
    public void run(){
        PoolManager manager = null;
        PoolObject obj = null;
        try {
            manager = PoolManager.getInstance();
            obj = manager.getPoolObject();
            obj.execute();
        }
        finally{
            if ( obj != null ) manager.release(obj);
        }
    }
}

----8><--------------------------------------------------------------------

package pool;

/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class PoolManager {
    private static PoolManager manager = null;

    private int MAX = 20;
    private int count = 0;
    private java.util.Vector pool = new java.util.Vector();

    private PoolManager() {
        for(int i=0;i             pool.addElement( new PoolObject(new Integer(i)) );
        }
    }
    public static synchronized PoolManager getInstance() {
        if ( manager == null ) {
            manager = new PoolManager();
        }
        return manager;    
    }
    /**
     * PS: getPoolObject() 에서 파라메터로 "int from" 을 실제로는 받을 필요가
     *     없으나 어떤 Client Thread 에서 요청이 온지를 디버깅 하기 위해 넣어둔 것
     *     뿐임.
     */
    
    public synchronized PoolObject getPoolObject(int from) {
        while( count >= MAX ) {
            try{
                System.out.println("getPoolObject(from:" + from +
                                ") : sleep... active count=" + count);
                
                // Max값을 초과한 나머지는 모두 아래 wait(timeout) 에서 대기함.
                // 아래에서 1초를 wait 하게 하지만, 실제로는 그 이전에
                // notify() 에 의해 깨어나게 됨.
                wait(1*1000);
                //System.out.println("awaiked. ...");

            }catch(Exception e){
                System.out.println("getPoolObject: awaiked " + e.toString());
            }
        }
        PoolObject obj = (PoolObject)pool.elementAt(0);
        pool.removeElementAt(0);
        count++;
        System.out.println("getPoolObject(from:" + from +"): return : active count=" + count);
        return obj;
    }
    public synchronized void release(PoolObject obj) {
        count--;
        pool.addElement(obj);

        //notify();
        notifyAll();

        // 만약 notifyAll() 을 하면 getPoolObject()에서 wait()에서 걸린
        // 모든 Thread 가 일시에 일어나게 되어 쓸데없이 전부 while 문을 동시에
        // 확인하게되고, 이는 CPU 부하를 야기할 것으로 생각되나,
        // 일부 책에서는 notify() 대신 notifyAll() 을 권장하기도 함.
        // 두가지를 모두 테스트해 보면 아주 재밌는 현상을 만날 수 있을 것임.

        System.out.println("Thread-" + obj.getIndex() + ": released :active count=" + count);
    }
}

----8><--------------------------------------------------------------------

package pool;

/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class PoolObject {
        private static java.util.Random random = new java.util.Random();
        private Integer index = null;
    
    public PoolObject(Integer i) {
        this.index = i;
    }
    public void execute() {
        System.out.println("Thread-" + index + ": started.");
        int second = 0;
        try{
            // 5 - 15 seconds random sleep.
            second = 5 + (int)(random.nextDouble()*10);
            Thread.sleep(second*1000);
        }catch(Exception e){}
        System.out.println("Thread-" + index + ": elapsed=" + second);
    }
    public Integer getIndex() {
        return index;
    }
}

----8><--------------------------------------------------------------------


핵심은 어떤 경우든, Thread.sleep() 을 이용하여 가용한 자원이 생길때까지 기다리게
해서는 안된다는 사실입니다.
wait() 와 notify(),notifyAll() 을 이용하여만 합니다.

NOTE: wait() 은 synchronized block 의 lock 을 해제합니다. 따라서 연이어 들오는
      쓰레드들이 sync block 안으로 들어와 wait() 부분에서 함께 waiting 하게 됩니다.

그러나, 위의 샘플은 실 프로젝트 시스템에서는 사용할 법한 소스가 아닙니다. 중요한
한가지 요소가 빠져 있습니다. 갯수에 제한이 있는 자원에 대한 요청이 엄청나게 폭주할
경우, 가용한 자원을 할당 받지 못한 요청들은 위 로직대로라면 상당히 오랫동안
기다리게 됩니다. 이는 시스템의 hang 현상을 유발시킬 수 있는 절대적인 잠재력을
안고 있습니다. 일정한 TIME_WAIT 을 두어 일정시간을 기대려도 가용한 자원이
여전히 생기지 않을 경우 Fail 이 일어나게 하여 튕켜 나가게 하여야만 hang 으로
이어지지 않습니다.

아래는 실프로젝트에서 사용할 법한 로직입니다.


package org.jsn.pool;

/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class PoolManager {
    private static PoolManager manager = null;

    private int MAX_RESOURCE = 20;
    private long TIME_WAIT = 2 * 1000;
    private java.util.Hashtable pool = new java.util.Hashtable();
    private java.util.Stack freeStack = new java.util.Stack();

    private PoolManager() {
        for(int i=0;i             Integer index = new Integer(i);
            pool.put( index,  new PoolObject(index) );
            freeStack.push(index);
        }
    }
    public static synchronized PoolManager getInstance() {
        if ( manager == null ) {
            manager = new PoolManager();
        }
        return manager;    
    }
    public  PoolObject getPoolObject() throws Exception {
        Integer index = null;
        long start = System.currentTimeMillis();
        synchronized(this){
            while( freeStack.empty()  ) {
                try{
                    // 나머지는 모두 아래 wait(timeout) 에서 대기함.
                    // 아래에서 1 초를 wait 하게 하지만, 실제로는 그 이전에
                    // notify() 에 의해 깨어나게 됨.
                    wait(1 * 1000);
                    
                }catch(Exception e){
                    System.err.println("getPoolObject: awaiked " + e.toString());
                }
                long end = System.currentTimeMillis();
                if ( freeStack.empty() && (end - start) >= TIME_WAIT ) {
                    throw new Exception("getPoolObject : timeout(" + TIME_WAIT + ") exceed");
                }
            }
            index = (Integer)freeStack.pop();
        }
        PoolObject obj = (PoolObject)pool.get(index);
        return obj;
    }
    public void release(PoolObject obj) {
        synchronized ( this ){
            freeStack.push(obj.getIndex());
            
            //notify();
            notifyAll();
            // 만약 notifyAll() 을 하면 getPoolObject()에서 wait()에서 걸린
            // 모든 Thread 가 일시에 일어나게 되어 쓸데없이 전부 while 문을 동시에
            // 확인하게되고, 이는 CPU 부하를 야기할 것으로 생각되나,
            // 일부 책에서는 notify() 대신 notifyAll() 을 권장하기도 함.
            // 두가지를 모두 테스트해 보면 아주 재밌는 현상을 만날 수 있을 것임.
        }
        //System.out.println("Object-" + obj.getIndex() + ": released.");
    }
}
----8><--------------------------------------------------------------------

필요할 경우, 위의 java.util.Stack 대신 Queue 를 사용하실 수 있습니다.
사용하는 자원만 지속적으로 사용하고, Stack 의 저 아래에 있는 특정 자원은 한번도
사용되지 않을 것 같은 불안감(?)이 드신다면 Stack 대신 Queue 를 이용하여 다음처럼
골고루(?) 사용토록 고쳐셔도 됩니다.

package org.jsn.pool;

/*
* Lee WonYoung
* javaservice@hanmail.net, lwy@kr.ibm.com
* 2000.11.21
*/
public class PoolManager {
    private static PoolManager manager = null;

    private int MAX_RESOURCE = 20;
    private long TIME_WAIT = 2 * 1000;
    private java.util.Hashtable pool = new java.util.Hashtable();
    private org.jdf.util.Queue freeQueue = new org.jdf.util.Queue();

    private PoolManager() {
        for(int i=0;i             Integer index = new Integer(i);
            pool.put( index,  new PoolObject(index) );
            freeQueue.enqueue(index);
        }
    }
    public static synchronized PoolManager getInstance() {
        if ( manager == null ) {
            manager = new PoolManager();
        }
        return manager;    
    }
    public  PoolObject getPoolObject() throws Exception {
        Integer index = null;
        long start = System.currentTimeMillis();
        synchronized(this){
            while( freeQueue.empty() ) {
                try{
                    // 나머지는 모두 아래 wait(timeout) 에서 대기함.
                    // 아래에서 1 초를 wait 하게 하지만, 실제로는 그 이전에
                    // notify() 에 의해 깨어나게 됨.
                    wait(1 * 1000);

                }catch(Exception e){
                    System.err.println("getPoolObject: awaiked " + e.toString());
                }
                long end = System.currentTimeMillis();
                if ( freeQueue.empty() && (end - start) >= TIME_WAIT ) {
                    throw new Exception("getPoolObject : timeout(" + TIME_WAIT + ") exceed");
                }
            }
            index = (Integer)freeQueue.dequeue();
        }
        PoolObject obj = (PoolObject)pool.get(index);
        return obj;
    }
    public void release(PoolObject obj) {
        synchronized ( this ){
            freeQueue.enqueue(obj.getIndex());
            
            //notify();
            notifyAll();
            // 만약 notifyAll() 을 하면 getPoolObject()에서 wait()에서 걸린
            // 모든 Thread 가 일시에 일어나게 되어 쓸데없이 전부 while 문을 동시에
            // 확인하게되고, 이는 CPU 부하를 야기할 것으로 생각되나,
            // 일부 책에서는 notify() 대신 notifyAll() 을 권장하기도 함.
            // 두가지를 모두 테스트해 보면 아주 재밌는 현상을 만날 수 있을 것임.
        }
       //System.out.println("Object-" + obj.getIndex() + ": released.");
    }
}
----8><--------------------------------------------------------------------


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

Re: LinkedList 를 이용한 Queue.java
http://www.javaservice.net/~java/bbs/read.cgi?m=devtip&b=javatip&c=r_p&n=987580631


-------------------------------------------------------  
  본 문서는 자유롭게 배포/복사 할 수 있으나 반드시
  이 문서의 저자에 대한 언급을 삭제하시면 안됩니다
================================================
  자바서비스넷 이원영
  E-mail: javaservice@hanmail.net
  PCS:019-310-7324
================================================