싱글톤을 구현해야 하는 상황
- 하나만 유지하면 좋은 경우
- 반드시 하나만 유지해야 하는 경우 (여러개가 생성된다면 문제가 생기는 경우)
첫번째 방법
- private 생성자 + public static final 필드를 사용하는 방법
public class Elvis {
public static final Elvis INSTANCE = new Elvis();
private Elvis() {}
}
장점
- 간결하고 싱글턴임을 API에 드러낼 수 있다.
단점
- 싱글톤을 사용하는 클라이언트 코드를 테스트하기 어려워진다.
- 리플렉션으로 private 생성자를 호출할 수 있다.
// 리플렉션을 통해 싱글톤을 깨뜨리는 방법 예제
public static void main(String[] args) {
try {
Constructor<Elvis> defaultConstructor = Elvis.class.getDeclaredConstructor();
defaultConstructor.setAccessible(true);
Elvis elvis1 = defaultConstructor.newInstance();
Elvis elvis2 = defaultConstructor.newInstance();
System.out.println(elvis1 == elvis2); // false
} catch (...생략) {
e.printStackTrace();
}
}
// 이를 막기 위한 예제
public class Elvis implements IElvis, Serializable {
public static final Elvis INSTANCE = new Elvis();
private static boolean created; // 생성여부 필드 추가
private Elvis() {
if (created) {
throw new UnsupportedOperationException("can't be created by constructor.");
}
created = true;
}
...생략
}
- 역직렬화 할 때 새로운 인스턴스가 생길 수 있다.
// 역직렬화로 여러 인스턴스 만들기
public static void main(String[] args) {
try (ObjectOutput out = new ObjectOutputStream(new FileOutputStream("elvis.obj"))) {
out.writeObject(Elvis.INSTANCE);
} catch (IOException e) {
e.printStackTrace();
}
//역직렬화
try (ObjectInput in = new ObjectInputStream(new FileInputStream("elvis.obj"))) {
Elvis elvis3 = (Elvis) in.readObject();
System.out.println(elvis3 == Elvis.INSTANCE); // false
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
// 역직렬화 시 새로운 인스턴스 생성 방지 코드
public class Elvis implements IElvis, Serializable {
public static final Elvis INSTANCE = new Elvis();
// 역직렬화 시 이 메서드가 오버라이드 처럼 활용 되지만 @Override를 표기 할 수 없음
private Object readResolve() {
return INSTANCE;
}
...생략
}
두번째 방법
- private 생성자 + 정적 팩터리 메서드를 사용하는 방법
public class Elvis implements Singer {
// public => private 선언
private static final Elvis INSTANCE = new Elvis();
// public => private 선언
private Elvis() { }
// 정적 팩터리 메서드
public static Elvis getInstance() { return INSTANCE; }
... 생략
}
장점
- API를 바꾸지 않고도 싱글턴이 아니게 변경할 수 있다.
// 다시 싱글톤이 아니게 클라이언트 코드를 수정하지 않고 변경 가능
public static Elvis getInstance() { return new Elvis; }
- 정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있다. ( == 비교는 안됨..)
public class MetaElvis<T> {
private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();
private MetaElvis() { }
// 제네릭 싱글톤 팩터리 메서드
@SuppressWarnings("unchecked")
public static <E> MetaElvis<E> getInstance() { return (MetaElvis<E>) INSTANCE; }
//클라이언트 코드
public static void main(String[] args) {
// 제네릭 타입이 다른 싱글톤 객체 생성 가능
MetaElvis<String> elvis1 = MetaElvis.getInstance();
MetaElvis<Integer> elvis2 = MetaElvis.getInstance();
System.out.println(elvis1.equals(elvis2));
System.out.println(elvis1);
System.out.println(elvis2);
//출력 결과
//true
//me.whiteship.chapter01.item03.staticfactory.MetaElvis@133314b
//me.whiteship.chapter01.item03.staticfactory.MetaElvis@133314b
}
}
- 정적 팩터리의 메서드 참조를 공급자(Supplier)로 사용할 수 있다.
package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
// Singer 인터페이스를 구현하게 변경
public class Elvis implements Singer { ... 생략}
public class Concert {
public void start(Supplier<Singer> singerSupplier) {
Singer singer = singerSupplier.get();
singer.sing();
}
//사용 코드
public static void main(String[] args) {
Concert concert = new Concert();
concert.start(Elvis::getInstance); // Supplier<Singer> 메서드참조로 구현
}
}
세번째 방법
- 열거 타입
- 가장 간결한 방법이며 직렬화와 리플렉션에도 안전하다.
- 대부분의 상황에서는 원소가 하나뿐인 열거 타입이 싱글턴을 만드는 가장 좋은 방법이다.
public enum Elvis{
INSTANCE;
// 메서드 정의
public void leaveTheBuilding() {
System.out.println("안녕하세요");
}
}
// 클라이언트 코드 사용 예제
public static void main(String[] args) {
Elvis elvis = Elvis.INSTANCE;
elvis.leaveTheBuilding();
}
출처
https://www.inflearn.com/course/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-1