반응형

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

 

반응형

+ Recent posts