본문 바로가기
Backend/Design Pattern

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

by Dev_Mook 2022. 6. 28.
Singleton Pattern이란?

 

싱글톤 패턴(Singleton Pattern)은 하나의 클래스에서 오직 하나의 인스턴스만 생성하게 하는 디자인 패턴이에요.

주로 데이터베이스를 연결하는 모듈을 만들 때 많이 사용하죠.

먼저 클래스를 어떻게 싱글톤 패턴(Singleton Pattern)으로 작성하는지 소스코드를 통해 알아볼게요.

 

class Singleton {
    private static Singleton INSTANCE = null;
    
    // 외부에서 Instance를 생성할 수 없게 Default Constructor의 접근제어자는 private으로 하기
    private Singleton() {}
    
    // Singleton Instance가 없는 경우에만 new를 이용하여 Instance 생성
    public static Singleton getInstance() {
        if(INSTANCE == null) INSTANCE = new Singleton();
        return INSTANCE;
    }
}

 

먼저 외부에서 생성할 수 없도록 기본 생성자의 접근제어자를 private으로 설정했네요.

이렇게 하면 외부에서는 new Singleton()을 사용할 수 없고 getInstance()를 통해서만 Instance를 가져올 수 있어요.

 

그리고 getInstance()에서는 Singleton의 INSTANCE가 생성되지 않았을 경우 new Singleton()을 이용해 Instance를 생성하고, 이미 INSTANCE가 생성되어 있는 경우에는 그냥 반환만 해주면 되요.

 

class Main {
    public static void main(String[] args) {
        // Singleton 클래스의 Instance 가져오기
        Singleton singleton_1 = Singleton.getInstance();
        Singleton singleton_2 = Singleton.getInstance();
        
        // 가져온 Singleton 클래스 Instance의 hashCode() 값 확인하기
        System.out.println("singleton_1 : " + singleton_1.hashCode());
        System.out.println("singleton_2 : " + singleton_2.hashCode());
    }
}

 

[출력 결과]
singleton_1 : 653305407
singleton_2 : 653305407

 

그리고 Main 클래스처럼 외부에서 Singleton을 이용하고 싶으면 Single.getInstance() 메소드를 이용하면 되죠.

그런데 출력 결과를 보니 Singleton의 해시코드 값이 동일하네요.

왜일까요?

 

앞서 설명한 것 처럼 Singleton 클래스의 getInstance() 메소드를 보면 알 수 있어요.

singleton_2에서는 이미 생성되어 있는 Instance를 반환해주고 있기 때문이죠.

 

Singleton Pattern의 장점은 뭘까요?

 

싱글톤 패턴을 이용하면 인스턴스를 한 번만 생성하죠.

그렇게 때문에 인스턴스를 생성하는 비용이 감소해요.

갑자기 왠 비용이냐고요?

 

음... 클래스를 사용할 때 마다 인스턴스를 생성하면 메모리에 매번 인스턴스를 저장해야 돼요.

그런데 싱글톤 패턴을 이용하면 인스턴스를 한 번만 생성하기 때문에 메모리에 한 번만 저장하면 되죠.

그래서 '비용이 감소한다'고 표현해요.

 

그리고 인스턴스를 한 번만 생성하기 때문에 다른 모듈들이 인스턴스를 공유하며 사용할 수 있어요.

하지만... 위에서 작성한 싱글톤 패턴에는 공유와 관련된 처리를 해주지 않았네요.

이거는 아래 'Instance를 공유하기 위한 코드 작성 방법' 파트에서 다시 알아볼게요.

 

그럼 Singleton Pattern의 단점은 없을까요?

 

Singleton Pattern을 사용하면 클래스 사이의 의존도가 높아질 수 밖에 없어요.

Singleton 클래스에서 이미 만들어진 인스턴스만 사용할 수 있기 때문에 외부에서 수정할 수도 없고,

Singleton 클래스가 수정되면 이를 사용하는 다른 클래스들도 수정해야하는 경우가 발생하죠.

 

의존도가 높아진 만큼 단위 테스트에도 방해요소가 될 수 밖에 없어요.

단위 테스트는 기능 단위 별로 독립적인 테스트를 할 수 있어야 하는데

Singleton Pattern은 테스트마다 독립적으로 생성할 수 없기 때문이에요.

 

그리고 가장 중요한 공유!

만약 다중 스레드에서 Singleton 클래스를 동시에 접근 한다면?

Singleton Pattern은 오직 하나의 인스턴스만 갖는 거라고 했죠?

하지만 다중 스레드에서 동시에 접근하게 되면 인스턴스가 하나 이상 생성될 수 있어요.

이런 문제를 해결하기 소스코드를 추가로 작성해야 되죠.

 

그럼에도 외부에서 클래스를 마음대로 수정하면 안되는 경우가 있기 때문에 Singleton Pattern을 사용하고 있어요.

 

Instance를 공유하기 위한 코드 작성 방법

 

그럼 이제 인스턴스를 공유하기 위해 어떻게 소스코드 작성해야 하는지 알아볼게요!

 

▶ 첫 번째. getInstance() 메소드에서 동기화 처리!

 

class Singleton {
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    // getInstance()를 접근할 때마다 동기화 해주기
    public static synchronized Singleton getInstance() {
        if(INSTANCE == null) INSTANCE = new Singleton();
        return INSTANCE;
    }
}

 

getInstance() 메소드에 synchronized 키워드를 이용해서 동기화 해줬어요.

당연히 다중 스레드에서 동시에 접근을 해도 synchronized 덕분에 Instance를 하나만 생성할 수 있어요.

하지만 getInstance() 메소드에 접근 할 때마다 동기화가 진행되기 때문에 성능이 떨어질 수 밖에 없어요.

 

 두 번째. getInstance() 메소드에서 인스턴스를 생성할 때 동기화 처리!

 

class Singleton {
    private static Singleton INSTANCE = null;
    
    private Singleton() {}
    
    // INSTANCE를 생성할 때만 동기화 해주기
    public static synchronized Singleton getInstance() {
        if(INSTANCE == null) {
            synchronized (ThreadSafeLazyInitialization.class) {
                if(INSTANCE == null) INSTANCE = new Singleton();
            }
        }
        
        return INSTANCE;
    }
}

 

이번에는 인스턴스를 생성할 때만 동기화 처리를 하도록 수정했어요.

이렇게 되면 getInstance() 메소드에서 인스턴스를 생성할 때 한 번만 동기화 처리를 해주기 때문에

매번 접근할 때마다 동기화하는 첫 번째 방식보다 더 효율적이에요.

 

세 번째. JVM의 클래스 초기화 과정을 이용한 방법

 

class Singleton {
    private Singleton() {}
    
    // JVM에 SingletonHolder 클래스가 로드되는 시점에 인스턴스 생성
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    // SingletonHolder에서 생성한 인스턴스 반환
    public static synchronized Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

 

이 방법이 가장 많이 사용되고 가장 완벽하다고 평가된다고 해요.

먼저 Singleton 클래스가 로딩되어도 SingletonHolder 변수가 없기 때문에 아직 인스턴스가 생성되지 않아요.

대신 getInstance() 메소드를 접근하게 되면 SingletonHolder가 로딩되고, 이때 인스턴스를 초기화하죠.

클래스가 로딩되는 시점에서 이미 스레드에 대한 안정성(Thread-safe)을 보장하고 성능도 우수하기 때문에 많이 사용한데요.

 

게다가 volatile이나 synchronized를 이용하지 않기 때문에 자바 버전에 상관 없이 효율적인 성능을 낼 수 있어요.