본문 바로가기

JAVA

이펙티브 자바 규칙3. private 생성자나 enum 자료형은 싱글턴 패턴을 따르도록 설계하라

728x90

싱글턴

객체를 하나만 만들 수 있는 클래스이다.

 

1.  정적 멤버를 final 로 선언한다. (private 생성자 , static 멤버)

private 생성자는 public static final 필드인 인스턴스를 초기화할 때 한 번만 호출되기 때문에 항상 같은 객체를 참조한다.

 

public class Singleton1{
      public static final Elvis INSTANCE = new Singleton1();
      private Singleton1(){...}
}
        Singleton1 singleton1 = Singleton1.INSTANCE; // 항상 동일한 인스턴스 참조
        Singleton1 singleton11 = Singleton1.INSTANCE; // 항상 동일한 인스턴스 참조
        System.out.println(singleton1 == singleton11);

 

+ 선언만 보면 클래스가 싱글턴인지 금방 알 수 있어 좋다.

 

+ 정적 팩토리 메서드를 사용하는 방법에 비해 더 명확하고 간단하다.

 

- 최신 JVM 경우 정적 팩토리 메소드 호출을 거의 항상 인라인 처리해 버리기 때문에 성능이 굳이 좋지는 않다.

 

- 인스턴스를 사용하지 않아도 인스턴스를 호출하게 된다.

 

- 리플렉션 기능을 통해 private 생성자를 호출할 있다.

-> 생성자에 카운트 변수나 인스턴스의 존재 여부를 확인하여 인스턴스가 여러 개 생성되는 상황을 방지할 수 있다.

 

    static int count;

    private Singleton1() {
        count ++;
        if (count != 1) {
            throw new IllegalStateException("this object should be singleton");
        }
        if(INSTANCE != null){
            throw new RuntimeException("생성자를 호출할 수 없습니다!");
        }
    }

 

    private Singleton1() {
        if(INSTANCE != null){
            throw new RuntimeException("생성자를 호출할 수 없습니다!");
        }
    }

 

- 방법 모두 직렬화한다면 역직렬화 같은 타입 인스턴스가 여러 생길 있다.

-> 인스턴스에 "transient" 키워드를 선언하고 기존에 있던 인스턴스를 반환하도록 readResolve() 을 작성한다.

 

    private static final transient Singleton2 INSTANCE = new Singleton2();
    
        private Object readResolve() {
        return INSTANCE;
    }

 

2. public 으로 선언된 정적 팩토리 메서드를 이용한다. (private 생성자 , static 멤버)

getInstance() 는 항상 같은 객체에 대한 참조를 반환한다.

 

public class Elvis{
      private static final Elvis INSTANCE = new ELVIS();
      private Elvis(){...}
      public static Elvis getInstance() {return INSTANCE;}
}
        Singleton2 singleton2 = Singleton2.getInstance();
        Singleton2 singleton22 = Singleton2.getInstance();
        System.out.println(singleton2 == singleton22);

 

+ api 를 변경하지 않고도 싱글턴 패턴을 포기할 수 있다. 

 

+ 정적 팩토리의 메소드 참조를 공급자로 사용할 있다

        Supplier<Singleton2> s2supplier = Singleton2::getInstance;
        s2supplier.get();

 

- 인스턴스를 사용하지 않아도 인스턴스를 호출하게 된다.

 

- 리플렉션 기능을 통해 private 생성자를 호출할  있다.

-> 생성자에 카운트 변수나 인스턴스의 존재 여부를 확인하여 인스턴스가 여러 개 생성되는 상황을 방지할 수 있다.

 

        try {
            Constructor<Singleton1> constructor = Singleton1.class.getDeclaredConstructor();
            constructor.setAccessible(true);
            constructor.newInstance();

        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

 

-  방법 모두 직렬화한다면 역직렬화   같은 타입 인스턴스가 여러  생길  있다. 

-> 인스턴스에 "transient" 키워드를 선언하고 기존에 있던 인스턴스를 반환하도록 readResolve() 을 작성한다.

    private static final transient Singleton2 INSTANCE = new Singleton2();
    
        private Object readResolve() {
        return INSTANCE;
    }

 

 

3. 원소가 하나뿐인 enum 자료형 정의한다. 

JDK 1.5 부터 가능하며 기능적으로는 public 필드를 사용하는 구현법과 동등하다.

 

public enum Singleton3 {
    INSTANCE; // 이 타입의 인스턴스는 하나

    public String getName() {
        return "yerincho";
    }
}

 

+ 좀 더 간결하다.

 

+ 직렬화가 자동으로 처리되어 여러 객체가 생길 일이 없다.

 

+ 리플렉션을 통한 공격에도 안전하다.

 

+ thread-safe

 

- enum 말고 다른 상위 클래스 상속해야 한다면 사용 불가 (인터페이스 구현은 가능)

 


추가 내용 ) if 문을 사용한 방법

public class Singleton4 {
     public static RestApi getInstance() {
        if(INSTANCE == null) {
            INSTANCE = new RestApi();
        }
        return INSTANCE;
    }
}

+ 인스턴스를 사용하지 않을 때는 호출되지 않는다.

- 멀티 스레드 환경에서 인스턴스가 여러 개 만들어질 수 있다.

 

해결 방법 1

getInstance() 에 synchronized 키워드를 붙여준다.

public static synchronized RestApi getInstance() { }

- 약 100배 정도의 성능 저하

해결 방법 2

Double-checking Locking

public class Singleton5 {
    private Singleton5() { }

    private volatile static Singleton5 instance; //volatile: Multi Thread 환경에서 하나의 Thread만 read & write 하고 나머지 Thread가 read하는 상황에서 적합
    public static Singleton5 getInstance() {
        if(instance == null){
            synchronized (Singleton5.class) {
                if(instance == null) {
                    instance = new Singleton5();
                }
            }
        }
        return instance;
    }
}

+ 해결 방법 1과 비교하여 더 나은 성능 (인스턴스가 생성되기 이전에만 동기 처리됨)

해결 방법 3

Initialization on demand holder idiom

public class Singleton6 {
    private Singleton6() { }

    private static class SingletonHolder {
        // getInstance() 호출 전에는 class 로더에 의해 실행되지 않는다.
        private static final Singleton6 instance = new Singleton6();
    }
    public static Singleton6 getInstance() {
        return SingletonHolder.instance;
    }
}

+ getInstance() 가 호출될 때 SingletonHolder 가 로딩되며 인스턴스가 생성되어, 인스턴스를 사용하지 않을 때는 인스턴스를 생성하지 않음

+ 클래스 로더에서 멀티 쓰레드 문제 막아줌

 

'JAVA' 카테고리의 다른 글

Java Native Interface (JNI)  (0) 2021.02.26
놓치고 있던 것  (0) 2021.02.03