싱글턴
싱글턴이란 인스턴스를 오직 하나만 생성할 수 있는 클래스를 말한다. 싱글턴을 만드는 방식은 보통 둘 중 하나이다.
1. 생성자를 private으로 감추고 public static 멤버 변수를 만든다.
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {};
}
[장점]
이 방법은 JavaDocs를 만들 때 싱글턴임이 API에 명백히 드러난다는 장점이 있다. 또 한 간결하다는 특징이 있다.
[단점]
- 권한이 있는 클라이언트의 경우 리플렉션을 사용해서 private 생성자를 호출할 수 있다.
=> 이를 방지하기 위해 생성자를 수정하여 두 번째 객체가 생성되려 할 때 예외를 던질 수 있다.
public class Elvis implements IElvis {
private static boolean created;
private Elvis() {
if (created) {
throw new UnsupportedOperationException("already created");
}
created = true;
}
}
- public static final로 싱글톤을 만들면, Mock을 사용할 수 없기 때문에 테스트가 힘들어진다.
=> 이를 해결하기 위해서 인터페이스를 이용할 수 있다.
// Elvis를 위한 인터페이스를 만든다.
public interface IElvis {
public void hello();
}
// 이를 상속한다.
public class Elvis implements IElvis {
...
}
public class BroadCast {
private int channel;
private boolean on;
private IElvis elvis; // 인터페이스를 타입으로 설정한다.
public BroadCast(IElvis elvis) {
this.elvis = elvis;
}
public void perform() {
on = true;
elvis.hello();
}
public int getChannel() {
return channel;
}
public boolean isOn() {
return on;
}
}
// Mock용 Elvis를 만든다.
public class MockElvis implements IElvis {
@Override
public void hello() {
System.out.println("Hello This is Tester Elvis");
}
}
@Test
void testWithMock() {
BroadCast broadCast = new BroadCast(new MockElvis());
broadCast.perform();
assertTrue(broadCast.isOn());
}
2. 정적 팩터리 방식의 싱글턴
두 번째 방법은 정적 메서드를 이용하여 INSTANCE를 얻는 방법이다.
public class Elvis {
private static final Elvis INSTANCE = new Elvis();
private Elvis() {}
public static Elvis getInstance() { return INSTANCE; }
}
[장점]
1. 메서드의 로직을 바꿔 스레드별로 다른 인스턴스를 넘겨줄 수 있다.
public class Elvis {
public static Elvis getInstance() { new Elvis();}
}
2. 정적 팩터리를 제네릭 싱글턴 패턴으로 만들 수 있다. 동일한 객체를 다른 타입으로 받을 수 있게 할 수 있다.
public class MetaElvis<T> {
private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();
private MetaElvis() {}
public static <T> MetaElvis<T> getInstance() {
return (MetaElvis<T>) INSTANCE;
}
public void hello(T t) {
System.out.println(t);
}
public static void main(String[] args) {
MetaElvis<String> instance = MetaElvis.getInstance();
MetaElvis<Integer> instance2 = MetaElvis.getInstance();
System.out.println(instance);
System.out.println(instance2);
instance.hello("Hello I'm Elvis");
instance2.hello(200);
}
}
3. 정적 팩터리 메서드 참조를 공급자로 사용할 수 있다.
=> Elvis::getInstance를 supplier로 샤용할 수 있다.
public class BroadCast {
private IElvis elvis;
public BroadCast() {}
public BroadCast(IElvis elvis) {
this.elvis = elvis;
}
public void start(Supplier<Elvis> supplier) {
Elvis instance = supplier.get();
instance.hello();
}
public static void main(String[] args) {
BroadCast broadCast = new BroadCast();
broadCast.start(Elvis::getInstance);
}
}
3. 열거 타입을 이용한다.
열거 타입은 리플렉션 공격에도 방어가 되며, 직렬화를 위한 추가 작업이 필요 없다. 따라서 대부분 상황에서는 원소가 하나뿐인 열거 타입이
싱글턴을 만드는 가장 좋은 방법이다.
public enum Elvis2 {
INSTANCE;
public void hello() {};
}