Study/Effective-Java

[item#03] private 생성자나 열거타입으로 싱글턴임을 보증하라

hongeeii 2023. 11. 27. 18:09
728x90
반응형

https://readystory.tistory.com/116

private 생성자나 열거타입으로 싱글턴임을 보증하라

싱글턴(singleton) : 인스턴스를 하나만 생성할 수 있는 클래스

(인터페이스를 구현해서 만든 싱글턴이 아니면 mock 구현으로 대체할 수 없음 => (1)싱글턴은 클라이언트가 테스트하기 어려울 수 있음)

싱글턴을 만드는 방식

모두 공통적으로 생성자는 private으로 감춤, 유일한 인스턴스 접근수단으로 public static 멤버를 만듬

  1. public static 멤버가 final 인 방식

    public class Elvis{
      public static final Elvis INSTANCE = new Elvis();
      private Elvis(){...}
    
      public void leaveTheBuilding(){...}
    }
    

    private 생성자가 Elvis.INSTANCE를 초기화할 때 딱 한번만 호출되어 싱글턴임을 보장.

    but 리플렉션을 사용해 private 생성자 호출 가능. => 이럴때는 두번째 객체생성 때 오류 던져주면됨.

    장점

    해당 클래스가 싱글턴인게 명확히 드러남.

    간결함.

  2. 정적 팩터리 메서드를 public static 멤버로 제공

    public class Elvis{
      private static final Elvis INSTANCE = new Elvis();
      private Elvis(){...}
      public static Elvis getInstance(){ return INSTANCE; }
    
      public void leavTheBuilding(){...}
    }
    

    getInstance() 가 항상 같은 객체만 return 하지만 역시나 리플렉션에 뚫림.

    장점

    API를 바꾸지 않아도 싱글턴이 아니게 변경 가능.

    정적 팩터리를 제네릭 싱글턴 팩터리로 만들 수 있음.

    정적 팩터리의 메서드 참조를 supplier로 사용 가능

    1번화 2번 방법은 직렬화(Serializable 구현) 후 역직렬화 할 때 새로은 인스턴스가 만들어짐.

    => readResolve 메서드를 제공해야 같은 인스턴스를 return 함.

      private Object readResolve(){
        return INSTANCE;
      }
    
  3. 원소가 하나인 열거타입을 선언 * 가장 추천

    public enum Elvis{
      INSTANCE;
      
      public void leaveTheBuilding(){...}
    }
    

    장점

    간결.

    직렬화가능.

    리플렉션 공격도 막아줌.

    단, Enum 외의 클래스를 상속해야 한다면 이 방법은 사용불가.

    (열거 타입이 다른 인터페이스를 구현하도록 선언할 수는 있음.)

싱글턴을 사용하는 클라이언트를 테스트하기가 어려워질 수 있다.

public class Concert {
    private boolean lightsOn;
    private boolean mainStateOpen;
    private Elvis elvis;

    public Concert(Elvis elvis) {this.elvis = elvis;}
    
    public void perform() {
        mainStateOpen = true;
        lightsOn = true;
        elvis.sing();
    }
    public boolean isLightsOn() {return lightsOn;}
    public boolean isMainStateOpen() {return mainStateOpen;}
}

위와 같이 싱글턴을 사용할 때는 싱글턴의 메서드가 비용이 크다면 테스트마다 큰 비용을 지불해야함.

이럴 때는 아래와 같이 인터페이스를 구현한 mock 객체로 테스트를 하면 됨.

public class Concert {
    private boolean lightsOn;
    private boolean mainStateOpen;
    private IElvis elvis;

    // 콘서트 생성자에 mock 객체를 전달 받음. 받을때 인터페이스로 받음.
    public Concert(IElvis elvis) {this.elvis = elvis;}
    
    public void perform() {
        mainStateOpen = true;
        lightsOn = true;
        elvis.sing();
    }
    public boolean isLightsOn() {return lightsOn;}

    public boolean isMainStateOpen() {return mainStateOpen;}
}

리플렉션을 사용하면 싱글턴이 깨진다.

public class ElvisReflection {
    public static void main(String[] args) {
        try {
            Constructor<Elvis> defaultConstructor = Elvis.class.getDeclaredConstructor();
            Elvis elvis1 = defaultConstructor.newInstance();
            Elvis elvis2 = defaultConstructor.newInstance();
            System.out.println(elvis1 == elvis2);
            System.out.println(elvis1 == Elvis.INSTANCE);
        } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

위와 같이 리플렉션을 해도 인스턴스를 가져올때는

java.lang.IllegalAccessException: class me.whiteship.chapter01.item03.field.ElvisReflection cannot access a member of class me.whiteship.chapter01.item03.field.Elvis with modifiers "private"
    at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:361)
    at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:591)
    at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
    at me.whiteship.chapter01.item03.field.ElvisReflection.main(ElvisReflection.java:12)

private 이라 접근할 수 없다고 에러가 뜨나.

public class ElvisReflection {
    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);
            System.out.println(elvis1 == Elvis.INSTANCE);
        } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

accessible 을 true 로 주게 되면 private 생성자를 사용하게될 뿐만 아니라.

image


생성자를 사용하게 되었기 때문에 모두 다른 객체를 반환한다. => 싱글턴이 깨짐.

=> 이걸 해결하기 위해 flag 를 줘서 해결할 수 있는데 소스가 간결해지지 않음.

직렬화 후 역직렬화 문제.

public class ElvisSerialization {
    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);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

직렬화 : 객체의 정보를 어딘가에 저장해놓고, 역직렬화 : 어딘가에 저장되어있는 객체정보들을 읽어옴.

역직렬화 시 생성자를 사용하기 때문에 다른 인스턴스를 불러옴.

image

// Elvis 클래스에 메서드를 만들어줌
    private Object readResolve() {
        return INSTANCE;
    }

readResolve 라는 메서드를 역직렬화 시 사용하기 때문에 저 메서드를 정의해주면 미리 만들어둔 인스턴스를 리턴할 수 있게됨.
image

정적 팩터리 메서드를 제네릭 싱글턴 팩터리로 만들수 있다.

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 void say(T t) {
        System.out.println(t);
    }

    public void leaveTheBuilding() {
        System.out.println("Whoa baby, I'm outta here!");
    }

    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);
    }
}

클라이언트 코드에서는 모두 같은 팩터리 메서드를 사용하지만(인스턴스는 동일하지만) 각각의 원하는 타입으로 변경해 사용 가능.

elvis1와 elvis2의 해시 값은 같지만 == 비교를 하면 타입이 다르기 때문에 flase가 뜸 하지만 equals 로 비교하면 true임.

(제네릭 싱글턴 팩터리를 보면 제네릭을 앞에다가 한번 더 사용해줬는데 이건 스코프가 달라서 사용한거임.(static 스코프))

메서드 참조를 공급자로 사용할 수 있다.

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를 받는 부분에 메서드 참조를 사용하여 인스턴스를 넘길 수 있음.

임의 객체 메서드 참조

public class Person {
    LocalDate birthday;
    public Person() {}
    public Person(LocalDate birthday) {
        this.birthday = birthday;
    }

    public int compareByAge(Person b) {
        return this.birthday.compareTo(b.birthday);
    }

    public static void main(String[] args) {
        List<Person> people = new ArrayList<>();
        people.add(new Person(LocalDate.of(1982, 7, 15)));
        people.add(new Person(LocalDate.of(2011, 3, 2)));
        people.add(new Person(LocalDate.of(2013, 1, 28)));

        people.sort(Person::compareByAge);
    }

    public int getAge() {
        return LocalDate.now().getYear() - birthday.getYear();
    }
}

원래 comparator 는 두개의 인자를 받지만 임의객체 메서드 참조(static도 아니고 객체를 선언한것도 아닐때)를 할때는 첫번째 인자를 자기자신으로 하기 때문에 인수를 하나만 받도록 함.

객체직렬화

오브젝트를 바이트스트림으로 변환(데이터를 보내기위해)하는 작업 = 직렬화

직렬화된 데이터를 다시 오브젝트로 변환하는 작업 = 역직렬화

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;
    private String isbn;
    private String title;
    private LocalDate published;
    private String name;
    private transient int numberOfSold;

    public Book(String isbn, String title, String author, LocalDate published) {
        this.isbn = isbn;
        this.title = title;
        this.published = published;
    }

    public String getIsbn() {return isbn;}
    public void setIsbn(String isbn) {this.isbn = isbn;}
    public String getTitle() {return title;}
    public void setTitle(String title) {this.title = title;}
    public LocalDate getPublished() {return published;}
    public void setPublished(LocalDate published) {this.published = published;}
    public int getNumberOfSold() {return numberOfSold;}
    public void setNumberOfSold(int numberOfSold) {this.numberOfSold = numberOfSold;}
}

직렬화는 Serializable을 implements 받아야 함.

static 과 transient는 직렬화 되지 않음.

728x90
반응형