ITEM 3 "private 생성자나 열거 타입으로 싱글톤임을 보장하라"
이 ITEM 을 확인하기 전에 위 백기선님의 Effective Java 해설 영상을 보는 것을 추천한다.
열심히 문어체로 정리하고 작성하지만, 구어체를 따라 올 전달력은 없는 것 같다. 열심히 예를 들어 설명해주셔서 덕분에 이해가 잘 됐다.
이번 장에서는 private 생성자나 열거 타입으로 싱글톤임을 보장해야 한다고 서술하고 있다.
싱글톤 패턴이란, 객체의 인스턴스가 오직 1개만 생성이 되는 패턴을 말한다.
최초 한 번만 메모리에 할당이 되고 그 인스턴스를 어디에서든지 참조할 수 있도록 할 수 있다. 어플리케이션 로딩 시 꼭 한 개의 인스턴스 생성만 필요한 경우에 이 패턴을 사용한다. 설정 파일이나 데이터베이스 연결 등이 그 예이다.
싱글톤 클래스를 사용할 때, 한 번 생성된 객체를 공용으로 사용하고 별도로 생성하는 것을 금지한다고 약속할 수 있다. 하지만 사람이기 때문에 실수할 수 있다. 한 번만 생성해야 하는 객체를 여러 번 생성할 수 있다.
그래서 클래스 선언 시 외부에서 인스턴스를 생성하지 못하게 별도의 장치를 설정하는데 이번 장에서 소개할 private 생성자와 열거 타입이 그 주인공이다.
첫 번째 방법은,
private 생성자를 사용하여 클래스 내부에 public static final 필드로 객체를 생성해두어 그 인스턴스를 반환하는 방법이다. final 필드이기 때문에 초기화 될 때 한 번만 호출된다.
// 선언부
public class Test {
public static final Test INSTANCE = new test();
private Test(){}
}
// 사용부
Test test1 = Test.INSTANCE;
Test test2 = Test.INSTANCE;
두 번째 방법은,
첫 번째 방법과 유사한데 public static final 필드를 private static final 필드로 바꾸고, 정적 팩토리 메서드를 제공하는 것이다.
public class Test {
private static final Test INSTANCE = new test();
private Test() {}
public static Test getInstance() { return INSTANCE; }
}
두 방법 모두 항상 같은 객체를 반환하므로 안전해보인다. 이 책에서는 Reflection API 를 통해 setAccessible 을 사용해 권한을 획득하고 private 생성자를 호출할 수 있다고 하는데 사실 협업 과정에서 저렇게까지 코딩하지는 않을 것 같다. ㅎㅎㅎ;;; 싱글톤처럼 전역적으로 사용하는 객체는 안전하게 제공해야 할 의무가 있기 때문에 책에서 제안하는 것처럼 생성자에 두 번째 생성자를 생성할 때 Exception 예외를 던지자.
// 생성자 내부에서 count 변수 체크 후 1 이상이면 예외 출력
static int count;
private Test(){
count++;
if (count != 1) {
throw new IllegalStateException("this object should be singleton");
}
}
첫 번째 방법은 간결하고 해당 클래스가 싱글톤이라는 점을 명백히 알려준다.
public static 이 final 이니 절대로 다른 객체를 참조할 수 없다.
두 번째 방법은 싱글톤이 아니게 변경할 수 있다는 장점이 있다. 팩터리 메서드에서 반환하던 싱글톤 객체를 다른 객체로 변경이 가능하다. 또 제네릭 싱글톤 팩터리로 만들어 줄 수 있다. 마지막으로 정적 팩터리의 메서드 참조를 공급자로 사용할 수 있다. getInstance 를 Supplier<객체> 로 사용하는 식이다.
이러한 장점들이 있지만 싱글톤이 아니게 변경될 수 있다는 유연한 구조 때문에 두 번째 방법의 장점을 사용하지 않을 것이라면 가급적 첫 번째 방법을 권장하고 있다.
안전하게 싱글톤 객체를 만들기 위해 한 가지 경우를 더 생각해야 한다. Java 에서는Serializable 클래스로 직렬화를 제공하고 있는데 역직렬화할 때 가짜 객체를 생성한다. 이 가짜 객체 대신 진짜 객체를 반환하기 위해서 readResolve 함수를 재정의해야 한다. (모든 필드를 transient 로 선언해야 한다는 점도 잊지 말아야 한다.) 아래는 Java 에서 Serializable 클래스를 사용했을 때 직렬화/역직렬화 구조이다.
앞서 소개한 두 방법은 리플렉션이나 직렬화 문제 때문에 보완적인 코드들을 많이 생성해야 한다. 마지막 세 번째 방법, Enum 열거를 활용한 방법도 있다. 복잡한 직렬화 상황이나 리플렉션 구조에도 완벽히 방어할 수 있으며 간단한다. 하지만 상속은 못 한다는 단점이 있다.
// 선언부
public enum Test {
INSTANCE;
public String getName() {
return "gold-egg";
}
}
// 사용부
String name = Test.INSTANCE.getName();
'독후감 > Effective JAVA' 카테고리의 다른 글
[Effective JAVA] 6 "불필요한 객체 생성을 피하라" (0) | 2022.04.28 |
---|---|
[Effective JAVA] 5 "자원을 직접 명시하지 말고 의존 객체 주입을 사용하라" (0) | 2022.04.27 |
[Effective JAVA] 4 "인스턴스화를 막으려거든 private 생성자를 사용하라" (0) | 2022.04.26 |
[Effective JAVA] 2 "생성자에 매개변수가 많다면 빌더를 고려하라" (0) | 2022.04.21 |
[Effective JAVA] 1 "생성자 대신 정적 팩터리 메서드를 고려하라" (0) | 2022.04.20 |