[item#03] private 생성자나 열거타입으로 싱글턴임을 보증하라
https://readystory.tistory.com/116
private 생성자나 열거타입으로 싱글턴임을 보증하라
싱글턴(singleton) : 인스턴스를 하나만 생성할 수 있는 클래스
(인터페이스를 구현해서 만든 싱글턴이 아니면 mock 구현으로 대체할 수 없음 => (1)싱글턴은 클라이언트가 테스트하기 어려울 수 있음)
싱글턴을 만드는 방식
모두 공통적으로 생성자는 private으로 감춤, 유일한 인스턴스 접근수단으로 public static 멤버를 만듬
public static 멤버가 final 인 방식
public class Elvis{ public static final Elvis INSTANCE = new Elvis(); private Elvis(){...} public void leaveTheBuilding(){...} }
private 생성자가 Elvis.INSTANCE를 초기화할 때 딱 한번만 호출되어 싱글턴임을 보장.
but 리플렉션을 사용해 private 생성자 호출 가능. => 이럴때는 두번째 객체생성 때 오류 던져주면됨.장점
해당 클래스가 싱글턴인게 명확히 드러남.
간결함.정적 팩터리 메서드를 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; }
원소가 하나인 열거타입을 선언 * 가장 추천
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 생성자를 사용하게될 뿐만 아니라.
생성자를 사용하게 되었기 때문에 모두 다른 객체를 반환한다. => 싱글턴이 깨짐.
=> 이걸 해결하기 위해 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();
}
}
}
직렬화 : 객체의 정보를 어딘가에 저장해놓고, 역직렬화 : 어딘가에 저장되어있는 객체정보들을 읽어옴.
역직렬화 시 생성자를 사용하기 때문에 다른 인스턴스를 불러옴.
// Elvis 클래스에 메서드를 만들어줌
private Object readResolve() {
return INSTANCE;
}
readResolve 라는 메서드를 역직렬화 시 사용하기 때문에 저 메서드를 정의해주면 미리 만들어둔 인스턴스를 리턴할 수 있게됨.
정적 팩터리 메서드를 제네릭 싱글턴 팩터리로 만들수 있다.
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는 직렬화 되지 않음.
'Study > Effective-Java' 카테고리의 다른 글
[item#07] 다 쓴 객체 참조를 해제하라. (1) | 2023.11.27 |
---|---|
[item#06] 불필요한 객체 생성을 피하라 (2) | 2023.11.27 |
[item#05] 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (1) | 2023.11.27 |
[item#02] 정적 팩터리와 생성자에 선택적 매개변수가 많을 때 고려할 수 있는 방안 (1) | 2023.11.27 |
[item#01] 생성자 대신 정적 팩터리 메서드를 고려하라 (0) | 2023.11.27 |
댓글