개발 & 데이터베이스/CS

싱글톤 패턴(Singleton pattern) 이해하기 (사용하는 이유와 구현 방법)

K.두부 2023. 3. 23. 14:06
반응형

싱글톤 패턴은 생성자가 여러 번 호출 되더라도 실제로 생성되는 객체는 하나이며 최초로 생성된 이후에 호출된 생성자는 이미 생성한 객체를 반환시키도록 만드는 디자인 패턴이다. 간단하게 설명하자면 단 하나의 인스턴스만 생성하여 사용하는 디자인 패턴을 의미한다.

 

✅ 디자인 패턴

프로그램을 설계할 때 발생했던 문제점들을 객체 간의 상호 관계 등을 이용하여 해결할 수 있도록 하나의 규약 형태로 만들어 놓은 것을 의미함. 일종의 설계 기법으로 SW 재사용성, 호환성, 유지 보수성을 위함이다.

 

✅ 싱글톤을 사용하는 이유

객체는 생성할 때마다 메모리 영역을 할당받아야한다. 싱글톤을 사용하게 되면 한 번의 객체를 생성하기 때문에 메모리 낭비를 방지할 수 있다.

 

또한, 이미 생성된 인스턴스를 활용하기 때문에 속도 측면에서도 유리하고, 전역으로 사용하는 인스턴스이므로 다른 클래스의 인스턴스들이 데이터를 공유할 수 있다.

 

싱글톤 패턴 구현

public class Singleton {
    private static Singleton instance;
    
    // private 생성자로 외부에서의 객체 생성을 막음.
    private Singleton() {}
    
    // 외부에서는 getInstance()로 instance를 반환
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        
        return instance;
    }
}

싱글톤 패턴의 기본 구현이라고 볼 수 있다.

 

자바에서는 생성자를 private 으로 선언해서 외부에서의 객체 생성을 막고, getInstance() 메서드를 이용해서 instance 값을 반환해준다.

 

멀티쓰레드에서의 싱글톤 패턴

멀티쓰레드 환경에서는 동시성 문제가 발생한다.

instance가 null 값으로 있을 때 동시에 getInstance() 메서드를 실행할 경우에 여러 개의 instance가 생성될 수 있다.

 

동시성을 문제를 해결하는 방법에는 여러가지가 존재한다. 

방법에 따라서 장단점이 존재하므로 상황에 따라서 잘 사용하면 되겠다.

 

1. synchronized 메서드 선언

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

synchronized 키워드를 사용해서 getInstance() 메서드를 동기화해주면 최초로 접근한 쓰레드가 해당 메서드 호출을 종료할 때까지 다른 쓰레드가 접근하지 못하도록 lock을 걸어준다.

 

동시성 문제를 해결할 수 있는 쉽고 빠른 방법이지만 메서드가 호출될 때마다 lock을 걸어주므로 비교적 큰 성능 저하가 발생한다.

 

 

2. DCL (Double Checked Locking) 방식

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

위처럼 getInstance() 메서드를 호출할 때마다 lock을 거는 방식이 아니라 생성된 인스턴스가 없을 경우에만 lock을 걸어 인스턴스 생성 과정에서 동시성 문제를 해결할 수 있다.

 

하지만 완벽한 방법이 아니다.

쓰레드 A와 쓰레드 B가 있다고 가정해보자.

 

쓰레드 A가 최초에 getInstance() 메서드에 접근하여 instance가 null 값인 것을 확인하고 synchronized 블록에 진입한다.

 

instance가 null 값이기 때문에 instance = new Singleton(); 을 실행한다.

 

instance = new Singleton(); 를 처리할 때 instance 라는 공간을 먼저 할당한 뒤에 아직 새로운 Singleton 객체의 레퍼런스를 미리 할당해둔 instance에 할당하지 않은 상태일 때 instance에 접근이 가능하다.

 

그렇기 때문에 낮은 확률로 쓰레드 B가 getInstance() 메서드를 접근했을 때 instance가 null이 아니게 되어 Singleton 객체를 생성하지 않고 넘어가는 문제가 발생할 수 있다.

 

 

3. DCL 방식에 volatile 키워드 사용 (jdk 1.5 이상)

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

volatile 키워드를 사용하여 instance를 선언하게 되면 instance에 값을 할당, 수정하거나 읽어들일 경우에 CPU 캐시를 거치지않고 바로 메인 메모리부터 읽어서 2번에서 언급한 변수 값 불일치 문제를 해결할 수 있다.

 

 

4. static 초기화 (이른 초기화)

public class Singleton {
    private static Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

이 방법은 멀티쓰레드 환경에서 발생하는 동시성 문제를 완벽하게 해결하고 소스도 간결하다. 쓰레드가 getInstance() 메서드를 호출하는 시점이 아닌 클래스가 로딩되는 시점에 instance를 생성하기 때문에 하나만 생성되는 것을 보장한다.

 

하지만 해당 클래스가 100초, 5분, ··· 혹은 바로 사용하거나 아예 사용하지않을 수 있다. 즉, instance가 필요한 시점을 정확하게 알 수 없기 때문에 메모리 낭비 문제점이 발생한다.

 

 

5. LazyHolder 방식

public class Singleton {
    private Singleton() {}
    
    private static class LazyHolder {
        // static 키워드 선언으로 클래스 로딩 시점에서 한 번만 호출
        // final 키워드 선언으로 다시 값을 할당되지 않도록함.
        public static final Singleton INSTANCE = new Singleton();
    }
    
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

LazyHolder라는 이름의 inner class를 선언해서 Singleton 클래스가 최초 클래스 로딩 단계에서 로드가 되더라도 LazyHolder 클래스에 대한 변수를 가지고 있지 않아 함께 초기화되지 않는 점을 이용한 방법이다.

 

그렇기 때문에 getInstace() 가 호출될 때  LazyHolder 클래스가 로딩되며 인스턴스를 생성하게 되면서 위에서 발생하는 초기화 문제를 해결할 수 있다.

 

실제로 많이 사용하고 현재까지 가장 완벽하다고 평가받는 방법이다.

 

 

 

 

 

 


● 참고

[JAVA 디자인패턴] Singleton 패턴의 모든 것 (멀티스레드 고려) (tistory.com)

싱글톤 패턴(Singleton pattern) | 👨🏻‍💻 Tech Interview (gyoogle.dev)

반응형