싱글톤 패턴
싱글톤이란 ‘단 하나의 원소만을 가진 집합’이라는 수학 이론에서 유래된 것으로, 싱글톤 패턴은 인스턴스가 오직 하나만 생성되는 것을 보장하고 어디에서든 해당 인스턴스에 접근할 수 있도록 하는 디자인 패턴이다.
싱글톤 패턴은 인스턴스를 보관하는 정적 변수와 private 생성자, 인스턴스에 접근할 수 있는 getter 밖에 없다.
싱글톤 패턴을 순차 다이어그램으로 표현하면 위와 같은데, getInstance를 호출 했을 때 인스턴스가 이미 생성되어 있다면 반환하고 없다면 생성자를 호출해 생성 후 저장 및 반환한다.
싱글톤 패턴 예시
단 하나의 Printer
만약 하나의 프린터를 통해 여러 직원이 공유해서 사용해야하는 상황과 같이 단 하나의 프린터로만 작업을 해야하는 상황을 가정해보자.
public class Printer {
public Printer() {
}
public void print(final Resource r) {
//...
}
}
Java
복사
만약 이와 같이 Printer 클래스를 작성한다면, 클라이언트에서는 new Printer()가 반드시 한 번만 호출되도록 매우 주의를 기울여야 할 것이다.
public class Printer {
private Printer() {
}
//...
}
Java
복사
이와 같이 생성자 접근을 막아버린다면 외부의 new Printer() 호출을 막아낼 수 있다. 하지만 Printer의 인스턴스에 접근할 필요는 있기 때문에,
public class Printer {
private static Printer INSTANCE;
private Printer() {
}
public static Printer getPrinter() {
if (INSTANCE == null) {
INSTANCE = new Printer();
}
return INSTANCE;
}
public void print(final Resource r) {
}
}
Java
복사
이처럼 내부 정적 변수로 Printer의 인스턴스를 두고 호출 시점에 단 한번만 생성하도록 구현할 수 있다. 이와 같이 구현해두면, getPrinter()를 몇 번 호출하든지 상관없이 Printer 클래스의 인스턴스는 단 하나만 생성되는 것이 보장된다.
문제점과 해결책
위 구현 방식에는 문제가 있다.
public static Printer getPrinter() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
INSTANCE = new Printer();
}
return INSTANCE;
}
Java
복사
public class Client {
public static void main(String[] args) {
for (int i = 0; i < 50; i++) {
final Thread thread = new Thread(() -> {
final Printer printer = Printer.getPrinter();
System.out.println("printer = " + printer);
});
thread.start();
}
}
}
Java
복사
이처럼 Printer 인스턴스가 생성되지 않은 시점에, 여러 스레드에 동시에 getInstance를 호출하는 경우 Printer 인스턴스가 여러 개 생길 수 있다는 점이다.(위에서는 Thread.sleep을 통해 상황을 재현했다.)
이러한 문제는 두 가지의 해결 방법이 있다.
public class Printer {
private static Printer INSTANCE = new Printer();
private Printer() {
}
public static Printer getPrinter() {
return INSTANCE;
}
public void print(final Resource r) {
}
}
Java
복사
첫 번째는 정적 변수에 인스턴스를 생성해 초기화하는 방법이다. 정적 변수는 클래스가 메모리에 로딩될 때 만들어지고 초기화가 단 한번만 실행된다. 이를 통해 INSTANCE == null 조건에 대한 검증이 불필요해져 스레드 안전한 상태가 된다.
public synchronized static Printer getPrinter() {
if (INSTANCE == null) {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
INSTANCE = new Printer();
}
return INSTANCE;
}
Java
복사
두 번째는 인스턴스를 만드는 메서드를 sychronized 키워드로 임계 영역으로 지정해 동기화하는 방법이다.
정적 클래스
사실 위의 두 번째 해결 방법은 싱글톤 패턴이 아니더라도 정적 클래스로 구현해도 동일한 효과를 얻을 수 있다.
public class StaticPrinter {
public synchronized static void print(Resource r) {
// print
}
}
Java
복사
싱글톤 패턴과의 차이점은 객체를 전혀 인스턴스화 하지 않고 메서드만 호출한다는 점이다.
하지만 정적 클래스를 사용할 수 없는 경우가 있는데, 가장 대표적으로 인터페이스를 구현하는 경우이다.
public interface Printer {
public static void print(Resource r); // 허용되지 않음
}
public class RealPrinter315 implements Printer {
// ...
}
Java
복사
여러 프린터 중 하나를 선택해 프린트하거나, 테스트 코드를 작성하기 위해 테스트 더블을 만드는 경우 위 방식을 사용할 수 없다. 그러니 이런 경우 싱글톤 패턴을 사용해야한다.