design pattern

디자인패턴 - 싱글톤패턴 ( java )

malangcow 2022. 8. 24.

singleton이 왜 필요할까?

singleton이 해결하려는 문제는 다음과 같다.

  • 클래스의 인스턴스가 1개임을 보장하고 싶다.
  • 인스턴스 생성을 제어할수 있어야한다.
  • 1개뿐인 인스턴스에 쉽게 접근할 수 있어야 한다.(글로벌 객체)

디자인패턴 - 싱글톤패턴 ( java ) - undefined - singleton이 왜 필요할까? - singleton이 해결하려는 문제는 다음과 같다.
singleton class diagram (wiki)

위키백과에서 싱글톤 클래스 다이어그램의 예시를 보여주고있다.

특이점은

  • 현재클래스를 필드변수로 선언하고
  • 생성자는 private으로 외부에서 사용할 수 없고
  • 유일한 public 접근자인 getInstance 메소드로 필드변수에 생성된 인스턴스를 얻을 수 있다.

언뜻 보기만 해도 이렇게하면 싱글톤이 유지될 것 같다.

 

public class Singleton1Basic {
    /*
    *   인스턴스를 클래스 내부 필드에서 생성
     * */
    private static volatile Singleton1Basic instance = new Singleton1Basic();

    /*  생성자를 private으로 봉인
     * */
    private Singleton1Basic() {}

    /*
     *   싱글톤은 글로벌로 접근이 가능해야 하므로 static
     * */
    public static Singleton1Basic getSingleton() {
        return instance;
    }


    public static void main(String[] args) {
        /*
        *   매번 같은 인스턴스를 반환하는 클래스 생성 테스트!
        * */
        Singleton1Basic singleton1 = getSingleton();
        Singleton1Basic singleton2 = getSingleton();
        assert singleton1 == singleton2;
    }
}

간단하게 구현해본 싱글톤 클래스이다.

간단하지만 이 클래스를 사용하지 않을 때에도 프로그램 시작부터 항상 메모리를 차지하고 있다는 단점이 존재한다.

 

인스턴스가 필요할 때 메모리에 올라가면 훨씬 메모리사용이 효율적일 것이다.

 

public class Singleton2Advanced {
    private static Singleton2Advanced instance;

    private Singleton2Advanced() {}

	// instance를 호출한 시점에 메모리에 올라간다.
    public static synchronized Singleton2Advanced getInstance() {
        if (instance == null) {
            instance = new Singleton2Advanced();
        }
        return instance;
    }

    public static void main(String[] args) {
        /*
         *   매번 같은 인스턴스를 반환하는 클래스 생성 테스트!
         * */
        Singleton2Advanced singleton1 = Singleton2Advanced.getInstance();
        Singleton2Advanced singleton2 = Singleton2Advanced.getInstance();
        assert singleton1 == singleton2;
    }
}
  • 싱글톤 인스턴스 생성을 늦춰서 메모리를 효율적으로 가져가는 개선방식의 코드이다.

하지만 또 thread safe하지 않다는 단점이 있음!

2개의 쓰레드가 동시에 접근했을 때 null을 마주친다면 인스턴스는 2개를 생성할것이다.

간단하게 메서드에 synchrosize를 붙여서 해결할 수 있다.

  • 하지만 인스턴스를 호출할 때마다 동기화를 하기 때문에 느릴 수 밖에 없다.

 

인스턴스가 이미 생성된 경우는 동기화처리를 안한다면 더 효율적일 것 같다.

 

public class Singleton3volatile {
	// volatile 키워드를 사용
    private static volatile Singleton3volatile instance;

    private Singleton3volatile() {}

	// instance가 null일 때만 동기화 처리
    public static Singleton3volatile getInstance() {
        if (instance == null) {
            synchronized (Singleton3volatile.class) {
                if (instance == null) {
                    instance = new Singleton3volatile();
                }
            }
        }

        return instance;
    }

    public static void main(String[] args) {
        /*
         *   매번 같은 인스턴스를 반환하는 클래스 생성 테스트!
         * */
        Singleton3volatile singleton1 = Singleton3volatile.getInstance();
        Singleton3volatile singleton2 = Singleton3volatile.getInstance();
        assert singleton1 == singleton2;
    }
}
  • instance가 null일때만 synchrosize 처리를 해서 성능을 개선한 코드이다.

하지만 인스턴스를 스레드에서 생성하면 일반적으론 cpu 캐시에서 먼저 생성되고 메모리에 올리게 되는데

메모리에 쓰는시점과 다른 스레드가 메모리의 인스턴스를 읽는 시점이 타이밍이 달라 동기화 문제가 생긴다.

예를들어 ) thread1이 인스턴스를 캐시에 생성하고 메모리에 올리기 전에 thread2가 메모리에 null을 읽고 인스턴스를 생성할 수 있어서 동기화문제가 발생할 수 있다.
  • volatile 키워드를 사용하면 cpu 캐시를 사용하지 않고 메모리로 바로 접근하기 때문에
    이러한 동기화를 해결할 수 있지만 또 캐시를 사용하지 않으니 성능에서 손해는 발생한다.

 

마지막으로 Holder방식이 존재한다.

public class Singleton4Holder {
    private static Singleton4Holder instance;

    private Singleton4Holder() {}

	// holder 클래스 추가
    private static class SingletonHolder {
        private static final Singleton4Holder instance = new Singleton4Holder();
    }

    public static Singleton4Holder getInstance() {
        return SingletonHolder.instance;
    }

    public static void main(String[] args) {
        /*
         *   매번 같은 인스턴스를 반환하는 클래스 생성 테스트!
         * */
        Singleton4Holder singleton1 = Singleton4Holder.getInstance();
        Singleton4Holder singleton2 = Singleton4Holder.getInstance();
        assert singleton1 == singleton2;
    }
}
  • 내부에 인스턴스 홀더 생성하여 효율적인 지연된 초기화 방식을 구현할 수 있다.
  • 홀더는 inner class로 외부 클래스의 필드변수를 의존한다.
보통 static class는 클래스 로딩시점에 한 번만 호출되는데
이렇게 내부에 static class를 선언하면 그렇지 않고 실제로 호출하는 시점에 로딩이된다.
여기서는 바로 getInstance를 호출했을 때 로딩이 된다는 얘기다.

jvm의 클래스 로더는 클래스를 하나씩 읽어오는데,
이렇게 하면 싱글톤을 jvm레벨에서 보장해주기 때문에 동기화 걱정을 할 필요가 없어진다.

 

실제로 싱글톤은 여러 오픈소스에서 쓰이고 있는 것을 볼 수 있었다.

 

예시로 java.util.UUID 클래스에서 Holder 방식을 사용하고 있다.

public final class UUID implements java.io.Serializable, Comparable<UUID> {
	// ...
    
    private static class Holder {
        static final SecureRandom numberGenerator = new SecureRandom();
    }
    
    public static UUID randomUUID() {
        SecureRandom ng = Holder.numberGenerator;
        // ... 생략
    }
}
  • Holder.numberGenerator는 randomUUID()를 호출한 시점부터 메모리 상에 올라갈 것이다.

 

 

 

싱글톤은 자바, 스프링 생태계에서 정말 많이 보이는 디자인 패턴인 것 같다.

 

각종 오픈소스들을 이해하려면 꼭 공부를 해봐야 할 것 같다. 물론 다른 디자인패턴들도 모두..

댓글