Singleton Pattern
1. Singleton Pattern이란
Singleton Pattern이란 전역 변수를 사용하지 않고 객체를 하나만 생성하도록 하며, 생성된 객체를 어디에서든지 참조할 수 있도록 하는 패턴입니다. 아래의 예제를 통해 Singleton Pattern을 살펴보겠습니다.
public class Printer {
// 외부에 제공할 자기 자신의 인스턴스
private static Printer printer = null;
private Printer() { }
// 자기 자신의 인스턴스를 외부에 제공
public static Printer getPrinter(){
if (printer == null) {
// Printer 인스턴스 생성
printer = new Printer();
}
return printer;
}
public void print(String str) {
System.out.println(str);
}
}
위의 코드를 살펴보면, 우선 Printer 클래스의 객체를 아무 곳에서나 생성할 수 없도록 생성자를 private으로 생성했습니다. 그리고 클래스 자기 자신에 대한 객체를 static으로 하나 만들어 getPrinter라는 메소드를 통해 외부에 제공하는 메소드를 만들어줍니다. 이렇게 함으로써 클래스의 인스턴스를 통하지 않고서도 메소드를 실행할 수 있고 변수를 참조할 수 있습니다.
2. Singleton을 사용하는 이유
- 고정된 메모리 영역을 얻으면서 하나의 인스턴스를 사용하기 때문에 메모리 낭비를 방지할 수 있습니다.
- Singleton으로 만들어진 클래스의 인스턴스는 전역 인스턴스이기 때문에 다른 클래스의 인스턴스들이 데이터를 공유하기 쉽습니다.
- 두 번째 호출부터는 이미 만들어진 객체를 가져오는 것이기 때문에 객체 로딩 시간이 줄어 성능이 향상됩니다.
3. Singleton의 문제점
다중 스레드에서 객체가 만들어지지 않은 시점에 인스턴스가 1개 이상 생성될 수 있습니다.
위의 예제를 이용한다면, Printer 인스턴스가 아직 생성되지 않았을 때 동시에 여러 개의 스레드가 getPrinter()를 호출하고 인스턴스가 생성되었는지 확인합니다. 이 때, printer는 null인 상태입니다. 여러 개의 스레드가 동시에 확인을 하면, 당연히 null인 상태라고 확인하고 각 스레드는 새로운 인스턴스를 생성합니다. 이렇게 되면 한 개만 존재해야 하는 printer 인스턴스가 여러 개 발생할 수 있다는 점입니다.
4. Singleton의 문제점 해결 방법
정적 변수에 인스턴스를 만들어 바로 초기화 하는 방법
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);
}
}
위처럼 정적 변수에 바로 인스턴스를 초기화 해준다면, 클래스가 메모리에 로딩될 때 초기화가 한 번만 실행됩니다. 그리고 프로그램 시작부터 종료할 때까지 없어지지 않고 메모리에 계속 상주하며 클래스에서 생성된 모든 객체에서 참조할 수 있습니다.
인스턴스를 만드는 메소드에 동기화하는 방법
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 메소드에 synchronized 키워드를 통해 여러 스레드가 동시에 getPrinter를 호출하는 것을 방지합니다. 다만 이러한 방법은, getInstance 메소드에 Lock을 하는 방식이라 문제가 발생하거나 시간이 오래 걸리면 속도가 많이 느려집니다.