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

2021. 3. 5. 19:19개발/디자인패턴

Head First Design Patterns를 읽고 학습한 내용을 작성합니다. 

 

0. 싱글턴 패턴의 정의와 필요성


싱글턴 패턴은 해당 클래스의 인스턴스가 하나만 만들어지고, 어디서든지 그 인스턴스에 접근할 수 있도록 하기 위한 패턴입니다.

사실 얼핏보기에는 이게 패턴까지 될필요가 있나싶다. 전역변수 하나면 쉽게 해결될 문제인것같다.

하지만 인스턴스를 전역변수로 셋팅하면 하면 해당 파일이나 화면이 로딩 될때 항상 생성된다. 

전혀 사용되지 않는 경우에도 말이다. 거기다 해당 인스턴스가 상당히 많은 양의 리소스를 소모한다면 사용하지도 않는데 성능까지 저하시킬수도 있다. 또한, 소스코드 어딘가에 인스턴스를 추가로 생성하는 코드가 있다면 이를 방지할 방법이 없다.

 

 

1. 고전적인 싱글턴 패턴


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Singleton{
    
    private static Singleton uniqueInstance;
    
    // 생성자를 private로 셋팅함
    private Singleton(){}
    
    // 정적 메소드
    public static Singleton getInstance(){
        if( uniqueInstance == null){
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

Line 3 : private로 셋팅함으로써 외부에서 해당 변수를 사용하지 못하게 했다. static(정적) 변수이므로 uniqueInstance라는 이름으로 유일성을 가진다. 

Line 6 : 생성자를 private로 셋팅함으로써 Singleton 클래스가 인스턴스화 되지못하도록 한다. 

Line 9 : getInstance라는 정적 메소드를 작성한다. 외부에 공개하기 위해 public으로 셋팅한다. 정적 메소드이기 때문에 접근을 위해선 Singleton.getInstance() 형태로 사용한다. 

Line 10~13 : uniqueInstance 변수가 셋팅되어 있지 않다면 Singleton 인스턴스를 return 하도록한다. 

 

위 방식은 생성자를 private로 함으로써 인스턴스 생성을 막고, 내부 메소드 getInstance를 통해 인스턴스를 생성할 수 있도록했다.

 

 

2. 고전적인 싱글턴 패턴 만들기


 

초콜렛을 끓이기 위한 ChocolateBoiler 클래스를 하나 만들었다.

empty와 boiled 변수를 사용하여 초콜릿보일러의 상태를 기억하고 있다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
 
    public ChocolateBoiler(){
        empty = true;
        boiled = false;
    }
    
    // 채우기
    public void fill(){
        // 비어있다면 
        if(isEmpty()){ 
            empty = false//내용물을 채운다.
            boiled = false// 끓이진 않는다.
        }
    }
 
    // 비우기
    public void drain(){
        if(!isEmpty() && !isBoiled()){ // 비어있지않고, 끓고있는게 아니라면
            empty = true// 내용물을 비운다
        }
    }
    
    // 끓이기
    public void boil(){
        if(!isEmpty() && !isBoiled()){ // 비어있지않고, 끓고있는게 아니라면
            boiled = true// 내용물을 끓인다
        }
    }
 
    public boolean isEmpty(){
        return empty;
    }
 
    public boolean isBoiled(){
        return boiled;
    }
 
 
}
 

 

 

 

전체 코드에서 ChocolateBoiler 인스턴스를 하나로 유지해야한다면 다음과 같이 코드를 수정할 수 있다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    // ChocolateBoiler static 객체 생성
    private static ChocolateBoiler uniqueInstance;
    
    // public --> private
    private ChocolateBoiler(){
        empty = true;
        boiled = false;
    }
    
    // 인스턴스 생성을 위한 static 메소드 정의
    public static ChocolateBoiler getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }    
 
 
    // 채우기
    public void fill(){
        // 비어있다면 
        if(isEmpty()){ 
            empty = false//내용물을 채운다.
            boiled = false// 끓이진 않는다.
        }
    }
 
    // 비우기
    public void drain(){
        if(!isEmpty() && !isBoiled()){ // 비어있지않고, 끓고있는게 아니라면
            empty = true// 내용물을 비운다
        }
    }
    
    // 끓이기
    public void boil(){
        if(!isEmpty() && !isBoiled()){ // 비어있지않고, 끓고있는게 아니라면
            boiled = true// 내용물을 끓인다
        }
    }
 
    public boolean isEmpty(){
        return empty;
    }
 
    public boolean isBoiled(){
        return boiled;
    }
 
 
}
 
cs

 

 

3. 고전적 싱글턴 패턴의 문제점


2번에서 ChocolateBoiler를 싱글턴 패턴으로 코드를 수정했다. 여기서 발생할 수 있는 문제점을 살펴본다.

 

2개의 스레드를 생성하고 아래의 코드를 각 스레드가 실행하면 어떻게 될까?

1
2
3
4
5
ChocolateBoiler boiler = ChocolateBoiler.getInstance();
 
boiler.fill();
boiler.boil();
boiler.drain();
cs

 

작성한 코드를 2개의 쓰레드에서 동시에 실행한다면 "항상 인스턴스가 1개만 생성됨"을 보장 할 수 없다.

쓰레드의 코드 컴파일이 아래와 같은 순서로 진행된다면 2개의 ChocolateBoiler 인스턴스가 생성될 수 있다.

 

4. 멀티스레딩 문제 해결하기


 

 

1. sysnchronized

자바의 sysnchronized는 해당 메소드에 멀티쓰레드에 의한 동시접근을 막아준다.

단 여기서 기억해야 할것은 getInstance메소드에 sysnchronized를 적용하면 해당 메소드가 속한 Class에 lock이 걸린다. 

즉, getInstance메소드를 수행하는 동안에는 fill()이나 drain()같은 메소드에 접근할 수 없다. 

getInstance메소드 수행시간이 오래 걸린다면 성능저하와 직결될 수 있다. ( 속도에 큰 차이가 없다면 그냥 쓰는것도 괜찮음 )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    
    private static ChocolateBoiler uniqueInstance;
    
    
    private ChocolateBoiler(){
        empty = true;
        boiled = false;
    }
    
    // sysnchronized 적용
    public static sysnchronized ChocolateBoiler getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }    
 
    // 생략....
 
}
cs

 

2. 클래스 내부에서 미리 인스턴스를 생성하기 

 

또다른 해결법은 프로그램이 실행될때 인스턴스를 미리 생성해버리는 것이다. 

다만, ChocolateBoiler 인스턴스를 사용하지 않는다면 메모리 낭비가 발생한다.

[ 진짜 극한의 상황이 아니라면 메모리는 넉넉한편이니 클래스에 lock을 걸어버리는 1번보다 2번을 쓰는게 낫겠다. ]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    
    // 컴파일 과정에서 
    private static ChocolateBoiler uniqueInstance = new ChocolateBoiler();
    
    
    private ChocolateBoiler(){
        empty = true;
        boiled = false;
    }
    
    // 로직 제거
    public static sysnchronized ChocolateBoiler getInstance(){
        return uniqueInstance;
    }    
 
    // 생략....
 
}
cs

 

2번 방법은 얼핏보면 전역변수를 사용하는 것과 차이가 없어보인다. 

전역변수도 컴파일시에 인스턴스가 생성된다. 다만, ChocolateBoiler 인스턴스가 추가적으로 생성되는것을 막을 수 없다. 

 

 

3.DCL(Double-Checking Locking)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ChocolateBoiler {
    private boolean empty;
    private boolean boiled;
    
    
    private volatile static ChocolateBoiler uniqueInstance;
    
    
    private ChocolateBoiler(){
        empty = true;
        boiled = false;
    }
    
    public static ChocolateBoiler getInstance(){
        if(uniqueInstance == null){
            uniqueInstance = new ChocolateBoiler();
        }
        return uniqueInstance;
    }    
 
    // 생략....
 
}
 
cs

volatile을 변수앞에 붙여주면 해당 변수를 Main Memory에 올려두고, 멀티 쓰레드가 uniqueInstance에 접근할때 Main Memory에 올라가 있는 uniqueInstance를 공유해서 사용하도록 해준다.