Singleton pattern

싱글턴 패턴(Singleton pattern)

애플리케이션이 시작될 때 어떤 클래스가 최초 한번만 메모리를 할당하고, 그 메모리에 인스턴스를 만들어 사용하는 디자인 패턴.

객체가 너무 많아지면 컴퓨터 자원을 과도하게 사용하게 되고, 이는 프로그램 전체의 속도를 느리게 할 수 있다.

-> 개발자는 객체의 최대 개수를 제한할 필요가 생긴다.

싱글턴 패턴은 최대 N개로 객체 생성을 제한하는 패턴이다. 이 때 중요한 것은 생성되는 객체의 최대 개수를 제한하는 데 있어서 객체의 생성을 요청하는 쪽에서는 일일이 신경쓰지 않아도 되도록 만들어주는 것이다.

싱글턴 장점

  1. 메모리 낭비를 방지한다. 고정된 메모리 영역을 얻으면서 한번의 new로 인스턴스를 사용하기 때문.

  2. 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽다.

    싱글턴으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문.

  3. 두 번째 호출부터는 객체 로딩시간이 현저하게 줄어 성능이 좋아진다.

싱글턴 단점

  • 객체 지향 설계 원칙에 어긋나며 수정과 테스트가 어려워진다.

    싱글턴 인스턴스가 너무 많은 일을 하거나 많은 데이터를 공유시킬 경우 다른 클래스의 인스턴스들 간에 결합도가 높아져 '개방-폐쇄 원칙'을 위배하게 된다.

  • 멀티쓰레드 환경에서 동기화처리를 적절하게 해주지 않으면 인스턴스가 두 개 이상 생성될 수 있다.

싱글턴 패턴 활용될 상황 및 예시

자바 프로그래밍

  • 데이터베이스 커넥션 풀

  • 로그 라이터

게임 프로그래밍

  • 사운드 매니저

  • 스코어 매니저

싱글턴 구현

유틸리티 메서드로 싱글턴을 구현하자. 이 때 유틸리티 메서드 외에 생성자를 직접 호출해서 객체를 생성하는 것을 막기 위해서는 생성자를 private로 설정하면 된다.

   public static Database getInstance(String name) {
        if (singleton == null) {
            singleton = new Database(name);
        }
        return singleton;
    }

문제는 위처럼 구현할 경우, 멀티쓰레드 환경에서는 객체를 하나만 생성한다는 것을 보장할 수 없다. 쓰레드가 singleton 객체가 null인지 확인하는 과정이 거의 동시에 이뤄질 수 있기 때문이다. 이 취약점은 어떻게 해결할까?

   public synchronized static Database getInstance(String name) {
        if (singleton == null) {
            singleton = new Database(name);
        }
        return singleton;
    }

위처럼 synchronized를 예약어로 넣어주면 해결된다.

하지만 이건 조금만 생각해보면 비용이 너무 비효율적인 점을 알 수 있다.

  • 최초에 singleton이 null인 경우에만 다른 쓰레드와의 경합을 막으면 충분한데, synchronized를 예약어를 넣으면 모든 쓰레드를 기다리게하여 병목현상이 일어난다.

따라서 아래처럼 클래스 내부에서 최초에 객체 인스턴스를 만들도록 해서 개선할 수 있다.

public class Database {

    private static Database singleton = new Database("product");
    private String name;

    private Database(String name) {
        try {
            Thread.sleep(100);
            this.name = name;
        } catch (Exception e) {
        }
    }

    public static Database getInstance(String name) {
        return singleton;
    }

아래처럼 lazy holder에 의해 초기화를 시킬 수도 있다.

public class Database {

    private Database(String name) {
        try {
            Thread.sleep(100);
            this.name = name;
        } catch (Exception e) {
        }
    }
    
    private static class LazyHolder {
        public static final Database singleton = new Database("product");
    }
    
    public static Database getInstance() {
        return LazyHolder.INSTANCE;
    }
}

이 방법은 JVM의 클래스 초기화 과정에서 보장되는 원자적 특성을 이용하여 싱글턴의 초기화 문제에 대한 책임을 JVM에 떠넘긴다.

holder 안에 선언된 instance가 static이기 때문에 클래스 로딩시점에 한번만 호출될 것이며, final을 사용해 다시 값이 할당되지 않도록 만든 방법이다.

Last updated