본문 바로가기
Programming

디자인패턴 시리즈 13. 오브젝트 풀 패턴 (Object Pool Pattern)

by LeeJ1Hyun 2023. 1. 27.

오브젝트 풀 패턴 (Object Pool Pattern)

재사용 가능한 객체들을 모아둔 객체 풀 클래스를 정의한다. 객체가 필요로 할 때 풀에 요청하고 사용한 후엔 반환한다.

코드에 적용해보기

오브젝트 풀 패턴의 특성을 보면 마치 플라이웨이트 패턴과 유사하다고 느껴진다. 플라이웨이트 패턴특정한 클래스의 인스턴스 하나로 여러 개의 가상 인스턴스를 제공한다. 즉, 같은 인스턴스를 여러 객체에서 공유한다는 의미에서의 재사용이다. 메모리 차지를 최소한으로 줄이기 위해 사용하는 것이 목적이다. 반면 오브젝트 풀 패턴은 객체를 재사용하지만 하나의 객체는 독점적으로 사용된다. 반드시 사용하고 있지 않은 객체만을 재사용하는 것이다.

 

Pool


인스턴스를 만들 때 비용이 많이 드는 경우에 사용할 수 있다. 데이터 베이스 연결같이 객체 생성시 비용이 많이 들기 때문에 생성해둔 것들을 재사용하면 좋다. 그 예시로 JDBC에서는 JDBC Connection Pool(데이터베이스와 애플리케이션을 효율적으로 연결)을 제공하고 있다. Thread Pool도 오브젝트 풀이 기본 원리이다. 비용이 많이드는 경우뿐만 아니라 객체를 빈번하게 생성, 삭제해야 하는 경우에 메모리 단편화(Memory Fragmentation)가 발생하는 것을 막을 수 있다.

 

커넥션 풀 (Connection Pool)


웹 어플리케이션 서버와 데이터 베이스의 연결은 비용이 많이 든다. 하나의 INSERT문을 수행할 때 연결, 쿼리를 서버에 보내기, 쿼리를 파싱하기, row를 INSERT, index를 INSERT, 종료의 순으로 각각의 과정마다 비용이 발생한다. 이중에서 가장 큰 비용을 차지 하는 것이 바로 연결하는 과정이다. row를 INSERT하는 과정의 대략 3배 가까이 비용이 발생한다.

연결(Connection) 과정을 보완할 수 있는 것이 커넥션 풀 (Connection Pool)이다.

 

Connection Pool


커넥션 풀은 여러 커넥션을 미리 만들어 놓고 이를 관리할 수 있다. 개발자가 커넥션 풀을 이용하여 커넥션의 개수를 제한할 수 있다. 커넥션 풀이 크다고 무작정 좋을까 고민해볼 필요가 있다. 커넥션을 사용하는 주체는 Thread이다. Thread의 개수보다 커넥션 풀이 크다면 아무리 많아도 사용할 수 없기 때문에 잉여 커넥션이 생기고 곧 메모리 낭비로 이어진다.

적절한 커넥션 풀의 크기를 찾으려면 최대 연결 수를 무제한으로 설정하고 부하 테스트를 통해 최적의 값을 도출해야 한다. 일반적으로 MySql의 경우 600명의 User를 대응하기 위해 15~20개의 커넥션 풀이 필요하다고 한다.

 

메모리 단편화(Memory Fragmentation)


컴퓨터 메모리에는 프로세스, 리소스 등이 끊임없이 올라가고 해제되면서 메모리 공간이 작은 조각 공간으로 나뉘게 된다. 잘게 쪼개져 사용 가능한 공간이 존재함에도 불구하고 할당이 불가능한 상태를 말한다. 이는 내부 단편화와 외부 단편화로 나누어진다.

  • 내부 단편화

내부 단편화가 일어난 메모리 영역


할당 받은 영역이 실제로 메모리상에 사용되는 영역보다 더 커서 잉여 공간이 생긴다. 이 메모리 공간의 차이를 내부 단편화라고 한다. 프로세스에서 필요한 만큼의 메모리만 할당하여 낭비되는 공간이 없게 하는 것이 일반적이다. 예를 들자면 A 프로세스는 1KB만큼의 영역을 차지하는 작업이라고 할 때 3KB를 할당할 경우 2KB만큼의 내부 단편화가 생긴 것이다.

 

  • 외부 단편화

외부 단편화가 일어난 메모리 영역


메모리가 할당, 해제되는 과정이 반복되면 실제로 사용중인 영역이 불연속적으로 차지된다. 사용중인 메모리 사이에 조각난 빈공간들이 외부 단편화이다. 전체적인 사용 가능 공간은 충분하지만 15KB를 할당할 연속된 메모리 영역이 없기 때문에 메모리 할당이 불가능하다. 이렇게 되면 이를 할당하기 위한 메모리 영역을 확보하기 위해 더 많은 탐색을 거쳐야 한다. 이는 속도의 저하를 야기한다.

 

내부 단편화와 외부 단편화


내부 단편화는 프로세스 하나에 할당된 메모리 영역이 남는 것을 의미하고 외부 단편화는 프로세스 사이의 공간이 쪼개져 특정 메모리 공간이 필요한 프로세스에 할당하지 못하는 것을 의미한다.

커넥션 풀의 기반이 되고, 메모리 단편화의 해결방법인 오브젝트 풀 패턴으로 다시 돌아오자. 오브젝트 풀 패턴은 객체를 유지하기 위해 Creation, Validation, Destroy의 라이프 사이클을 가진다. Creation 단계에서는 Pool을 만든다. Validation 단계에서는 총 3가지 하위 단계를 거친다. Pool안에 객체가 없다면 오브젝트 풀은 1개의 객체를 만들고 요청한 스레드에 제공한다. 객체들이 다른 스레드에 의해 사용되고 있을 때 오브젝트 풀은 1개의 객체를 만들어 제공한다. Pool의 모든 객체가 다른 스레드에 의해 사용중일 경우 1개의 객체가 반환될 때까지 기다린다. 즉, 사용 가능한 객체가 있다면 사용하고 없다면 만들어 사용하거나 기다린다. Destroy 단계에서는 불필요한 풀을 파괴한다.

아주 간략한 예제 코드를 살펴보자.

 

public interface ObjectFactory<T> {

    T createObject();

    boolean validateObject(T object);

    void destroyObject(T object);
}


객체를 만들고, 검증하고, 파괴할 메소드를 선언한다.

 

public class ObjectPool<T> {

    private LinkedList<T> pool;
    private ObjectFactory<T> factory;

    public ObjectPool(ObjectFactory<T> factory) {
        this.factory = factory;
        this.pool = new LinkedList<T>();
    }

    public T getObject() {
        if (pool.isEmpty()) {
            return factory.createObject();
        } else {
            T object = pool.removeFirst();
            if (factory.validateObject(object)) {
                return object;
            } else {
                factory.destroyObject(object);
                return getObject();
            }
        }
    }

    public void returnObject(T object) {
        if (factory.validateObject(object)) {
            pool.addLast(object);
        } else {
            factory.destroyObject(object);
        }
    }
}


다양한 방식으로 Pool을 구현할 수 있겠지만 LinkedList를 선택했다. ObjectPool의 생성자에 ObjectFactory를 인자로 넘겨준다. 하나의 Pool이 만들어지면 하나의 LinkedList가 만들어진다고 생각하면 된다. getObject() 메소드는 객체를 달라고 요청한다. 만약 Pool이 비어있으면 ObjectFactory를 통해 객체를 만들어 반환한다. 비어있지 않다면 객체 하나를 가져와서 유효성 검증을 한다. 통과하면 해당 객체를 반환하고, 그렇지 않으면 파괴후 다시 getObject()를 호출한다.

returnObject() 메소드는 사용한 객체를 반환한다. 만약 유효성 검증을 통과했다면 다시 Pool에 담고, 그렇지 않으면 파괴한다. 간단하게 라이프 사이클이 돌아가는 구성을 보여주기 위한 코드 조각이기 때문에 interface를 구현하지는 않았다.

이제 본격적으로 데이터 베이스 커넥션을 만들어볼 것이다. 좋은 예시가 있어서 출처를 남기고 사용하고자 한다.

 

public interface Disposable {

    void dispose();
}

 

public class DBConnection implements Disposable {

    private String connectionUrl;
    private String username;
    private String password;

    @SuppressWarnings("unused")
    private DBConnection() {
        super();
    }

    public DBConnection(String connectionUrl, String username, String password) {
        super();
        this.connectionUrl = connectionUrl;
        this.username = username;
        this.password = password;
    }

    public String queryData(String query) {
        return "쿼리 [" + query + "] 실행!";
    }

    @Override
    public void dispose() {
        connectionUrl = null;
        username = null;
        password = null;
    }
}


DBConnection 클래스는 데이터 베이스에 연결에 필요한 url, username, password을 전달한다. 일종의 POJO(환경과 기술에 의존하지 않고 필요에 따라 재활용될 수 있는 방식으로 설계된 자바 오브젝트) 표현이다.

 

public class MyDatabaseObjectPool {

    private final int MAX_SIZE = 8;
    private int occupiedSize = 0;
    private int availableSize = 0;
    private final DBConnection[] availableConnections = new DBConnection[MAX_SIZE];
    private final DBConnection[] occupiedConnections = new DBConnection[MAX_SIZE];

    private String connectionUrl;
    private String username;
    private String password;

    @SuppressWarnings("unused")
    private MyDatabaseObjectPool() {
        super();
    }

    public MyDatabaseObjectPool(String connectionUrl, String username, String password) {
        super();
        this.connectionUrl = connectionUrl;
        this.username = username;
        this.password = password;
    }

    synchronized public DBConnection getObject() {
        if (availableSize == 0 && occupiedSize == MAX_SIZE) {
            return null;
        } else if (availableSize == 0 && occupiedSize < MAX_SIZE) {
            final DBConnection newDbConnection = new DBConnection(connectionUrl, username, password);
            assign(newDbConnection);
        }
        return assign();
    }

    synchronized public void releaseObject(final DBConnection dbConnection) {
        deAssign(dbConnection);
    }

    private void assign(DBConnection dbConnection) {
        for (int position = 0; position < MAX_SIZE; position++) {
            if (availableConnections[position] == null) {
                availableConnections[position] = dbConnection;
                availableSize++;
                break;
            }
        }
    }

    private DBConnection assign() {
        DBConnection toBeAssigned = null;
        for (int position = 0; position < MAX_SIZE; position++) {
            final DBConnection dbConnection = availableConnections[position];
            if (dbConnection != null) {
                toBeAssigned = dbConnection;
                for (int occupiedPosition = 0; occupiedPosition < MAX_SIZE; occupiedPosition++) {
                    if (occupiedConnections[occupiedPosition] == null) {
                        occupiedConnections[occupiedPosition] = toBeAssigned;
                        availableConnections[position] = null;
                        availableSize--;
                        occupiedSize++;
                        break;
                    }
                }
            }
        }
        return toBeAssigned;
    }

    private void deAssign(DBConnection dbConnection) {
        for (int position = 0; position < MAX_SIZE; position++) {
            if (Objects.equals(occupiedConnections[position], dbConnection)) {
                occupiedConnections[position] = null;
                occupiedSize--;
                break;
            }
        }

        for (int position = 0; position < MAX_SIZE; position++) {
            if (availableConnections[position] == null) {
                availableConnections[position] = dbConnection;
                availableSize++;
                break;
            }
        }
    }

    @Override
    protected void finalize() {
        for (DBConnection dbConnection : availableConnections) {
            if (dbConnection != null) {
                dbConnection.dispose();
            }
        }

        for (DBConnection dbConnection : occupiedConnections) {
            if (dbConnection != null) {
                dbConnection.dispose();
            }
        }
    }
}


DBConnection 객체가 필요할 때마다 인스턴스를 생성하지 않고 바로 가져오기 위해 만든 Pool이다. 사용 가능한 객체의 수와 사용중인 객체의 수를 추적하면서 스레드가 객체를 요청할 때마다 상황에 맞는 처리를 한다.

getObject() 메소드에서는 모든 객체가 사용중이라 사용 가능한 객체가 없을 때 null을 반환하도록 처리했다. 사실 이 요청을 대기 상태로 유지하기 위해 해당 조건문 블록 내부를 동기화해야 하지만 단순하게 예를 들기 위해 객체가 할당 될 때까지 요청을 보내기 위해 MyDatabase 클래스로 책임을 위임했다.

사용가능한 객체는 없지만 공간은 존재할 경우 새로운 DBConnection 객체를 만들고 커넥션 배열에 할당한다. 모든 조건에 부합하지 않으면 사용 가능한 슬롯을 확인하고 객체 참조를 사용 가능에서 사용중으로 이동합니다.

releaseObject() 메소드에서는 객체 참조를 사용중에서 사용 가능으로 이동한다.

finalize() 메소드는 체 풀을 파괴하기 전에 재사용 가능한 목적으로 보관했던 객체들을 방출한다.

 

public class MyDatabase {

    private String connectionUrl;
    private String username;
    private String password;

    private MyDatabaseObjectPool objectPool = null;

    @SuppressWarnings("unused")
    private MyDatabase() {
        super();
    }

    public MyDatabase(String connectionUrl, String username, String password) {
        this.connectionUrl = connectionUrl;
        this.username = username;
        this.password = password;
        objectPool = new MyDatabaseObjectPool(this.connectionUrl, this.username, this.password);
    }

    public String queryData(final String query) {
        DBConnection dbConnection = objectPool.getObject();
        while (dbConnection == null) {
            sleepThread(3000);
            dbConnection = objectPool.getObject();
        }
        
        System.out.println("Querying data with object - " + dbConnection);
        final String queryResult = dbConnection.queryData(query);
        objectPool.releaseObject(dbConnection);
        return queryResult;
    }

    private void sleepThread(long milliseconds) {
        try {
            Thread.sleep(milliseconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}


MyDatabase 클래스는 MySQL 같은 다른 데이터베이스 시뮬레이션같은 역할을 한다. MyDatabase 객체가 생성될 때 MyDatabaseObjectPool 클래스들의 객체도 Client에서 받은 url, username, password를 이용한다. queryData() 메소드는 MyDatabaseObjectPool 클래스에게 데이터 베이스에서 DBConnection 객체가 쿼리를 수행하도록 요청한다. 만약 객체가 없다면 3초간 기다리도록 설정했다. 기다리는 시간이 길어질수록 중복된 스레드를 사용할 가능성이 높아졌다.

 

public class Client {

    public static void main(String[] args) {
        MyDatabase myDatabase = new MyDatabase("url", "username", "password");

        for (int offset = 1; offset <= 10; offset++) {
            final int number = offset;

            new Thread(() -> {
                final String queryData = myDatabase.queryData("Query " + number);
                System.out.println(queryData);
            }).start();
        }
    }
}


Client 클래스에서는 데이터 베이스 연결을 하고 해당 Connection으로 매번 새로운 스레드를 만들어 쿼리 수행을 맡긴다. 결과를 살펴보면 10개의 쿼리를 수행할 때 @3b94891a, @38a3fdfc 객체가 각각 3번 담당했다. 즉, 새로 만들지 않고 사용이 끝난 커넥션 객체를 가져다 쓴 것이다. Pool의 크기를 작게하면 사용 가능한 객체도 적어진다. 이를 변동 시켰을 때 작업이 더 오래 걸린다는 것을 확인할 수 있다.

 

오브젝트 풀 패턴 다이어그램






* 아래의 자료들을 참고하였습니다.

 

메모리 단편화(Memory Fragmentation)

메모리 단편화(Memory Fragmentation) 메모리 단편화란? 메모리 단편화(Memory Fragmentation)란 메모리 사용 가능 공간이 충분함에도 메모리 할당이 불가능한 상태를 얘기한다. 메모리 단편화는 두 가지 종

internet-craft.tistory.com

 

[OS] 메모리 단편화(Memory Fragmentation)란?

🚀 메모리 단편화(Memory Fragmentation)의 정의 컴퓨터에서 프로그램을 실행하거나 작업을 할 때 컴퓨터는 메모리에 해당 프로그램을 올리고 실행을 하게 됩니다. 이때 주기억장치 상에서 빈번하게

cocoon1787.tistory.com

 

DB 커넥션 풀(Connection pool)이란? HikariCP란?

커넥션 비용 WAS(Web Application Server)와 데이터베이스 사이의 연결에는 많은 비용이 든다. MySQL 8.0을 기준으로 INSERT 문을 수행할 때 필요한 비용의 비율은 다음과 같다. 괄호 안의 숫자가 비율을 의

code-lab1.tistory.com

 

Object Pool Design Pattern– Simply Engineers by Sandeep Dass

Object Pool Design Pattern is one of the famous Creational Design Patterns. In this design pattern, the focus is on reusing the objects.

simplyengineers.in

댓글