Home [Design Pattern] 싱글턴 패턴 (Singleton Pattern)
Post
Cancel

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

[DesignPattern] 싱글턴 패턴 (Singleton pattern)

학교 수업 중 하나인 설계 패턴을 수강하며 ‘JAVA 객체 지향 디자인 패턴’ 책을 정리한 포스트 이다.

정의

인스턴스가 오직 하나만 생성되는 것을 보장하고 어디에서든 이 인스턴스에 접근할 수 있도록 하는 디자인 패턴이다. 원래 싱글턴이라는 단어는 ‘단 하나의 원소만을 가진 집합’이라는 수학 이론에서 유래되었다.

  • 싱글턴 패턴은 매우 단순해 Singleton 요소 하나밖에 없다.
  • getInstance 메서드를 통해 모든 클라이언트에게 동일한 인스턴스를 반환하는 작업을 수행한다.

사용 시점

프로그램 상에 단 하나만 존재해야 하는 객체가 있다.

사용 효과

운용하는 객체를 하나만 두어 관리의 집중화를 통해 유지 보수 비용을 줄이고 높은 신뢰성을 부여할 수 있다.

구현

싱글턴 패턴을 이해하기 위해 책에서는 프린터 관리자 예제를 가지고 설명하였다.

1. 프린터 관리자 만들기

세상에는 무한으로 사용할 수 있는 자원은 거의 없다. 자원의 양은 제한되어 있으며 제한된 자원에 맞춰 사용해야 한다. 아래의 코드는 리소스를 받아 이를 출력하는 print 메서드를 제공하는 Printer 클래스이다.

1
2
3
4
5
6
7
8

public class Printer{
    public Printer(){}
    public void print(Resource r){
        ...
    }
} 

Printer 클래스를 사용해 프린터를 이용하려면 클라이언트 프로그램에서 new Printer()가 반드시 한번만 호출되어야 한다. ( 프린터는 한대 뿐이니까 ).. 이를 해소할 수 있는 직관적인 방법은 생성자를 외부에서 호출할 수 없게 하는 것이다. 바로 Printer 클래스의 생성자를 private로 선언하면 된다.

1
2
3
4
5
6
7
public class Printer{
    private Printer(){}
    public void print(Resource r){
        ...

    }
}

이렇게 하면, 외부에서 아무나 new Printer()를 사용할 수 없게 된다. 그러나 일단, 하번의 new Printer()는 호출되어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
public class Printer{
    private static Printer printer = null;
    private Printer(){}
    public static Printer getInstance(){
        if(printer == null) printer = new Printer();
        return printer;
    }
    public void print(Resource r){
        ...
    }
}

getIstance()메서드는 프린터 인스턴스가 생성되었는지를 검사한다. 만약 처음 호출되어 아직 인스턴스가 생성되지 않은 상태라면, 생성자를 호출해 인스턴스를 생성한다. 이렇게 생성된 인스턴스는 정적 변수 printer에 의해 참조가 된다. 만약 이미 인스턴스가 생성되었다면 printer 변수에서 참조하는 인스턴스를 반환한다.

그리고 getInstance메서드와 printer 변수는 static 타입(클래스 메서드, 정적 메서드, 정적 변수)로 선언되어 있다. 이는 정적 메서드 , 정적 변수라 하는데 이는 클래스 자체에 속한다는 의미이다. 따라서 클래스의 인스턴스를 통해서가 아니라 메서드를 실행할 수 있고 변수를 선언할 수 있다.

그러면 5명의 사용자가 프린터를 이용하는 상황으로 코드를 작성하면 다음과 같다.

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
public class User {
    private String name;
    public User(String name){
        this.name = name;
    }
    public void print(){
        Printer printer = Printer.getInstance();
        printer.print(this.name + "print using "+ printer.toString() +".");

    }
}

public class Printer {
    private Printer(){}
    private static Printer printer = null;
    public static Printer getInstance(){
        if(printer == null) printer = new Printer();
        return printer;
    }
    public void print(String resource){
        System.out.println(resource);
    }
}

public class Main {
    private static final int User_NUM = 5;
    public static void main(String[] args) {
        User[] user = new User[User_NUM];
        for(int i=0;i<User_NUM;i++){
            user[i] = new User((i+1)+"user");
            user[i].print();
        }
    }
}

실행해 보면 프린터 객체 하나만 사용하며 성공적으로 콘솔에 출력한 것을 볼 수 있다. threadsafe

2. 문제점

그러나 위 코드는 다중 스레드에서 Printer 클래스를 이용할 때 인스턴스가 1개 이상 생성되는 일이 발생할 수 있다.다음과 같은 시나리오에서, 문제가 발생할 수 있다.

  1. Printer 인스턴스가 아직 생성되지 않았을 때 스레드 1이 getInstance 메서드의 if문을 실행해 이미 인스턴스가 생성되었는지 확인한다. 현재 printer 변수는 null인 상태이다.
  2. 만약 스레드 1이 생성자를 호출하여 인스턴스를 만들기 전 스레드 2가 if 문을 실행해 printer 변수가 null인지 확인한다. 현재 null이므로 인스턴스를 생성하는 코드, 즉 생성자를 호출하는 코드를 실행하게 된다.
  3. 스레드 1도 스레드 2와 마찬가지로 인스턴스를 생성하는 코드를 실행하게 되면 결과적으로 Printer 클래스의 인스턴스가 2개 생성된다.
  • 위 시나리오는 경합 조건 (race condition)을 발생시킨다. 경합 조건이란 메모리와 같은 동일한 자원을 2개 이상의 스레드가 이용하려고 경합하는 현상을 말한다.

위 시나리오대로 동작하는 것을 볼 수 있도록 스레드 스케줄링을 변경하면, 코드는 다음과 같다.

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
public class UserThread extends Thread{
    public UserThread(String name){
        super(name);
    }
    public void run(){
        Printer printer = Printer.getInstance();
        printer.print(Thread.currentThread().getName()+" print using "+printer.toString()+".");
    }
}
public class Printer {
    private Printer(){}
    private static Printer printer = null;
    public static Printer getInstance(){
        if(printer == null) {
            try{
                Thread.sleep(1);
            }catch(InterruptedException e){ }
            printer = new Printer();
        }
        return printer;
    }
    public void print(String resource){
        System.out.println(resource);
    }
}
public class Main {
    private static final int User_NUM = 5;
    public static void main(String[] args) {
        UserThread[] user = new UserThread[User_NUM];
        for(int i=0;i<User_NUM;i++){
            user[i] = new UserThread((i+1)+"-thread");
            user[i].start();

        }

    }
}

실행 결과는 다음과 같다. threadsafe

이처럼 각 스레드마다 완전히 같은 Printer 인스턴스를 사용하지 않음을 볼 수 있다. 그러나 Printer 클래스가 상태를 유지해야 하는 경우에는 문제가 발생한다.

그렇다면 해결책으로는 어떤 것이 있을까?

  • 정적 변수에 인스턴스를 만들어 바로 초기화 하는 방법
  • 인스턴스를 만드는 메서드에 동기화 하는 방법

    다음은 printer라는 정적 변수에 Printer 인스턴스를 만들어 초기화 하는 방법으로 코딩한 결과이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Printer {
    private Printer(){}
    private int counter = 0;
    private static Printer printer = new Printer(); //정적 변수에 인스턴스를 만들어 바로 초기화
    public static Printer getInstance(){
        return printer;
    }
    public void print(String resource){
        counter++;
        System.out.println(resource);
    }
}


정적 변수는 객체가 생성되기 전 클래스가 메모리에 로딩될 때 만들어져 초기화가 한 번만 실행된다. 또한 정적 변수는 프로그램이 시작될 때부터 종료될때까지 없어지지 않고 메모리에 계속 상주하며 클래스에서 생성된 모든 객체에서 참조할 수 있다. 정적 변수의 이러한 특징 때문에 private static Printer printer = new Printer(); 구문이 실행되면 정적 변수 printer에 Printer 클래스 인스턴스가 바인딩되며 getInstance라는 정적 메서드를 통해 참조되는 인스턴스를 얻어올 수 있다. 이 방법은 다중 스레드 환경에서 문제를 일으켰던 if(printer == null) 라는 조건 검사 구문을 원천적으로 제거하기 위항 방법이다.

다음은 Printer 클래스의 객체를 얻는 getInstance 메서드를 동기화 하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

public class Printer {
    private Printer(){}
    private static Printer printer = null;
    public synchronized static Printer getInstance(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }
    public void print(String resource){
        System.out.println(resource);
    }
}




이는 다중 스레드 환경에서 동시에 여러 스레드가 getInstance 메서드를 소유하는 객체에 접근하는 것을 방지한다. 결과적으로 Printer 클래스의 인스턴스가 오직 하나의 인스턴스만 생성한다.
실행 결과는 다음과 같다.
threadsafe

그러나 이는 Printer 객체가 하나만 생성되었음에도 여전히 counter 변수의 값이 이상하게 출력된다. 왜냐하면 여러개의 스레드가 하나뿐인 counter 변수 값에 동시에 접근해 갱신하기 때문이다.
따라서 이 문제를 해결하려면 print 메서드의 counter 변수를 변경하는 부분도 동기화할 필요가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Printer {
    private Printer(){}
    private int counter =0;
    private static Printer printer = null;
    public synchronized static Printer getInstance(){
        if(printer == null){
            printer = new Printer();
        }
        return printer;
    }
    public void print(String resource){
        synchronized (this){
            counter++;
            System.out.println(resource+counter);
        }
    }
}

threadsafe

This post is licensed under CC BY 4.0 by the author.

[Algorithm] 트라이(trie) / 백준 14425번 문자열 집합

[모던 자바 인 액션 ch1] 자바 8,9,10,11 : 무슨 일이 일어나고 있는가?