싱글턴 패턴이란
전역 변수를 사용하지 않고 객체를 하나만 생성 하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴
‘생성(Creational) 패턴’ 중 하나
하나의 인스턴스만을 생성하는 책임이 있으며 getInstance()를 통해 모든 Client에게 동일한 Instance를 반환하는 작업을 수행한다.
프린터기 하나를 5명이 공유하는 상황이다.
public class Printer {
public Printer() { }
public void print(Document d) { ... }
}
Client가 Printer 클래스를 사용해 프린터를 이용하려면 사용전에 new Printer()가 반드시 1 번만 호출되어야 한다. Printer는 단 하나이고 Client들이 동일한 Printer를 사용하기 때문이다.
이를 보장하는 방법은 생성자를 priavte로 선언해 외부에서 호출할 수 없게 하는 것이다.
public class Printer {
private Printer() { }
public void print(Document d) { ... }
}
Printer 클래스는 printer Instance를 생성하여 외부에서 접근 가능하도록 하는 방법으로 static method(getInstance())를 제공한다.
public class Printer {
// 외부에 제공할 static printer Instance
private static Printer printer = null;
private Printer() { }
// printer Instance를 외부에 제공하는 static method
public static Printer getPrinter(){
if (printer == null) {
// Printer 인스턴스 생성
printer = new Printer();
}
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
// Client가 printer Instance를 호출하여 사용하는 예
public class Clinet {
private String name;
public Client(String name) { this.name = name; }
public void print() {
Printer printer = printer.getPrinter();
printer.print(this.name + " uses " + printer.toString());
}
}
public class Main {
private static final int Client_NUM = 5;
public static void main(String[] args) {
Client[] client = new Client[Client_NUM];
for (int i = 0; i < Client_NUM; i++) {
client[i] = new Client((i+1))
client[i].print();
}
}
}
Multi-Thread에서 Printer 클래스를 이용할 때 Instance가 1개 이상 생성될 수 있고, 이는 Client들이 하나의 Printer Instance를 공유하는 상황이 아니다.
Race Condition 발생 상황
Printer Instance가 아직 생성되지 않았을 때 thread1이 getPrinter 메서드의 if문을 실행해 이미 Instance가 생성되었는지 확인한다(null check).
만약 thread1이 생성자를 호출해 Instance를 만들기 전 Interrupt가 발생하여 실행흐름이 thread2로 넘어가고, thread2가 if문을 실행해 printer 변수가 null인지 확인한다. 현재 printer Instance는 null이므로 Instance를 생성하는 생성자를 호출하는 코드를 실행한다.
이후 실행흐름이 thread1로 돌아와 if 블록에서 printer Instance를 생성하는 코드를 실행하게 되면 2개의 Printer 클래스의 인스턴스가 생성된다.
스레드 스케줄링을 고의로 변경하여 경합 조건을 만들어보자.
public class Printer {
private static Printer printer = null;
private Printer() { }
public static Printer getPrinter(){
if (printer == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) { }
printer = new Printer();
}
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
public class ClientThread extends Thread{
public ClientThread(String name) { super(name); }
public void run() {
Printer printer = printer.getPrinter();
printer.print(Thread.currentThread().getName() + " uses " + printer.toString());
}
}
public class Main {
private static final int THREAD_NUM = 5;
public static void main(String[] args) {
ClientThread[] user = new ClientThread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++) {
client[i] = new ClientThread((i+1));
client[i].start();
}
}
}
프린터 관리자(Lazy Initialization)는 사실 다중 스레드 애플리케이션이 아닌 경우에는 아무런 문제가 되지 않는다.
다중 스레드 애플리케이션에서 발생하는 문제를 해결하는 방법
정적 변수에 인스턴스를 만들어 바로 초기화하는 방법 (Eager Initialization)
인스턴스를 만드는 메서드에 동기화하는 방법 (Thread-Safe Initialization)
정적 변수에 인스턴스를 만들어 바로 초기화하는 방법
public class Printer {
// static 변수에 외부에 제공할 자기 자신의 인스턴스를 만들어 초기화
private static Printer printer = new Printer();
private Printer() { }
// 자기 자신의 인스턴스를 외부에 제공
public static Printer getPrinter(){
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
static 변수
객체가 생성되기 전 클래스가 메모리에 로딩될 때 만들어져 초기화가 한 번만 실행된다.
프로그램 시작~종료까지 없어지지 않고 메모리에 계속 상주하며 클래스에서 생성된 모든 객체에서 참조할 수 있다.
인스턴스를 만드는 메서드에 동기화하는 방법
public class Printer {
// 외부에 제공할 자기 자신의 인스턴스
private static Printer printer = null;
private int counter = 0;
private Printer() { }
// 인스턴스를 만드는 메서드 동기화 (임계 구역)
public synchronized static Printer getPrinter(){
if (printer == null) {
printer = new Printer(); // Printer 인스턴스 생성
}
return printer;
}
public void print(String str) {
// 오직 하나의 스레드만 접근을 허용함 (임계 구역)
// 성능을 위해 필요한 부분만을 임계 구역으로 설정한다.
synchronized(this) {
counter++;
System.out.println(str + counter);
}
}
}
인스턴스를 만드는 메서드를 임계 구역으로 변경
다중 스레드 환경에서 동시에 여러 스레드가 getPrinter 메서드를 소유하는 객체에 접근하는 것을 방지한다.
공유 변수에 접근하는 부분을 임계 구역으로 변경
여러 개의 스레드가 하나뿐인 counter 변수 값에 동시에 접근해 갱신하는 것을 방지한다.
getInstance()에 Lock을 하는 방식이라 속도가 느리다.
https://gmlwjd9405.github.io/2018/07/06/singleton-pattern.html