준비하는 대학생

[Design Pattern] Singleton Pattern(싱글턴 패턴) 본문

Programming/Design pattern

[Design Pattern] Singleton Pattern(싱글턴 패턴)

Bangii 2023. 6. 2. 08:19

Singleton 패턴이란?

Singleton 패턴은 객체 지향 프로그래밍에서 주로 사용되는 디자인 패턴 중 하나입니다. Singleton 패턴은 클래스의 특정 인스턴스가 한 개만 만들어지도록 보장하고, 그 인스턴스에 쉽게 접근할 수 있는 글로벌한 접근점을 제공합니다. 이를 통해 다른 객체들이 데이터를 공유할 수 있게 하며, 자원의 효율적인 관리를 할 수 있습니다.

고전적인 Singleton 패턴

싱글턴 패턴은 특정 클래스의 인스턴스가 하나만 생성되도록 보장하고, 이 인스턴스에 전역적으로 접근할 수 있게 하는 디자인 패턴입니다. 이를 사용하면 여러 객체들이 데이터를 공유할 수 있게 만들 수 있습니다.

Java로 Singleton 패턴을 구현하는 고전적인 방법은 다음과 같습니다.

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

고전적인 Singleton 패턴의 문제점

위의 구현은 심플하지만 멀티스레드 환경에서는 제대로 동작하지 않을 수 있습니다. 두 개의 스레드가 동시에 getInstance() 메서드에 진입하여 uniqueInstance == null 확인 후, 둘 다 새로운 인스턴스를 생성하게 될 수 있기 때문입니다.

이 문제를 해결하기 위한 한 가지 방법은 getInstance() 메서드를 동기화하는 것입니다.

public class Singleton {
    private static Singleton uniqueInstance;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

하지만 이렇게 동기화를 하게 되면 성능이 저하되는 단점이 있습니다.

문제 해결을 위한 두 가지 방법

이런 문제를 해결하기 위해 'eager initialization' 방식과 'double-checked locking' 방식을 사용할 수 있습니다.

1) Eager initialization 방식 : 클래스가 로딩될 때 인스턴스를 미리 생성해 두는 것

public class Singleton {
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

2) Double-checked locking 방식: getInstance() 메서드 내에서 필요한 경우(처음)에만 동기화하는 방식

public class Singleton {
    private static volatile Singleton uniqueInstance;

    private Singleton(){}
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

*volatile 키워드

Java에서 volatile 키워드는 멀티스레딩 환경에서 변수의 읽기와 쓰기를 원자적(atomic)으로 수행하도록 보장합니다. 이는 변수가 CPU 캐시가 아닌 메인 메모리에서 읽고 쓰이게 하여, 여러 스레드가 해당 변수의 값을 일관성 있게 볼 수 있도록 합니다.

volatile 키워드가 붙은 변수에 대한 모든 읽기와 쓰기 작업은 메모리 배리어(memory barrier)를 통해 수행되므로, 쓰기 작업이 완료되기 전에 읽기 작업이 일어나는 것을 방지합니다.

Singleton 패턴에서 'double-checked locking'을 사용할 때, uniqueInstance 변수에 volatile 키워드를 사용하여 인스턴스 생성 과정 중에 다른 스레드가 부분적으로 생성된 인스턴스를 보는 것을 방지합니다.

메모리 가시성
: Java에서 여러 스레드가 동작하면, 각 스레드는 자신만의 작업 메모리를 가지고, 이 메모리에서 변수를 읽고 쓰게 됩니다. 이 때, 한 스레드에서 변경된 변수의 값이 다른 스레드에게 언제 어떻게 보이는지를 결정하는 것이 메모리 가시성입니다. volatile 키워드를 사용하면, 변수의 변경이 즉시 메인 메모리에 반영되므로, 다른 스레드에서도 해당 변경을 즉시 볼 수 있습니다

*synchronized 키워드

Java의 synchronized 키워드는 여러 스레드가 동시에 접근할 수 있는 공유 자원에 대한 동시 접근을 제어하는 역할을 합니다. 이 키워드가 붙은 메소드 또는 블록은 한 번에 하나의 스레드만 접근할 수 있게 하여, 공유 데이터의 일관성을 유지하고 경쟁 상태(race condition)를 방지합니다.

Singleton 패턴에서 getInstance() 메서드를 synchronized로 선언하면, 한 번에 하나의 스레드만이 해당 메서드를 실행할 수 있습니다. 하지만 이 방식은 성능 저하를 일으킬 수 있습니다. 따라서 'double-checked locking'에서는 getInstance() 메서드 전체를 동기화하는 대신, 인스턴스가 아직 생성되지 않았을 때만 synchronized 블록을 실행하여 성능 저하를 최소화합니다.

동기화(synchronization)
: 여러 스레드가 동시에 공유 메모리에 접근하여 데이터의 일관성을 해칠 수 있는 상황을 방지하는 것이 동기화입니다. synchronized 키워드를 사용하면, 한 번에 하나의 스레드만이 synchronized로 표시된 코드 블록 또는 메서드에 접근할 수 있게 됩니다.

이 두 방법은 각각 성능 문제와 lazy initialization을 유지하는 장점을 가지고 있습니다.

Enum을 이용한 Singleton 패턴

그럼에도 불구하고 Singleton 패턴은 아직도 몇 가지 문제점이 있습니다. 가장 큰 문제는 바로 'serialization'입니다. 객체가 직렬화되었다가 다시 역직렬화될 때, 새로운 인스턴스가 생성될 수 있습니다. 이 문제를 해결하기 위해선 `readResolve()` 메서드를 구현해야 합니다. 하지만 이것도 코드가 복잡해지고, 실수로 버그를 만들 가능성이 높아집니다. 하지만 Java에서는 Enum을 이용해 이런 문제를 해결할 수 있습니다. Enum은 Java가 자동으로 직렬화를 관리하며, 인스턴스가 하나만 생성되도록 보장합니다.

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        // do something
    }
}

이렇게 Enum을 이용하면 직렬화 문제가 해결될 뿐만 아니라, 리플렉션(reflection)을 통한 인스턴스 생성도 막을 수 있습니다. 그리고 코드도 훨씬 간결해집니다.

결론

싱글턴 패턴은 자원을 효율적으로 관리할 수 있도록 도와주지만, 동시성 문제나 직렬화 문제 등 여러 가지 주의해야 할 점이 있습니다. 이런 문제점들을 이해하고, 적절한 해결책을 선택하는 것이 중요합니다.

Java에서는 Enum을 이용한 싱글턴 패턴 구현이 가장 안전하고 간결하다고 권장되고 있습니다. 그러나 어떤 방식을 선택하든 그 방식의 장단점을 이해하고 있는 것이 중요합니다.

Comments