2017년 4월 17일 월요일

effective java 규칙 3. private 생성자나 enum 자료형은 싱글턴 패턴을 따르도록 설계하라 (singleton)

하나의 객체만을 사용하도록 강제할때 주로 사용하는 패턴으로 singleton pattern 이 있다.
singleton pattern은 매우 흔히 사용되는 패턴으로 그 구현 방법도 여러 가지이다.

1. public final 필드를 이용한 방법은 가장 손쉽게 싱글턴 객체를 생성 하는 방법이다.

public class SingletonByPublicStaticVariable {

public static final SingletonByPublicStaticVariable INSTANCE = new SingletonByPublicStaticVariable();
private SingletonByPublicStaticVariable() {}

public void test() {
System.out.println("singleton by public static final variable");
}

}
위 코드는 클래스가 로드될때 초기화를 통하여 INSTANCE 변수에 객체가 생성되어 할당된다.
이 방법의 장점은 선언만 보고 싱글턴 여부를 바로 알 수 있지만 클래스가 로드될때 항상 객체가 생성된다.
가령 객체로 생성할 필요없이 public static method 를 사용할때에도 객체는 변수에 할당된다.
그리고 객체가 생성될때 에러처리도 불가능하며 객체 생성 전에 로직이 필요한 경우엔 사용할 수 없다.


2. static 블록을 이용한 방법은 1번 방법을 조금 개선한 방식이다.

public class SingletonByStaticBlock {
private static final SingletonByStaticBlock INSTANCE;
static {
INSTANCE = new SingletonByStaticBlock();
}

public static SingletonByStaticBlock getInstance() {
return INSTANCE;
}

public void test() {
System.out.println("singleton by static block");
}
}
static 초기화 블럭은 클래스가 로딩 될 때 최초 한번만 실행된다.
1번과 비교하여 객체를 생성하기전에 로직을 담을 수 있기 때문에 객체생성 로직 또는 에러처리 구문이나 초기변수 셋팅등의 코드 작성이 가능하다.
예를 들자면 현재시간을 체크하여 연초면 2개의 인자를 받는 생성자를 통해 객체를 생성하고 연말이면 3개의 인자를 받는 생성자를 사용한다던가의 로직 기술이 가능하다.
다만 이방식도 클래스가 로드되는 시점에 무조건 객체가 생성된다.

3. 정적 팩터리를 이용한 싱글턴 방식

public class SingletonByFactory {
private static final SingletonByFactory INSTANCE = new SingletonByFactory();

private SingletonByFactory() {
}

public static SingletonByFactory getInstance() {
return INSTANCE;
}

public void test() {
System.out.println("singleton by factory method");
}
}

이 방식은 싱글턴패턴을 포기하는 상황에서 좀더 유연하게 작동한다.
1번 방식으로 구현했을때 싱글턴을 변경하면 해당 클래스를 사용하는 클라이언트 코드는 모두 수정이 불가피하게 된다.
정적 팩터리를 사용하면 메소드를 사용하기 때문에 내부 구현의 변화에 따른 클라이언트 코드의 영향을 줄일 수 있다.
성능적인 측면에서도 최신 jvm 은 정적 팩터리 메서드 호출을 거의 항상 인라인 처리하기 때문에 1번과 성능차이는 거의 없다.
그리고 이방식은 제너릭 타입을 수용하기에 용이하다.


4. lazy initialization 방식

1~3번 방식은 모두 클래스 로딩시점에 객체가 생성되어진다. lazy initialization 은 이러한 부담을 개선한 방식이다.

public class SingletonByLazyInit {
private static SingletonByLazyInit INSTANCE;
private SingletonByLazyInit() {}

public static SingletonByLazyInit getInstance() {
if(INSTANCE == null)
INSTANCE = new SingletonByLazyInit();

return INSTANCE;
}

public void test() {
System.out.println("singleton by lazy initialization");
}
}
위 코드에 나타난 대로 객체는 getInstance 메소드 실행되고 객체가 생성 된 적이 없어야만 객체가 생성된다.
그러나 이 방식도 잠재된 문제점이 있다.
동일 시점에 getInstance 가 호출될때 가령 multi thread 환경에서 객체가 두개 이상 생성될 가능성이 있다.

5. thread safe lazy initialization 방식
이방식은 4번의 문제점을 개선한 방석이다.

public class SingletonByLazyInitThreadSafe {
private static SingletonByLazyInitThreadSafe INSTANCE;

private SingletonByLazyInitThreadSafe() {
}

public static synchronized SingletonByLazyInitThreadSafe getInstance() {
if(INSTANCE == null)
INSTANCE = new SingletonByLazyInitThreadSafe();

return INSTANCE;
}

public void test() {
System.out.println("singleton by thread safe initialize ");
}
}

코드는 거의 유사하나 getInstance method 에 synchronized 를 선언해 여러 thread 가 동시에 접근하지 못하도록 했다.
그러나 역시 여러 thread 가 getInstance 를 호출하게되면 높은 cost 비용으로 인해 프로그램 전반에 성능저하가 발생한다.

6. Enum 싱글턴 방식

4~5번 문제를 해결하는 effective java 에서 제안 하는 방식은 enum 싱글턴이다. effective java 에서는 이 방식이 가장 낫다고 한다.

public enum SingletonByEnum {
INSTANCE;

public void test() {
System.out.println("singleton by enum");
}
}

enum 방식은 일반적인 우리가 알고 있는 열거형을 싱글턴 패턴에 이용하는 방식이다.
원소가 하나인 enum 자료형을 정의하고 다른 method 들은 class 작성과 동일하게 작성한다.
effective java 에 따르면
단 한번의 인스턴스 생성을 보장하며 사용이 간편하고 직렬화가 자동으로 처리되고 직렬화가 아무리 복잡하게 이루어져도 여러 객체가 생길 일이 없으며, 리플렉션을 통해 싱글턴을 깨트릴수도 없다고 한다.
그리고 multi thread 로부터 안전하다. 
다만 이 방식은 생소하기도 하고 enum 자료형의 일반적인 쓰임새와 혼동될 것 같다는 생각이 든다. 

7. holder idiom 방식

이 방식 또한 thread 에 대해 안전하다
이 방식은 jvm 상에서 클래스 초기화 단계에 따른 특성을 활용한 방식이다.
public class SingletonByIdiom {

private SingletonByIdiom() {

}

private static class Singleton {
private static final SingletonByIdiom INSTANCE = new SingletonByIdiom();
}

public static SingletonByIdiom getInstance() {
return Singleton.INSTANCE;
}

public void test() {
System.out.println("singleton by idiom");
}
}

SingletonByIdiom 클래스는 클래스 로드 시점에 초기화되지만 정적 클래스로 정의된 내부 클래스의 초기화는 해당 시점에 이뤄지지 않는 특성이 있다.
즉 getInstance 를 통해 내부 클래스의 instance 를 호출할때 뒤늦게 초기화 되어 객체를 할당한다.
이 방식이 thread safe 한 이유는 jvm 의 클래스 초기화 과정에서 보장되는 원자적 특성(시리얼하게 동작)을 이용하기 때문이다. 즉 동기화 문제를 jvm이 처리 하도록 한다.
 


[참고 : 클래스가 최초로 초기화 되는 시점]
T에서 선언한 정적 필드가 상수 변수가 아니며 사용되었을 때
T가 클래스이며 T의 인스턴스가 생성될 때
T가 클래스이며 T에서 선언한 정적 메소드가 호출되었을 때
T에서 선언한 정적 필드에 값이 할당되었을 때
T가 최상위 클래스(상속 관계에서)이며 T안에 위치한 assert 구문이 실행되었을 때





직렬화 가능 클래스에서 역직렬화시 새로운 객체가 생기게 되는 문제 (singleton 깨짐, 단 enum 싱글턴 방식은 역직렬화에 따른 문제점이 없다. - holder idiom 방식은 미확인)

싱글턴 특성을 유지하려면 
1. 모든 필드를 transient 로 선언
2. readResolve 메서드 추가

// 싱글턴 상태를 유지하기 위한 readResolve 구현
private Object readResolve() {
    // 동일한 객체가 반환되도록 하는 동시에, 가짜 객체는 gc 가 처리하도록 만듬.
    return INSTANCE;
}


참고 사이트

댓글 1개:

Intelij 설정 및 plugin

1. preferences... (settings...) Appearance & Behavior > Appearance - Window Options        ✓   Show memory indicator Editor &...