이펙이트 자바!

초기화 지연은 신중하게 하라
초기화 지연(Lazy initialization)은 필드 초기화를 실제로 그 값이 쓰일 때까지 미루는 것이다. 값을 사용하는 곳이 없다면 필드는 결코 초기화되지 않을 것이다.
이 기법은 static 필드와 객체 필드에 모두 적용 가능하다.
대부분의 최적화가 그렇듯이, 초기화 지연을 적용할 때 따라야 할 최고의 지침은 정말로 필요하지 않으면 하지마라라는 것이다.
클래스를 초기화하고 객체를 생성하는 비용은 줄이지만, 필드 사용 비용은 증가 시킨다. 초기화 지연이 적용된 필드 가운데 실제로 초기화되어야하는 필드의 비율, 초기화 비용, 그리고 필드의 실제 이용 빈도에 따라 실제 성능은 떨어질 수 도 있다.
다시 말해서 초기화 지연 기법이 어울리는 곳이 따로 있다는 것이다. 필드 사용 빈도가 낮고 초기화 비용이 높다면 쓸만할 것이다.
실제로 대부분의 경우에는 지연된 초기화를 하느니 일반적으로 초기화하는 것이 낫다.

  //객체 필드를 일반적으로 초기화하는 방법
  private final FieldType field = new FieldType();

  //동기화 된 접근자를 사용한 객체 필드 초기화 지연방법
  private FieldType fieldType;

  synchronized FieldType getField() {
    if (fieldType == null) {
      fieldType = new FieldType();
    }
    return fieldType;
  }

초기화 순환성 문제를 해소하기 위해서 초기화를 지연시키는 경우에는 동기화 된 접근자를 사용하라.
위에 두개의 코드는 정적(static) 필드에도 똑같이 적용 가능하다. 차이라고는 static이 붙는다는 것 뿐이다.

성능 문제 때문에 정적 필드 초기화를 지연시키고 싶을 때는 초기화 지연담당 클래스(Lazy initialization holder class)를 적용하라.(요청 기반 초기화 담당 클래스 initialize-on-demand holder class) 클래스보다 이름이 더 길다는 그런…말이 있다.

  //정적 필드에 대한 초기화 지연 담당 클래스 
  private static class FieldHolder{
    static final FieldType field = new FieldType();
  }
  static FieldType getFieldType(){
    return FieldHolder.field;
  }

FieldHolder클래스는 FieldHolder.field가 처음으로 이용되는 순간, 그러니까 getFieldType 메서드가 처음으로 호출되는 순간에 초기화 된다. 이것이 좋은 점은 getFieldType을 동기화 메서드를 선언 하지 않아도 된다는 점이다. 따라서 초기화를 지연시켜도 메서드 이용 비용은 전혀 증가 하지 않는다.

성능 문제 때문에 객체 필드 초기화를 지연시키고 싶다면 이중검사를 사용하라.

  private volatile FieldType field1;

  FieldType getField1(){
    FieldType result = field1;
    if(result == null){
      synchronized (this){
        result = field1;
        if(result == null){
          field1 = result = new FieldType();
        }
      }
    }
    return result;
  }

이코드는 조금 난해해 보인다. (필자가 봐도 그렇다..) 특히 지역변수 result를 사용한 이유가 명확하지 않다. 이변수가 하는일은 이미 초기화된 필드는 딱 한번만 읽도록 하는 것이다. 엄밀하게 말해서 필요한 것 아니지만 성능을 높일 가능성이 있는 데다 저수준 병렬 프로그래밍에 적용되는 표준에 비춰 봐도 좀 더 우아하다.(글쎄다..필자는 우아한지 모르겠다ㅜㅜ)
오늘날 이중 검사 숙어는 객체 필드 초기화를 지연시키고자 할 때 반드시 사용해야 하는 것이라고 한다.
정적 필드에도 적용할 수는 있으나 그럴 이유는 없다. 초기화 지연담당 클래스를 사용하는 것이 더 바람직하다.

대부분의 필드 초기화는 지연시키지 않아야 한다. 더 좋은 성능을 내거나 해로운 초기화 순환성을 제거할 목적으로 필드 초기화를 지연시키고자 할 때는 적절한 초기화 지연 기술을 이용해라.

우리는 대부분(다는 아니지만) 싱글톤객체를 만들기 위해 저런 짓을 한다. 그러나 요즘은 프레임워크단에서 관리를 해주고 있다. 그래서 굳이 사용할 일은 없지만 그래도 혹시나 써야할 상황이 온다면 홀더 패턴 보다 더 좋은 싱글톤 패턴이 있다.

  enum Singleton {
    INSTANCE;
    public static Singleton getInstance() {
      return INSTANCE;
    }
  }

위의 코드는 스레드 세이프 하면서 시리얼라이즈에도 안전하다. Enum은 Serializable (구현?) 상속 받고 있다.
예를 들어 싱글톤 객체를 시리얼라이즈했는데 다시 디시리얼라이즈해서 비교를 해보면 다르다.

    FieldType fieldType = Role71.getFieldType();

    try (FileOutputStream fos = new FileOutputStream("/Users/wonwoo/test/s.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos)) {
      oos.writeObject(fieldType);
    } catch (Exception e) {
    }

    try (FileInputStream fis = new FileInputStream("/Users/wonwoo/test/s.ser"); ObjectInputStream ois = new ObjectInputStream(fis)) {
      FieldType obj = (FieldType) ois.readObject();
      System.out.println(fieldType == obj);
    } catch (Exception e) {
    }

테스트를 해보자 정말 그렇게 되나.
일반 싱글톤으로 만들었을 때이다. Serializable 구현하고 시리얼라이즈 후 다시 디시리얼라이즈해서 비교를 하면 false가 나온다.
객체가 두개가 된것이다.
그럼 enum으로 했을 때는 어떤 경우가 나오는지 보자.

    Singleton fieldType = Singleton.getInstance();

    try (FileOutputStream fos = new FileOutputStream("/Users/wonwoo/test/s.ser"); ObjectOutputStream oos = new ObjectOutputStream(fos)) {
      oos.writeObject(fieldType);
    } catch (Exception e) {
    }

    try (FileInputStream fis = new FileInputStream("/Users/wonwoo/test/s.ser"); ObjectInputStream ois = new ObjectInputStream(fis)) {
      Singleton obj = (Singleton) ois.readObject();
      System.out.println(fieldType == obj);
    } catch (Exception e) {
    }

테스트를 해보면 true가 나온다. 시리얼라이즈하고 디시리얼라이즈 해도 같은 객체인 것이다.
싱글톤 구현중 제일 안전한 방식은 잘 알려지지는 않았지만 enum으로 구현하는 것이다.