독서 기록/디자인패턴

[디자인패턴] 헤드퍼스트 디자인패턴 Chap.5 (Feat. 싱글톤 패턴)

JoJobum 2022. 10. 17.

인스턴스 1개만 있어도 되는 객체가 많음 

예를들어 스레드 풀, 캐시, 대화상자, 디바이스 드라이버 등등 

이런 경우 오히려 2개 이상이면 오작동, 일관성이 깨짐 혹은 자원 낭비 등의 문제가 발생할 수 있음

 

싱글톤 패턴: 클래스 인스턴스를 하나만 만들고, 그 인스턴스로의 전역 접근을 제공한다.

 

즉, 객체 인스턴스를 전역 변수처럼 어디서든 엑세스할 수 있게 함

또한 사용하지 않으면 자원이 낭비되는 전역 변수의 단점을 감수하지 않는다.

 

게으른 인스턴스 생성을 통해 필요할 인스턴스 생성하여 자원 낭비 막음

 

싱글톤 패턴을 제대로 구현하면 new를 써서 만들면 안됨

 

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

 

싱글톤을 구현하기 위한 3가지 규칙 

1. private 접근제어자(access modifier)를 가진 생성자 - private Singleton(){} 

클라이언트들이 new를 통해 인스턴스를 만드는 것을 막기 위해

 

2. private 생성자의 역할을 대신하는 static getInstance 메소드 - public static Singleton getInstance()

우리가 원하는 것은 getInstance()로 바로 인스턴스를 얻는 것 

singleton.getInstance() 가 아님!

왜냐하면 우리가 getInstance()를 애초에 사용하려고한 것이 singleton 객체가 없을때 가져오기 위한 것이였기 때문

고로 인스턴스와 상관없이 getInstnace()를 호출하기 위해 static 메소드여야 함 

 

3. private static 멤버 변수 - private static Singleton uniqueInstance; 

static 메소드에서 사용되어야하기 때문

private이 아니라면 외부에서 접근이 되어버림 => uniqueInstance 를 null로 바꾸고 다시 getInstance()를 호출하면 새로 만들 수 있음
    
 

멀티 쓰레드 환경에서의 싱글톤

위의 방식대로 싱글톤을 구현한 채로 멀티 쓰레드 환경에서 실행을 하면 다음과 같이 싱글톤의 규칙을 어기는 상황이 온다

 

synchronized 키워드를 사용하는 방법 

 

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

synchronized 키워드를 사용하면 메소드의 락을 걸어서 락을 획득한곳만 사용할 수 있게 만든다. 

락은 전체 클래스에 하나만 존재

=> 1번 쓰레드가 락을 획득하면 2번 쓰레드는 1번 쓰레드가 작업을 마치고 락을 풀어줄 때까지 기다려야하고 반대로되면 1번 쓰레드가 2번 쓰레드를 기다림

=> 처음이 아니면 uniqueInstnace가 null일 수 없음, 멀티 쓰레드로 발생하는 문제 해결

 

But 락을 사용함으로서 발생하는 오버헤드가 너무 큼, uniqueInstance가 null이 아니게 되면 그냥 들고나가면 되는데 불필요한 락 경쟁을 계속 해야함

 

처음 Singletone 클래스에서 uniqueInstance 를 선언할 때 인스턴스를 만들어버리는 방법 (Use Eager Instantiaion)

 

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

앞서 말한 불필요한 락 경쟁도 없애고 멀티 쓰레드에서도 문제 없음...

But, getInstance()를 하지 않아도 인스턴스를 만들어버려서 사용하지 않는다면 메모리 낭비를 발생시킴(전역 변수와 동일한 단점)

 

=> 읽을 때는 락이 거추장스럽고... 인스턴스를 쓸 때는 락이 필요하고

=> 쓸 때만 락을 쓰자

 

위의 2가지 방법을 혼용한 방법 (Double Checked Locking)

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

synchronized 안에서 한번더 검사하는 이유는 락을 경쟁하며 기다리는 대기자들이 이미 첫번째 if문을 통과했기에 첫번째 쓰레드가 인스턴스를 생성하고 2번째 쓰레드가 멈추는 것이아니라 그냥 순서대로 덧씌우기 때문

이러한 방법을 사용하면 거의 대부분의 상황에서 문제없음 하지만... concurrnecy 에러의 가능성이 존재

아래의 시나리오같은 문제가 발생할 수 있음

이러한 문제를 volatile 키워드를 추가함으로서 해결

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

volatile 키워드가 쓰레드들이 보는 값을 동일하게 만들어줌으로서 앞서 말한 문제 해결

volatile 키워드의 문제로 java 1.5 이상에서만 잘 돌아감, 이전에는 잘될수도 안될수도...

[Java] volatile 키워드 - SangWoo Blog (sangwoo0727.github.io)

 

반응형

댓글