티스토리 뷰

디자인 패턴

Singleton Pattern

기내식은수박바 2021. 10. 2. 15:55
반응형

싱글톤 패턴?

인스턴스가 오직 한 개만 생성된다는 것을 보장하고 이 인스턴스를 어디서나 사용할 수 있도록 하는 디자인 패턴

  • 싱글톤 (Singleton) : '단 하나의 원소만을 가진 집합' 이라는 수학 이론에서 유래되었다고 한다.

  • 단 하나의 인스턴스만을 생성하는 책임이 있으며, getInstance()를 통해 동일한 인스턴스를 반환한다.

 

 

등장 배경

프린터기가 있다고 생각해보자. 그리고 리소스를 입력 받아 이를 출력하는 print 메서드를 제공하는 Printer 클래스가 있다고 해보자.

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

 

프린터기는 한 개밖에 없다고 가정했을 때, new Printer() 는 외부에서 한 번만 호출 되도록 해야한다.

  • 직관적인 방법 : 생성자를 private으로 바꿔 생성자를 외부에서 호출할 수 없게 한다.
public class Printer {
    private Printer() { }
    
    public void print(Resource resource) {
        ...
    }
}

 

이제 외부에서 생성자를 호출할 수 없게 된다. 하지만 하나의 인스턴스는 필요하므로 인스턴스를 만들어 외부에 제공할 수 있는 메서드가 필요하다.

  • 인스턴스 외부 제공 메서드 : getPrinter()
    1. 인스턴스가 생성되어 있는지 검사한다.
    2. 인스턴스가 생성되어 있지 않다면 생성자를 호출해 인스턴스를 생성한다.
    3. 인스턴스가 생성되어 있다면 printer 변수에서 참조하는 인스턴스를 반환한다.
public class Printer {
    private static Printer printer = null;
    private Printer() { }
    
    public static Printer getPrinter() {
        if (printer == null)
            this.printer = new Printer();
            
        return this.printer;
    }
    
    public void print(Resource resource) {
        ...
    }
}

 

 

Why static?

코드에서 getPrinter 메서드와 printer 변수가 static으로 선언되어 있다.

  • 구체적인 인스턴스에 속하는 것이 아니라 클래스 자체에 속한다는 것.
  • 이는 인스턴스를 통하지 않고 클래스를 통해 메서드를 실행하고 변수를 참조할 수 있다는 것.

 

우리의 목적은 단 하나의 인스턴스를 생성해 이를 어디에서든 참조할 수 있게 하는 것이므로 최초에 인스턴스를 생성하기 위해서는 getInstance 메서드를 호출해야 한다.

만약 static 메서드로 선언되어 있지 않다고 해보자. 그렇다면 아래와 같이 실행해야 할 것이다.

instance.getInstance();

 

생성되지 않은 instance 객체는 null일 것이고, 이는 getInstance 메서드를 호출할 수 없다라는 것이기 때문에 모순이다.

printer 변수도 static으로 선언되어 있기 때문에 Printer 클래스에서 생성된 모든 인스턴스가 공유하는 변수가 되는 것이다.

 

이제 5명의 사용자가 프린터를 사용하는 상황일 때, 코드는 아래와 같을 것이다.

public class User {
    private String name;
    
    public User(String name) {
        this.name = name;
    }
    
    public void print() {
        Printer printer = Printer.getPrinter();
        printer.print(this.name + " print using " + printer.toString() + ".");
    }
}

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

public class Client {
    private static final int userNum = 5;
    public static void main(String[] args) {
        User[] user = new User[userNum];
        
        for (int i = 0; i < userNum; ++i) {
            user[i] = new User((i + 1) + "-user"); // User인스턴스 생성
            user[i].print();
        }
    }
}

 

 

문제점

다중 쓰레드에서 Printer 클래스를 이용할 때, 인스턴스가 1개 이상 생성되는 경우가 발생할 수 있다. 아래 시나리오를 보자.

참고로 아래 시나리오는 Race Condition을 발생시킨다.

  • Race Condition : 2개 이상의 쓰레드가 하나의 자원 (ex. 메모리) 을 차지하기 위해 경쟁하는 현상
  1. Printer 인스턴스가 아직 생성되지 않았을 때, 쓰레드 1이 getPrinter 메서드의 if문을 실행하여 이미 인스턴스가 생성되어 있는지 확인한다. 현재 printer 변수는 null 이다.
  2. 만약 쓰레드 1이 생성자를 호출해 인스턴스를 만들기 전, 쓰레드 2가 getPrinter 메서드의 if문을 실행하여 printer변수가 있는지 확인했을 때는 아직 printer 변수가 null이므로 생성자를 실행할 것이다.
  3. 쓰레드 1도 쓰레드 2와 마찬가지로 생성자를 호출하면 결국 2개의 Printer 인스턴스가 생성될 것이다.

이는 어떤 상태값 (ex. count) 을 유지해야 할 때, 생성된 각 인스턴스마다 값을 가지게 되므로 문제가 야기시킬 것이다.

 

 

해결책

1. static 변수에 인스턴스를 만들어 바로 초기화

public class Printer {
    private static Printer printer = new Printer();
    private int count = 0;
    private Printer() { }
    
    public Printer getPrinter() {
        return this.printer;
    }
    
    public void print(String str) {
        ++count;
        System.out.println(str):
    }
}
  • static 변수는 인스턴스가 생성되기 전, 클래스가 메모리에 로딩될 때 딱 한 번만 초기화가 실행된다.
  • 또한, 프로그램이 종료되기 전까지는 제거되지 않고 클래스에서 생성된 모든 인스턴스에서 참조할 수 있다.

 

2. 인스턴스를 만드는 메서드에 동기화

public class Printer {
    private static Printer printer = null;
    private int count = 0;
    private Printer() { }
    
    public synchronized static Printer getPrinter() {
        if (printer == null) 
            this.printer = new Printer();
        
        return this.printer;
    }
    
    public void print(String str) {
        synchronized(this) {
            ++count;
            System.out.println(str):
        }
    }
}

 

  • 메서드를 동기화하는 경우, count값에는 여러 쓰레드가 동시에 접근하여 값을 갱신할 수 있기 때문에 동일하게 동기화 처리를 해주어야 한다.

 

 

Reference

  • 도서 'UML과 GoF 디자인 패턴 핵심 10가지로 배우는 JAVA 객체 지향 디자인 패턴'
반응형

'디자인 패턴' 카테고리의 다른 글

Factory Method Pattern  (0) 2021.12.04
Decorator Pattern  (0) 2021.10.11
Template Method Pattern  (0) 2021.09.26
Strategy Pattern  (0) 2021.09.15
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
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
글 보관함