소개
자바 싱글톤 패턴은 강의 네 디자인 패턴 중 하나로, 생성 디자인 패턴 범주에 속합니다. 정의로 보면 간단한 디자인 패턴으로 보이지만, 구현에는 많은 고려사항이 따릅니다.
이 글에서는 싱글톤 디자인 패턴의 원칙을 배우고, 싱글톤 디자인 패턴을 구현하는 다양한 방법과 사용에 대한 최상의 방법을 알아보겠습니다.
싱글톤 패턴 원칙
- 싱글톤 패턴은 클래스의 인스턴스화를 제한하고, 자바 가상 머신에서 클래스의 인스턴스가 하나만 존재하도록 보장합니다.
- 싱글톤 클래스는 클래스의 인스턴스를 얻을 수 있는 전역 접근 지점을 제공해야 합니다.
- 싱글톤 패턴은 로깅, 드라이버 객체, 캐싱 및 스레드 풀에 사용됩니다.
- 싱글톤 디자인 패턴은 추상 팩토리, 빌더, 프로토타입, 퍼사드 등 다른 디자인 패턴에서도 사용됩니다.
- 싱글톤 디자인 패턴은 코어 자바 클래스에서도 사용됩니다(예:
java.lang.Runtime
,java.awt.Desktop
).
자바 싱글톤 패턴 구현
싱글톤 패턴을 구현하기 위해 다양한 접근 방법이 있지만, 모든 방법은 다음과 같은 공통 개념을 가지고 있습니다.
- 다른 클래스에서 해당 클래스의 인스턴스화를 제한하기 위한 private 생성자.
- 해당 클래스의 유일한 인스턴스인 동일한 클래스의 private 정적 변수.
- 외부에서 싱글톤 클래스의 인스턴스를 얻기 위한 전역 접근 지점인 public 정적 메소드.
추후 섹션에서는 싱글톤 패턴 구현의 다양한 접근 방법과 구현에 대한 디자인 관련 사항을 알아보겠습니다.
1. 열심히 초기화
열심히 초기화에서는 싱글톤 클래스의 인스턴스가 클래스 로딩 시점에 생성됩니다. 열심히 초기화의 단점은 클라이언트 애플리케이션이 사용하지 않을지라도 메서드가 생성된다는 것입니다. 다음은 정적 초기화 싱글톤 클래스의 구현 예입니다:
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
// 클라이언트 애플리케이션이 생성자를 사용하지 못하도록 비공개 생성자
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance() {
return instance;
}
}
싱글톤 클래스가 많은 리소스를 사용하지 않는 경우에는 이 접근 방식을 사용해야 합니다. 그러나 대부분의 시나리오에서는 파일 시스템, 데이터베이스 연결 등과 같은 리소스에 대해 싱글톤 클래스가 생성됩니다. 클라이언트가 getInstance
메서드를 호출하지 않는 한 인스턴스화를 피해야 합니다. 또한, 이 방법은 예외 처리에 대한 옵션을 제공하지 않습니다.
2. 정적 블록 초기화
정적 블록 초기화 구현은 이지 초기화와 유사하지만, 클래스의 인스턴스가 예외 처리를 제공하는 정적 블록에서 생성된다는 점이 다릅니다.
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
// 예외 처리를 위한 정적 블록 초기화
static {
try {
instance = new StaticBlockSingleton();
} catch (Exception e) {
throw new RuntimeException("Exception occurred in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
이지 초기화와 정적 블록 초기화는 인스턴스가 사용되기 전에 생성되므로 사용하는 것은 좋은 관행이 아닙니다.
3. 게으른 초기화
게으른 초기화 방법은 전역 접근 메서드에서 인스턴스를 생성하여 싱글톤 패턴을 구현합니다. 이 접근 방식으로 싱글톤 클래스를 만드는 샘플 코드는 다음과 같습니다:
package com.journaldev.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
앞의 구현은 단일 스레드 환경에서는 잘 작동하지만, 다중 스레드 시스템에서는 여러 스레드가 동시에 if
조건 안에 있을 때 문제가 발생할 수 있습니다. 이는 싱글톤 패턴을 파괴하고 두 스레드가 싱글톤 클래스의 서로 다른 인스턴스를 얻게 됩니다. 다음 섹션에서는 스레드 안전 싱글톤 클래스를 만드는 다른 방법을 살펴보겠습니다.
4. 스레드 안전한 싱글톤
A simple way to create a thread-safe singleton class is to make the global access method synchronized so that only one thread can execute this method at a time. Here is a general implementation of this approach:
package com.journaldev.singleton;
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
이전에 구현된 방식은 잘 작동하며 스레드 안전성을 제공하지만, 동기화된 메소드에 따른 비용으로 성능이 저하됩니다. 실제로 처음 몇 개의 스레드에서만 별도의 인스턴스를 생성해야 할 뿐이지만, 매번 이러한 추가 오버헤드를 피하기 위해 더블 체크된 락 원칙을 사용합니다. 이 접근 방식에서는 동기화된 블록을 if
조건문 내부에서 사용하며, 싱글톤 클래스의 단일 인스턴스만 생성되도록 추가로 확인합니다. 다음 코드 스니펫은 더블 체크된 락 구현을 제공합니다:
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
스레드 안전한 싱글톤 클래스로 학습을 계속하세요.
5. Bill Pugh 싱글톤 구현
Java 5 이전에는 Java 메모리 모델에 많은 문제가 있었고, 이전 접근 방식은 너무 많은 스레드가 동시에 싱글톤 클래스의 인스턴스를 가져 오려고 할 때 특정 시나리오에서 실패하는 경우가 있었습니다. 그래서 Bill Pugh는 내부 정적 도우미 클래스를 사용하여 싱글톤 클래스를 만드는 다른 접근 방식을 생각해냈습니다. 다음은 Bill Pugh 싱글톤 구현의 예입니다:
package com.journaldev.singleton;
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
싱글톤 클래스의 인스턴스를 포함하는 private 내부 정적 클래스에 주목하십시오. 싱글톤 클래스가 로드될 때, SingletonHelper
클래스는 메모리에 로드되지 않으며, 누군가 getInstance()
메소드를 호출할 때만 이 클래스가 로드되고 싱글톤 클래스 인스턴스를 생성합니다. 이는 동기화를 필요로하지 않기 때문에 가장 널리 사용되는 싱글톤 클래스 접근 방식입니다.
6. 싱글톤 패턴 파괴를 위한 Reflection 사용
Reflection을 사용하여 이전의 모든 싱글톤 구현 접근 방식을 파괴할 수 있습니다. 다음은 예제 클래스입니다:
package com.journaldev.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
// 이 코드는 싱글톤 패턴을 파괴할 것입니다
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
이전의 테스트 클래스를 실행하면, 두 인스턴스의 hashCode
가 동일하지 않다는 것을 알 수 있습니다. 이는 싱글톤 패턴을 파괴합니다. Reflection은 매우 강력하며, 스프링과 하이버네이트와 같은 많은 프레임워크에서 사용됩니다. Java Reflection Tutorial으로 학습을 계속하세요.
7. Enum Singleton
이러한 Reflection 상황을 극복하기 위해, Joshua Bloch은 싱글톤 디자인 패턴을 구현하기 위해 enum
의 사용을 제안합니다. Java는 enum
값이 프로그램에서 한 번만 인스턴스화되도록 보장하기 때문에 Java Enum 값도 전역적으로 접근 가능한 싱글톤입니다. 단점은 enum
유형이 다소 유연하지 않다는 것입니다 (예: 지연 초기화를 허용하지 않음).
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething() {
// 무언가 수행
}
}
8. 직렬화와 싱글톤
분산 시스템에서는 때로 파일 시스템에 상태를 저장하고 나중에 검색하기 위해 싱글톤 클래스에 Serializable 인터페이스를 구현해야 할 필요가 있습니다. 다음은 Serializable 인터페이스를 구현한 작은 싱글톤 클래스의 예입니다:
package com.journaldev.singleton;
import java.io.Serializable;
public class SerializedSingleton implements Serializable {
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton(){}
private static class SingletonHelper {
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance() {
return SingletonHelper.instance;
}
}
직렬화된 싱글톤 클래스의 문제점은 직렬화를 역직렬화할 때마다 클래스의 새 인스턴스를 생성한다는 것입니다. 다음은 예시입니다:
package com.journaldev.singleton;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
// 파일에서 객체로 역직렬화
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode="+instanceOne.hashCode());
System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
}
}
이 코드는 다음 출력을 생성합니다:
OutputinstanceOne hashCode=2011117821
instanceTwo hashCode=109647522
싱글톤 패턴이 파괴되었습니다. 이 시나리오를 극복하기 위해선, readResolve()
메서드의 구현을 제공하면 됩니다.
protected Object readResolve() {
return getInstance();
}
이후 테스트 프로그램에서 두 인스턴스의 hashCode가 동일한 것을 알 수 있습니다.
Java 직렬화와 Java 역직렬화에 대해 읽어보세요.
결론
이 글에서는 싱글톤 디자인 패턴을 다루었습니다.
더 많은 Java 튜토리얼로 학습을 계속하세요.