Featured image of post 5. 싱글턴 패턴

5. 싱글턴 패턴

헤드 퍼스트 디자인 패턴

싱글턴 패턴은 특정 클래스에 객체 인스턴스가 하나만 만들어지도록 해 주는 패턴이다.

  • 객체를 쓸 때 인스턴스가 2개 이상이면 프로그램이 이상하게 돌아가는 경우
  • 자원을 불필요하게 사용하는 경우
  • 결과에 일관성이 없어지는 경우

특정 상황에서는 객체가 1개만 있어도, 혹은 1개만 있어야 문제없이 수행되는 경우 활용하는 패턴이다.

  • 스레드 풀
  • 캐시
  • 대화상자
  • 사용자 설정
  • 레지스트리 설정을 처리하는 객체
  • 로그 기록용 객체
  • 디바이스 드라이버

전역 변수에 객체를 대입하면 애플리케이션이 시작될 때 객체가 생성되는데 그 객체가 자원을 많이 차지하면서 사용하지 않는 경우에도 제어할 수 없다.

하지만 싱글턴 패턴을 활용하면 필요할 때만 객체를 만들 수 있다.

고전적인 싱글턴 패턴 구현법

생성자를 private로 만들고 static 메소드를 통해 인스턴스를 얻도록 구현하기 때문에 생성자를 통해서 새로운 인스턴스를 얻을 수 있는 방법이 없다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Singleton {
    private static Singleton uniqueInstance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance;
        }
        
        return uniqueInstance;
    }
    // ...
}

아직 인스턴스가 만들어지지 않았다면 private으로 선언된 생성자를 사용하여 Singleton 객체를 만든 다음 uniqueInstance에 그 객체를 대입한다.

이렇게 처리한다면 인스턴스가 필요한 상황이 닥치기 전까지 아예 인스턴스를 생성하지 않게 된다.

이러한 방법을 **게으른 인스턴스 생성(lazyinstantation)**이라고 부른다.

싱글턴 패턴의 정의

**싱글턴 패턴(Singleton Pattern)**은 클래스 인스턴스를 하나만 만드록, 그 인스턴스로의 전역 접근을 제공한다.

싱글턴 패턴을 실제로 정용할 때는 클래스에서 하나뿐인 인스턴스를 관리하도록 만들면 된다.

  • 그리고 다른 어떤 클래스에서도 자신의 인스턴스를 추가로 만들지 못하게 해야한다.

어디서든 해당 인스턴스에 접근할 수 있도록 전역 접근 지점을 제공한다.

  • 언제든 해당 인스턴스가 필요하면 클래스에 요청할 수 있게 만들어 놓고, 요청이 들어오면 하나뿐인 인스턴스를 건네주도록 한다.
  • 자원을 많이 잡아먹는 인스턴스가 있다면 고전적인 싱글턴 처럼 게으른 방식으로 생성되도록 구현할 경우 유용할 수 있다.
classDiagram
    class Singleton {
        static uniqueInstance: Singleton
        static getInstance() Singleton
    }

싱글턴 패턴을 사용할 때는 일반적인 클래스를 만들 때와 마찬가지로 다양한 데이터와 메소드를 사용할 수 있다.

멀티 스레딩 문제

싱글턴 패턴을 사용할 때 인스턴스를 동시에 요청하는 상황에서 독립적인 인스턴스를 제공하지 못해 문제가 발생한다.

이러한 문제는 자바는 getInstancesynchronized 연산자를 추가하면, 메소드 사용 완료 후 요청하기 때문에 쉽게 해결이 가능하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class Singleton {
    private static Singleton uniqueInstance;
    
    private Singleton() {}
    
    public static synchronized Singleton getInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        
        return uniqueInstance;
    }
}

더 효율적으로 멀티스레딩 문제 해결하기

synchronized 연산자를 getInstance에 적용하는 방법은 동기화로 인해 성능 저하가 발생할 수 있고, 싱글턴 패턴에서 멀티스레딩 문제가 발생하는 시점은 메소드가 시작되는 때 뿐이다.

  • uniqueInstance 변수에 할당이 완료되면, 항상 새로운 인스턴스를 생성하지 않기 때문에 동기화 처리는 불필요한 오버헤드를 증가시킨다.

getInstance 메소드의 성능이 중요하지 않다면 큰 문제가 이닐 수 있지만 메소드를 동기화하면 성능이 100배 정도 저하된다. 따라서 병목이 된다면 별도의 처리가 필요하다.

인스턴스 처음부터 만들기

애플리케이션에서 Singleton의 인스턴스를 생성하고 계속 사용하거나 인스턴스를 실행 중에 수시로 만들고 관리하기가 번거롭다면 아래와 같은 방식으로 처음부터 인스턴스를 만들 수 있다.

1
2
3
4
5
6
7
8
9
public class Singleton {
    private static Singleton uniqueInstance = new Singleton();

    private Singleton() {}

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

이 방법은 클래스가 로딩될 때 JVM에서 Singleton의 하나뿐인 인스턴스를 생성해주며, JVM에서 하나뿐인 인스턴스를 생성하기 전까지 그 어떤 스레드도 uniqueInstance 정적 변수에 접근할 수 없다.

DCL을 통한 동기화 줄이기

**DCL(Double-Checked Locking)**을 사용하면 인스턴스가 생서오디어 있는지 확인한 다음 생성되어 있지 않았을 때만 동기화 할 수 있다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public class Singleton {
    private volatile static Singleton uniqueInstance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new Singleton();
                }
            }
        }
        
        return uniqueInstance;
    }
}
  • volatile 키워드를 사용하면 멀티 스레딩을 쓰더라도 초기화되는 과정이 올바르게 진행된다.
  • DCL은 자바 5보다 낮은 버전을 사용한다면 동기화가 제대로 안될 수 있다.

싱글턴 패턴의 문제

  • 모든 메소드와 변수가 static으로 선언된 클래스를 만들어도 결과적으로 같지만 자바의 정적 초기화를 처리하는 방법으로 인해 디버깅이 어려울 수 있다.
  • 다른 이름의 클래스 로더가 2개 이상이라면 같은 클래스르 ㄹ여러 번 로딩하게되어 싱글턴에 적용 시 인스턴스가 여러개 만들어 질 수 있다.
  • 리플렉션, 직렬화, 역직렬화도 싱글턴에서 문제가 될 수 있다.
  • Singleton에 의존하는 객체는 전부 하나의 객체에 단단하게 결합되므로 느슨한 결합 원칙에 위배된다.

enum 활용하기

언급된 동기화 문데, 클래스 로딩 문제, 리플렉션, 직렬화와 역질렬화 문제 등은 enum으로 싱글턴을 생성해서 해결할 수 있다.

1
2
3
4
5
6
7
8
9
public enum Singleton {
    UNIQUE_INSTANCE;
}

public class SingletonClient {
    public static void main(String[] args) {
        Singleton singleton = Singleton.UNIQUE_INSTANCE;        
    }
}

정리

  • 싱글턴 패턴은 클래스 인스턴스를 하나만 만들고 그 인스턴스로의 전역 접근을 제공하는 방법.
  • 어떤 클래스에 싱글턴 패턴을 적용하면 그 클래스의 인스턴스가 1개만 있도록 할 수 있다.
  • 싱글턴 패턴을 사용하면 하나뿐인 인스턴스를 어디서든지 접근할 수 있도록 할 수 있다.
  • 자바에서 싱글턴 패턴을 구현할 때는 private 생성자와 정적 메소드, 정적 변수를 사용한다.
  • 멀티 스레드를 사용하는 애플리케이션에서는 속도와 자원 문제를 파악해보고 적절한 구현법을 사용한다.
    • 사실 모든 애플리케이션에서 멀티스레딩을 쓸 수 있다고 생각해야한다.
  • DCL을 써서 구현하면 자바 5 이전에 나온 버전에서는 스레드 관련 문제가 생길 수 있다.
  • 클래스 로더가 여러 개 있으면 싱글턴이 제대로 작동하지 않고, 여러개의 인스턴스가 생길 수 있다.
  • 자바의 enum을 쓰면 간단하게 싱글턴을 구현할 수 있다.