싱글톤 패턴 (Singleton Pattern)
- 인스턴스를 오직 한개만 제공하는 클래스
- 시스템 런타임, 환경 세팅에 대한 정보 등 인스턴스가 여러 개일 때 문제가 생길 수 있는 경우가 있다. 인스턴스를 오직 한 개만 만들어 제공하는 클래스가 필요하다.
예시 (as-is)
public class Settings {
}
public class App {
public static void main(String[] args) {
Settings settings = new Settings();
Settings settings1 = new Settings();
System.out.println(settings != settings1);
}
}
싱글톤 패턴을 적용하려면, 위 settings != settings1에서 false가 나와야 한다.
예시 (싱글톤 패턴을 가장 단순히 구현하는 방법, thread-safe 하지 않음)
public class Settings {
private static Settings instance;
private Settings() {}
public static Settings getInstance() {
if (instance == null) { // A
instance = new Settings(); // B
}
return instance;
}
}
public class App {
public static void main(String[] args) {
Settings settings = Settings.getInstance();
Settings settings1 = Settings.getInstance();
System.out.println(settings != settings1);
}
}
웹 애플리케이션은 보통 멀티 쓰레드로 구현하게 되는데, 위 싱글톤 객체를 thread-safe하지 않다.
두 쓰레드가 Settings.getInstance()로 접근했을 때, 한 쓰레드가 A를 거쳐 B로 진입한 뒤, 다른 쓰레드가 A로 들어왔다고 가정해보자. 그러면 다른 쓰레드가 A를 판별할 때, 한 쓰레드에서 new Settings()를 아직 거치지 못해 instance는 null일 수 있다. 따라서, 한 쓰레드와 다른 쓰레드의 Settings 객체가 달라질 수 있다.
예시 (멀티 쓰레드 환경에서 안전하게 구현하는 방법)
sychronized
public class Settings {
private static Settings instance;
private Settings() {}
public static synchronized Settings getInstance() {
if (instance == null) {
instance = new Settings();
}
return instance;
}
}
public class App {
public static void main(String[] args) {
Settings settings = Settings.getInstance();
Settings settings1 = Settings.getInstance();
System.out.println(settings != settings1);
}
}
java에서 지원하는 synchronized 키워드는 여러 개의 스레드가 한 개의 자원을 사용하고자 할 때, 현재 데이터를 사용하고 있는 해당 스레드를 제외하고 나머지 스레드들은 데이터에 접근 할 수 없도록 막는다. 하지만 getInstace를 한 쓰레드만 사용할 수 있기 때문에, 성능 저하가 발생할 수 있다.
이른 초기화 (eager initialization)
public class Settings {
private static final Settings INSTANCE = new Settings();
private Settings() {}
public static Settings getInstance() {
return INSTANCE;
}
}
public class App {
public static void main(String[] args) {
Settings settings = Settings.getInstance();
Settings settings1 = Settings.getInstance();
System.out.println(settings != settings1);
}
}
생성자를 통해 객체를 생성하는 게 큰 자원이 소모되지 않는다면, instance를 미리 초기화할 수 있다.
double checked locking 사용하기
이른 초기화 방식으로 하기에는 생성자 호출 자원이 너무 크고, synchronized 방식으로 하기에는 자원 낭비가 걱정된다면, double checked locking 방식을 사용하여 효율적인 synchronized 블록을 만들 수 있다.
public class Settings {
private static volatile Settings instance;
private Settings() {}
public static Settings getInstance() {
if (instance == null) { // A
synchronized (Settings.class) { // B
if (instance == null) { // C
instance = new Settings(); // D
}
}
}
return instance; // E
}
}
public class App {
public static void main(String[] args) {
Settings settings = Settings.getInstance();
Settings settings1 = Settings.getInstance();
System.out.println(settings != settings1);
}
}
두 쓰레드가 Settings.getInstance()로 접근했을 때, 한 쓰레드가 A를 거쳐 B, C로 진입한 뒤, 다른 쓰레드가 A, B로 들어왔다고 가정해보자. 한 쓰레드가 synchronized 블록을 마칠때까지 다른 쓰레드는 B에서 wait 한다. 한 쓰레드가 D에서 생성자를 호출하고, synchronized 블록을 마치면 다른 쓰레드가 synchronized 블록에 진입이 가능해지고, 다른 쓰레드는 C에서 false가 나오기 때문에, D를 거치지 않고 return문 E를 타고 메서드를 마친다. 제3자 쓰레드는 이미 instance가 만들어져 있으므로, A -> E를 타고 instance를 반환할 것이다.
getInstance 호출 때마다 동기화 처리를 하지 않기 때문에, synchronized 방식에 비해 효율적이다.
여기서 volatile을 사용하지 않으면, 인스턴스가 두개 이상 발생할 수 있다.
키워드 volatile이란, java 변수를 Main Memory에 저장하겠다는 것을 명시한다. volatile이 명시된 변수의 값을 읽을 때마다, CPU cache에 저장된 값이 아닌 Main Memory에서 읽는다. volatile이 명시된 변수의 값을 변경할 때마다, Main Memory에서도 수정이 일어난다.
멀티 쓰레드 애플리케이션에서의 non-volatile 변수에 대한 작업은 성능상의 이유로 CPU cahce를 사용한다. 따라서, 위 Settings instance를 volatile 처리하지 않으면, 멀티 쓰레드 환경 아래 각각 쓰레드는 instance를 CPU Cache에서 읽어오게 되는데, 한 쓰레드에서 instance를 변경해도, 다른 쓰레드에서 변경한 값이 보이지 않을 수가 있다. 즉, instance가 Main Memory로부터 실시간으로 동기화 되었다는 사실을 보장할 수 없다.
static inner 클래스 사용하기
위 double checked locking 방식은 유용하나, volatile 키워드 때문에 java 1.5 이상에서만 동작한다. 따라서 1.5 이하 버전에서도 활용될 수 있는 static inner 클래스를 사용하도록 하자.
public class Settings {
private Settings() {}
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
public class App {
public static void main(String[] args) {
Settings settings = Settings.getInstance();
Settings settings1 = Settings.getInstance();
System.out.println(settings != settings1);
}
}
클래스는 Singleton 클래스가 Load 될 때에도 Load 되지 않다가 getInstance()가 호출됐을 때 비로소 JVM 메모리에 로드되고, 인스턴스를 생성하게 된다.
INSTANCE가 static이기 때문에 초기화가 이루어진다. LazyHolder 클래스가 초기화 되면서 INSTANCE 객체도 Settings의 생성자의 의해 생성도 이루어지며, 이 과정에서 static이기 때문에 하나의 인스턴스만 생성되는 것을 보장해준다. 그리고 final로 선언했기 때문에 다시 instance가 할당되는 것 또한 막을 수 있다.
static inner 클래스 사용 시 싱글톤 구현 방법을 깨뜨리는 방법
리플렉션 사용
public class Settings {
private Settings() {}
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
public class App {
public static void main(String[] args) {
Settings settings = Settings.getInstance();
Constructor<Settings> declaredConstructor = Settings.class.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Settings settings1 = declaredConstructor.newInstance();
System.out.println(settings != settings1); // true
}
}
직렬화와 역직렬화 사용하기
public class Settings implements Serializable {
private Settings() {}
private static class SettingsHolder {
private static final Settings INSTANCE = new Settings();
}
public static Settings getInstance() {
return SettingsHolder.INSTANCE;
}
}
public class App {
public static void main(String[] args) throws IOException {
Settings settings = Settings.getInstance();
Settings settings1 = null;
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("settings.obj"))) {
out.writeObject(settings); // 직렬화되어 파일로 써졌다.
}
try (ObjectInput in = new ObjectInputStream(new FileInputStream("settings.obj"))) {
settings1 = (Settings) in.readObject(); // 파일을 역직렬화 하였다.
}
System.out.println(settings != settings1); // true
}
}
예시 (안전하고 단순하게 구현하는 방법)
public enum Settings {
INSTANCE;
}
public class App {
public static void main(String[] args) throws IOException {
Settings settings = Settings.INSTANCE;
Settings settings1 = Settings.INSTANCE;
System.out.println(settings != settings1); // false
}
}
- enum은 Serializable을 구현하고 있기 때문에, 직렬화 & 역직렬화를 해도 기존 객체를 가져온다.
- 리플렉션은 enum의 constructor 사용을 금하고 있다.
- 단, JVM이 구동될 때 미리 생성된다. 그리고 상속을 쓰지 못하는 단점이 있다.
활용
- 스프링에서 빈의 스코프 중에 하나인 싱글톤 스코프
- 다른 디자인 패턴 구현체의 일부로 쓰이기도 한다. (빌더, 퍼사드, 추상 팩토리 등)
- 자바의 java.lang.Runtime
Runtime runtime = Runtime.getRuntime();
출처
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4
https://junghyungil.tistory.com/150
'Design Pattern' 카테고리의 다른 글
[Design Pattern] Factory Method Pattern (팩토리 메서드 패턴) (0) | 2022.11.13 |
---|---|
[Design Pattern] Composite Pattern (컴포지트 패턴) (0) | 2022.11.07 |
[Design Pattern] 전략 패턴 (Strategy Pattern) (1) | 2022.10.30 |
[Design Pattern] 프록시 (Proxy) (0) | 2022.10.24 |