참고: 유튜브로 보기 (상단 이미지 클릭)
싱글턴 패턴은 인스턴스를 하나만 생성할 수 있는 클래스다.
이를 이용하여 무상태(stateless)객체나 설계상 유일해야 하는 시스템 컴포넌트 혹은 인스턴스를 하나만 유지해도 좋은 경우 사용한다.
책에서는 이러한 싱글턴 패턴을 구현 시 private 생성자를 사용하여 약간의 조정을 통한 방식이나 열거 타입으로 사용
할 것을 요구한다.
일부 개발자가 중요한 의미 인식을 하지 않고 무의식 중 사용하면 발생할 수 있는 흔한 케이스다.
아래 코드는 2가지의 잘못된 방식과 1가지의 불완전함을 포함한다.
public class ImperfectSingleton {
public static ImperfectSingleton instance;
public static ImperfectSingleton getInstance() {
if ( null == instance ) {
instance = new ImperfectSingleton();
}
return instance;
}
}
1) 필드가 외부로 공개될 경우, 직접 접근하여 변경할 수 있다.
2) 생성자가 공개될 경우 인스턴스화가 가능해진다.
3) Thread safe 하지 않음을 회피하는 기법으로 Synchronized, DCL(Double-checked Locking) 방식이 있다.
Synchronized 는 Intrinsic Lock 을 활용하여 임계영역을 Locking 한다.
getInstance 가 호출되는 매순간 동시 접근을 제어하여 Thread safe를 보장한다.
DCL 은 최초 생성 시 경합 과정에서만 동시 접근을 제어하는 기법이다.
본 주제가 디자인 패턴에 대한 부분이 아님으로 이 주제를 더 다루지 않겠다.
Note. DCL 방식은 Thread safe 하지 않다는 일부 전문가들의 주장이 있다.
public class ImperfectSingletonTest {
public static void doTest() {
// 필드가 public 인 경우 getInstance 를 통하지 않고 필드에 직접 접근하여 변경 가능!!
ImperfectSingleton.instance = null;
// 생성자를 명시적으로 지정하지 않으면 직접 생성 가능!!
// ImperfectSingleton imperfectSingleton = new ImperfectSingleton();
// Thread safe 하지 않음. (상위 주석 후 수차례 반복 시 확인 가능)
Runnable runnable = () -> {
ImperfectSingleton singleton = ImperfectSingleton.getInstance();
System.out.println( singleton );
};
List<Thread> threads = new ArrayList<>();
threads.add( new Thread( runnable ) );
threads.add( new Thread( runnable ) );
threads.add( new Thread( runnable ) );
threads.add( new Thread( runnable ) );
threads.add( new Thread( runnable ) );
threads.add( new Thread( runnable ) );
for ( int i = 0; i < threads.size(); i++ ) {
Thread thread = threads.get( i );
thread.start();
}
}
}
public class FieldSingleton {
public static final FieldSingleton INSTANCE = new FieldSingleton();
private FieldSingleton() {}
}
public class MethodSingleton {
private static final MethodSingleton INSTANCE = new MethodSingleton();
private MethodSingleton() {}
public static MethodSingleton getInstance() {
return INSTANCE;
}
}
클라이언트 코드를 변경시키지 않고 생성 방식을 변형시킬 수 있다.
public class MethodSingleton {
private MethodSingleton() {}
public static MethodSingleton getInstance() {
return new MethodSingleton();
}
}
Generic 을 활용하여 같은 인스턴스를 반환하지만 타입을 변환하여 사용할 수 있다.
public class GenericSingleton<T> {
public static final GenericSingleton<?> INSTANCE = new GenericSingleton<>();
private GenericSingleton() {
}
public static <T> GenericSingleton<T> getInstance() {
return (GenericSingleton<T>) INSTANCE;
}
public boolean send( T message ) {
// blah~ blah~
return true;
}
}
Supplier 를 활용하여, lambda expression or method reference 를 사용할 수 있다.
public class SingletonSupplier {
public void start( Supplier<ISingleton> supplier ) {
ISingleton instance = supplier.get();
instance.send( "test" );
}
public static void main( String[] args ) {
SingletonSupplier singletonSupplier = new SingletonSupplier();
singletonSupplier.start( MockSingleton::getInstance );
}
}
1) 싱글턴을 사용하는 클라이언트가 테스트하기 어려워진다.
싱글턴을 사용하는 인스턴스가 비용(Money)이 발생하는 루틴이라고 가정하면
하나의 인스턴스만 생성되는 싱글턴의 특성상 대체가 곤란하다는 내용이다.
이는 언급된 내용처럼 인터페이스를 구현해서 만든 Mock으로 대체할 수 있다.
// 실제 객체
public class RealSingleton implements ISingleton {
private static final RealSingleton INSTANCE = new RealSingleton();
private RealSingleton() {
}
public static RealSingleton getInstance() {
return INSTANCE;
}
@Override
public boolean send( String message ) {
// 비용 발생
System.out.println( "실제 비용 발생 루틴" );
return false;
}
}
// 가짜 객체 구현
public class MockSingleton implements ISingleton {
private static final MockSingleton INSTANCE = new MockSingleton();
private MockSingleton() {
}
public static MockSingleton getInstance() {
return INSTANCE;
}
@Override
public boolean send( String message ) {
// 가짜 구현체를 이용하여 비용 발생하지 않음.
return false;
}
}
public static main( String[] args ) {
ISingleton singleton = RealSingleton.getInstrance(); // 실 비즈니스 동작 시 사용
singleton = MockSingleton.getInstrance(); // 테스트 시 구현 대체
singleton.send( "fire~!" );
}
2) 리플렉션에 안전하지 않다.
리플렉션을 이용하여, 생성자에 접근 제어자를 수정하여 새로운 인스턴스를 생성할 수 있다.
공격을 방어하려면 생성자에서 두 번째 객체가 생성될 때 예외를 던진다.
public class ReflectionAttack {
@SneakyThrows
public static <T> T getNewInstance( Class<?> clz ) {
Constructor<?> declaredConstructor = clz.getDeclaredConstructor();
declaredConstructor.setAccessible( true );
T newInstance = (T) declaredConstructor.newInstance();
return newInstance;
}
public static void doTest() {
MethodSingleton instance = MethodSingleton.getInstance();
MethodSingleton newInstance = getNewInstance( MethodSingleton.class );
System.out.println( instance + " : " + newInstance );
}
}
public class ReflectionSafeSingleton {
private static final ReflectionSafeSingleton INSTANCE = new ReflectionSafeSingleton();
private static boolean isCreated;
// 생성자에서 두 번 생성하지 못하게 함으로 안전해진다.
private ReflectionSafeSingleton() {
if ( isCreated )
throw new UnsupportedOperationException( "already exists." );
isCreated = true;
}
public static ReflectionSafeSingleton getInstance() {
return INSTANCE;
}
}
3) 역직렬화 시 새로운 인스턴스가 생길 수 있다.
역직렬화 시 필드 변수의 값을 복사한 새로운 인스턴스를 생성할 수 있는 문제가 있다.
이를 방지하기 위해 필드 변수에는 transient
로 선언하여 다른 인스턴스로 복제됨을 방지한다.
또한 동일한 인스턴스를 생성 및 반환하도록 readResolve()
를 구현한다.
public static void serialize( Object obj, String fileName ) {
try ( ObjectOutput out = new ObjectOutputStream( new FileOutputStream( fileName ) ) ) {
out.writeObject( obj );
} catch ( FileNotFoundException e ) {
throw new RuntimeException( e );
} catch ( IOException e ) {
throw new RuntimeException( e );
}
}
public static Object deserialize( String fileName ) {
try ( ObjectInput in = new ObjectInputStream( new FileInputStream( fileName ) ) ) {
return in.readObject();
} catch ( IOException e ) {
throw new RuntimeException( e );
} catch ( ClassNotFoundException e ) {
throw new RuntimeException( e );
}
}
public static void doTest() {
String fileName = "serial.obj";
SerializationSingleton serialIns = SerializationSingleton.getInstance();
serialize( serialIns, fileName );
System.out.println( serialIns );
SerializationSingleton deserializeIns = (SerializationSingleton) deserialize( fileName );
System.out.println( deserializeIns );
}
// com.ntigo.study.effectivejava3rd.item03.assist.SerializationSingleton@34cd072c
// com.ntigo.study.effectivejava3rd.item03.assist.SerializationSingleton@34c4973
public class SerializationSingleton implements Serializable {
private static final SerializationSingleton INSTANCE = new SerializationSingleton();
private transient String name = "ntigo";
private SerializationSingleton() {
}
public static SerializationSingleton getInstance() {
return INSTANCE;
}
public void display() {
System.out.println( this.name );
}
private Object readResolve() {
return INSTANCE;
}
}
// com.ntigo.study.effectivejava3rd.item03.assist.SerializationSingleton@34cd072c
// ntigo
// com.ntigo.study.effectivejava3rd.item03.assist.SerializationSingleton@34cd072c
// ntigo
대부분 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만들기 최적의 방법!
public enum EnumSingleton {
INSTANCE
}
싱글턴을 안전하게 구현할 수 있도록 William Pugh가 고안한 이디엄이다.
public class HolderSingleton {
private HolderSingleton() {}
public static HolderSingleton getInstance() {
return Holder.INSTANCE;
}
private static class Holder {
static final HolderSingleton INSTANCE = new HolderSingleton();
}
}
static inner class는 해당 객체 사용 시 초기화 된다.
https://wesome.org/bill-pugh-singleton-solution-or-holder-singleton-pattern
http://www.cs.umd.edu/~pugh/java/memoryModel/
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html
http://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html