DEV ℧ Developer Diary

[DesignPattern] 싱글톤 (Singleton)

싱글톤 (Singleton)

오직 한 개의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근점을 제공합니다.

활용 방안

싱글톤은 다음과 같은 상황에 사용할 수 있다.

  • 클래스의 인스턴스가 오직 하나여야 함을 보장하고, 잘 정의된 접근점(accesspoint)으로 모든 사용자가 접근할 수 있도록 해야 할 때
  • 유일한 인스턴스가 서브 클래싱으로 확장되어야 하며, 사용자는 코드의 수정없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때

구조

싱글톤 구조

  • Singleton: Instance() 연산을 정의하여, 유일한 인스턴스로 접근할 수 있도록 합니다. 유일한 인스턴스를 생성하는 책임을 맡습니다.

장점

  1. 유일하게 존재하는 인스턴스로의 접근을 통제합니다.
  2. 무분별한 전역 변수 사용으로 인한 Class 식별을 위한 이름 공간(name space)을 좁힙니다.
  3. 연산 및 표현의 정제를 허용합니다. 상속된 서브 클래스를 통해 새로운 인스턴스를 만들 수 있습니다.
  4. 인스턴스의 개수를 변경하기가 자유룝습니다.
  5. 클래스 연산을 사용하는 것보다 훨씬 유연한 방법입니다.

구현

Singleton 기본 구조

Singleton 패턴의 기본 구조는 아래와 같다.

public class Singleton1 {
    private static Singleton1 instance;
    private Singleton1() {}

    public static Singleton1 getInstance() {
        if (instance == null) {
            instance = new Singleton1();
        }

        return instance;
    }
}

static으로 객체의 인스턴스 변수를 생성한 뒤 생성자를 private 접근자로 설정해 외부에서 해당 클래스의 접근을 막아 줍니다.
이후 getInstance() static 메서드를 이용해 기존에 생성된 인스턴스만 가져올 수 있도록 구현합니다.

외부에선 아래와 같이 호출 할 수 있습니다.

public class Main {
    public static void main(String[] args) {
        Singleton1 singleton1 = Singleton1.getInstance();
    }
}

또한 Singleton에는 두가지 초기화 방법이 있다.

앞서 보여준 기본 구조는 느린 초기화(Lazy Initialization)로 실제 인스턴스를 사용하기 위해 호출할 때 인스턴스를 생성하는 방법이다.
반대되는 방식으로 이른 초기화(Early Initialization)가 있다.

이른 초기화(Early Initialization)
static final 변수에 미리 new를 통해 인스턴스화 해서 인스턴스를 반환한다.
Thread-safe 하지만 인스턴스를 생성할 때 메모리를 많이 사용하는 클래스가 사용한다면, 해당 클래스를 사용하지 않을때 메모리 낭비가 된다.

public class Singleton1 {
    private static final Singleton1 INSTANCE = new Singleton1();
    private Singleton1() {}

    public static Singleton1 getInstance() {
        return instance;
    }
}

하지만 기본 구조는 멀티쓰레드 환경에서 안전하지 못하다. 인스턴스가 생성되지 않은 시점에 두개 이상의 쓰레드가 동시에 if (instance == null) 조건에 접근한다면 하나 이상의 인스턴스가 생성될 것이고, 이 패턴은 깨지게 됩니다.

멀티 쓰레드 환경의 Singleton 구조

public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2() {}

    public static synchronized Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }

        return instance;
    }
}

getInstance() 메서드에 synchronized 키워드를 추가해 동기화 메서드로 만들어 주면 Thread-safe 한 Singleton 클래스를 만들 수 있다.

하지만 불필요한 동기화 때문에 성능이 저하 될 수 있다. 그래서 아래와 같은 방법들을 이용해 개선할 수 있다.

double checked locking 방법 사용

public class Singleton3 {
    private static volatile Singleton3 instance;

    private Singleton3() {}

    public static synchronized Singleton3 getInstance() {
        if (instance == null) {
            synchronized (Singleton3.class) {
                if (instance == null)
                    instance = new Singleton3();
            }
        }
        return instance;
    }
}

JDK 1.5 이상부터 사용 가능한 키워드인 volatile를 추가하여 instance 변수를 메인 메모리로 올려준 다음, 동기화 이전과 동기화 이후에 if (instance == null)의 조건을 두번 사용함으로써, 두개 이상의 쓰레드가 접근할 때만 동기화 블럭으로 진입함으로 무분별한 동기화를 막을 수 있다.

만약 JDK 1.5 미만의 버전을 사용하여 volatile 키워드를 사용할 수 없다면 static inner Class를 사용해 Singleton을 구현할 수 있다.

public class Singleton4 {
    private Singleton4() { }

    private static class Singleton4Holder {
        private static final Singleton4 INSTANCE = new Singleton4();
    }

    public static Singleton4 getInstance() {
        return Singleton4Holder.INSTANCE;
    }
}

이 구조는 Singleton4 클래스의 정적 메서드인 getInstance()가 호출될 때 static inner Class로 선언된 Singleton4Holder 클래스가 초기화 하여 정적필드인 INSTANCE변수의 초기화가 일어나면서 인스턴스가 생성되고 반환합니다.

열거형을 이용한 Singleton 구현

앞서 구현한 Singleton 패턴은 리플렉션 API직렬화 & 역직렬화를 이용해서 쉽게 깨트릴 수 있다.

보통 리플렉션 API를 사용한다면 막을 수 있는 방법은 거의 없다. 하지만 enum을 사용하면 안전하게 방어를 할 수 있게 된다. 단점이 있다면 이른 초기화로 인스턴스가 미리 만들어진다는 것과 상속을 사용할 수 없다는 점이다.

public enum Singleton5 {
    INSTANCE;

    /* 이하 구현 */
    ...
}

자바와 스프링의 Singleton

자바 Runtime

자바의 기본 클래스 Runtime의 경우 이른 초기화를 이용해 미리 인스턴스를 생성하고 반환한다.

public class Runtime {
    private static final Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
}

스프링의 Bean

스프링에서 사용하는 Bean객체는 정확하게 Singleton은 아니지만 내부의 ApplicationContext에서 인스턴스를 하나씩 생성하여 관리하고 있습니다.
getBean을 이용해 생성된 Bean을 호출하면 자체적으로 관리하고 있는 인스턴스를 반환해 프레임워크에서 사용합니다.

public class SpringExample {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(SpringConfig.class);
        String hello = applicationContext.getBean("hello", String.class);
        String hello2 = applicationContext.getBean("hello", String.class);
        System.out.println(hello == hello2);
    }
}