생성자나 열거 타입으로 싱글턴임을 보증하라(Effective Java)

Choizz·2023년 7월 11일
0

이펙티브 자바

목록 보기
3/13

오늘은 이펙티브 자바 중 "생성자나 열거 타입으로 싱글턴임을 보증하라"에 대한 주제로 포스팅을 해보려고 합니다.

싱글턴 객체란 객체를 여러 개 생성하지 못하고 하나만 생성할 수 있게 만든 객체를 말합니다.
즉, 객체를 하나만 생성하는게 효율적일 경우 사용할 수 있습니다.

(밑의 코드들은 백기선님의 이펙티브 자바 완벽 공략 강의를 참고했습니다.)


싱글턴을 만드는 방법

1. private 생성자와 public static final 필드

  • 클라이언트에서 new 생성자로 객체를 생성하지 못하게 하고 필드만 사용해서 미리 생성된 객체만 사용하게 합니다.
  • 이렇게 구성을 하면 싱글턴을 만드는 코드가 간결하고 javadoc을 만들 때, 주석을 표시해 주면 싱글턴임을 들어낼 수 있습니다.
public class Elvis{

    public static final Elvis INSTANCE = new Elvis(); //객체를 미리 생성해 static 영역에 저장한다.
    
    private Elvis(){} // 클라이언트에서 new 생성자로 객체를 생성하지 못하게 한다.
}

2. private 생성자와 정적 팩터리 메서드

  • 정적 팩터리 메서드를 통해 객체를 가지고 올 수 있습니다.

정적 팩터리 메서드를 사용했을 때 장점

  1. 클라이언트 코드에서 #getInstace() 코드를 쓰면서 행위를 바꿀 수 있습니다.

    • new 생성자를 리턴하게해 매번 새로운 객체를 생성하게 할 수 있지만 클라이언트 코드에서는 코드를 변경하지 않아도 됩니다.
public class Elvis{

    private static final Elvis INSTANCE = new Elvis();
    
    private Elvis(){}
    
    public static Elvis getInstance(){
        //return new Elvis();
        return INSTANCE;
    }
}

public static void main(String[] args){
	//클라이언트 코드는 변경되지 않음.
    Elvis.getInstace();
}
  1. 정적 팩터리 메서드를 제네릭 싱글턴 팩터리로 만듦으로써 싱글턴에 제네릭 타입을 지정해 줄 수 있습니다.
public class MetaElvis<T> {

    private static final MetaElvis<Object> INSTANCE = new MetaElvis<>();

    private MetaElvis() { }

    public static <E> MetaElvis<E> getInstance() { return (MetaElvis<E>) INSTANCE; }

    public void say(T t) {
        System.out.println(t);
    }

    public static void main(String[] args) {
        MetaElvis<String> elvis1 = MetaElvis.getInstance();
        MetaElvis<Integer> elvis2 = MetaElvis.getInstance();
        System.out.println(elvis1);
        System.out.println(elvis2);
        elvis1.say("hello");
        elvis2.say(100);
    }

}
  1. 정적 팩터리 메서드 참조를 공급자로(Supplier)로 사용할 수 있습니다.
  • 함수형 인터페이스인 Supplier는 인자 없이 리턴 값을 주는 함수를 의미합니다.
  • 이것은 싱글턴에서 사용되는 정적 팩터리 메서드와 비슷합니다. #getInstance() 또한 인자 없이 객체를 반환하기 때문입니다.
public interface Singer {
    void sing();
}
public class Elvis implements Singer {
    private static final Elvis INSTANCE = new Elvis();
    private Elvis() { }
    public static Elvis getInstance() { return INSTANCE; }

    @Override
    public void sing() {
        System.out.println("my way~~~");
    }
}
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();
        // start에 인자로 들어가는 Supplier가 getInstance와 일맥상통하다.
        concert.start(Elvis::getInstance);
    }
}

3. enum을 사용한 싱글턴

  • enum은 상수를 보장받기 때문에 싱글턴에 매우 적합합니다.
public enum Elvis{
	INSTANCE;
    
    public void sing(){
    	...
    }
}

싱글턴 단점

1. 테스트를 하기가 어렵다.

  • 정적 필드는 한 번 할당되면 프로그램이 종료될 때 까지 살아있습니다. 테스트는 독립적이어야 하는데 테스트에서 싱글턴 객체가 만들어지면 이후 다른 테스트에서도 영향을 받을 수 있습니다.
  • 대안으로 인터페이스를 만들어서 mock객체를 임의로 만들어 사용하는 방법을 들 수 있습니다.

2. 리플렉션을 사용하면 private 생성자를 호출할 수 있다.

  • 리플렉션을 사용하면 private 생성자에 접근을 할 수 있고 여러 개의 객체를 만들 수 있게 됩니다.
  public static void main(String[] args){
        try {
            Constructor<Elvis> defaultConstructor = Elvis.class.getDeclaredConstructor();
            //private 생성자에 접근을 가능하게 한다.
            defaultConstructor.setAccessible(true);
            //각기 다른 객체가 만들어 진다.
            Elvis elvis1 = defaultConstructor.newInstance(); 
            Elvis elvis2 = defaultConstructor.newInstance();
        } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
  • 따로 필드를 둬서 리플렉션을 통해서도 객체 생성을 못하게 막을 수 있습니다.
  • 하지만 코드가 복잡해 지고 있습니다.
public class Elvis{
    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;
    }
}

3. 역직렬화 할 때 새로운 인스턴스가 생길 수 있다.

  • 역직렬화를 할 경우, 새로운 객체가 생성되어 싱글턴이 깨지게 됩니다.
 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();
            //false를 리턴한다.
            System.out.println(elvis3 == Elvis.INSTANCE);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
  • readResolve()를 오버라이드(실제로 오버라이드는 아니지만 오버라이드처럼 인식)하면 객체의 싱글턴이 보장됩니다.
  • 이러한 대안 역시 코드를 복잡하게 만듭니다.
public class Elvis implements Serializable{
    public static final Elvis INSTANCE = new Elvis();

    private Elvis() {}
    
    //이 메서드에서 INSTANCE를 리턴하게 한다.
    private Object readResolve() {
        return INSTANCE;
    }

}

정리

  • 싱글턴 패턴은 장점도 있지만 위에서 언급한 것 외의 단점 또한 많은 것으로 알고 있습니다.
  • 보통 spring을 사용해 객체를 빈으로 등록하면 싱글턴처럼 사용할 수 있습니다.
  • 따라서, 싱글턴으로 객체를 생성할 때는 많은 고민을 해야할 것 같습니다.

reference

profile
집중

0개의 댓글