확장 가능한 enum을 만들어야 한다면 인터페이스를 이용하라

enum 자료형은 형 안전 enum 패턴보다 거의 모든 면에서 월등하다. 그러나 형 안전 enum 패턴은 계승을 통한 확장이 가능했단 반면 enum 자료형은 그 렇지 않다. 다시 말해서 형 안전 enum 패턴을 쓸 경우에는 다른 열거 자료형을 계승해서 새로운 열거 자료형을 만드는 것이 가능하지만 enum 자료형으로는 그럴 수 없다는 이야기다. 그러나 이것을 단점이라 볼 수 는 없는데 enum 자료형을 계승한다는 것을 대체로 바람직하지 않기 때문. 확장된 자료형의 상수들이 기본 자료형의 상수가 될 수 있다는 것, 그러나 그 반대는 될 수 없다는 것이 혼란 스럽다. 게다가 기본 자료형과 그 모든 하위 자료형의 enum 상수들을 순차적으로 살펴볼 좋은 방법도 없다. 마지막으로 계승을 허용하게 되면 설계와 구현에 관련된 많은 부분이 까다로워진다. (흠..)

하지만 열거 자료형의 확장이 가능하면 좋은 경우가 적어도 하나는 있다. 연산 코드를 만들어야 할 때다. 아래 코드를 보자


enum BasicOperation implements Operation { PLUS("+"){ public double apply(double x, double y) { return x + y; } }, MINUS("-"){ public double apply(double x, double y) { return x - y; } }, TIMES("*"){ public double apply(double x, double y) { return x * y; } }, DIVIDE("/"){ public double apply(double x, double y) { return x / y; } }; private final String s; BasicOperation(String s) { this.s = s; } @Override public String toString(){ return s; } } interface Operation { double apply(double x, double y); }

BasicOperation은 enum 자료형이라 계승할 수 없지만 Operation은 인터페이스라 확장이 가능하다. API가 사용하는 모든 연산은 이 인터페이스로 표현한다. 따라서 이 인터페이스를 계승하는 새로운 enum 자료형을 만들면 Operation 객체가 필요한 곳에 해당 enum 자료형의 상수를 이용할 수 있게 된다. 위의 기본 연산을 확장해서 지수 연산과 나머지 연산을 추가하고 싶다고 해보자.

enum ExtendedOperration implements Operation {
  EXP("^") {
    public double apply(double x, double y) {
      return Math.pow(x, y);
    }
  },
  REMAINDER("%") {
    public double apply(double x, double y) {
      return x % y;
    }
  };
  private final String s;

  ExtendedOperration(String s) {
    this.s = s;
  }

  @Override
  public String toString() {
    return s;
  }
}

새로 만든 연산들은 기존 연산들이 쓰였던 곳에는 어디든 사용할 수 있다. API가 BasicOperation이 아니라 Operation을 사용하도록 작성되어 있기만 하면 된다. 상수별 메서드 구현을 이용해서 확장 불가능 enum을 만들 때는 apply 메서드를 abstract로 선언할 필요가 있었는데 여기서는 그럴 필요가 없다.

아래 코드를 보자

public static void main(String[] args) {
  test(ExtendedOperration.class, 20, 14);
}

private static <T extends Enum<T> & Operation> void test(Class<T> opSet, double x, double y) {
  for (Operation op : opSet.getEnumConstants()) {
    System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
  }
}

확장된 연산을 나타내는 자료형의 class 리터럴인 ExtendedOperration.class가 main에서 test로 전달되고 있음에 유의하자. 확장된 연산 집합이 무엇인지 알리기 위한 것이다. 이 class 리터럴은 한정적 자료형 토큰 구실을 한다. opSet의 형인자 T는 굉장히 복잡하게 선언 되었는데
& Operation>, class 객체가 나타내는 자료형이 enum 자료형인 동시에 Operation의 하위 자료형이 되도록 한다.

다음으로는 한정적 와일드카드 자료형을 opSet인자의 자료형으로 사용하는 것이다.

public static void main(String[] args) {
  test1(Arrays.asList(ExtendedOperration.values()), 20, 14);
}

private static void test1(Collection<? extends Operation> opSet, double x, double y) {
  for (Operation op : opSet) {
    System.out.printf("%f %s %f = %f%n", x, op, y, op.apply(x, y));
  }
}

조금 덜 복잡한 코드가 만들어지긴 하지만, test 메서드의 코드는 조금 더 복잡해졌다. 메서드를 호출할 때 여러 enum 자료형에 정의한 연산들을 함께 전달할 수 있도록 하기 위한 것이다. 그러나 이렇게 하면 EnumSet이나 EnumMap을 사용할 수 없기 때문에 여러 자료형에 정의한 연산들을 함께 전달할 수 있도록 하는 유연성이 필요 없다면, 한정적 자료형 토큰을 쓰는 편이 낫다.

정리

계승 가능 enum 자료형은 만들 수 없지만, 인터페이스를 만들고 그 인터페이스를 구현하는 기본 enum 자료형을 만들면 계승 가능 enum 자료형을 흉내 낼 수 있다. 그렇게 하면 클라이언트가 해당 인터페이스를 구현하는 enum 자료형을 손수 만들 수 있다. API가 인터페이스를 이용해 작성 되었다면, 그런 enum자료형은 기본 enum 자료형이 쓰이는 곳에는 어디든 사용할 수 있다.

객체화된 기본 자료형 대신 기본 자료형을 이용하라

오랜만에 이펙티브자바!

자바의 자료형 시스템은 두부분으로 나뉜다. 하나는 int, double, boolean 등의 기본 자료형, 다른 하나는 String과 List 등의 참조 자료형이다. 모든 자료형에는 대응되는 참조 자료형이 있는데 이를 객체화된 기본 자료형(boxed primitive type)이라 부른다. int, double, boolean의 객체화된 기본 자료형은 각각 Integer, Double, Boolean이다.
자바 1.5부터 자동 객체화(autoboxing)와 자동 비객체화(auto-unboxing)가 언어의 일부가 되었다. 그 둘사이에는 실직적인 차이가 있으므로 둘 가운데 무엇을 사용하고 있는지를 아는 것이 중요하며, 어떤 것을 사용할지 신중하게 결졍해야 된다.

기본 자료형과 객체화된 자료형 사이에는 세 가지 큰 차이점이 있다. 첫 번째는 기본 자료형은 값만 가지지만 객체화된 기본 자료형은 값 외에도 신원(inentity)(아이덴티티 신원?흠..)을 가진다는 것이다. 따라서 객체화된 기본 자료형 객체가 두 개 있을 때, 그 값은 같더라고 신원..은 다를 수 있다. 두번째는 기본 자료형에 저장되는 값은 전부 기능적으로 완전한 값이지만 객체화된 기본 자료형에 저장되는 값에는 그 이외에도 아무 기능도 없는 값 null이 있다는 것이다. 세번째는 기본 자료형은 시간이나 공간 요구량 측면에서 일반적으로 객체 표현형보다 효율적이라는 것이다. 주의하지 않으면 이런 차이 때문에 곤란을 겪게 될 것이다. 아래의 비교자 예제를 보자

private static final Comparator<Integer> naturalOrder = new Comparator<Integer>() {
  @Override
  public int compare(Integer o1, Integer o2) {
    return o1 < o2 ? -1 : (o1 == o2 ? 0 : 1);
  }
};

이 반복자는 얼핏보기엔 괜찮고 많은 테스트를 별 문제 없이 통과할 것이다. 예를 들어 위의 반복자는 Collections.sort와 함께 백만개 원소를 갖는 리스트를 (중복 여부 상관 없이) 정확히 정렬하는 용도로 사용 될 수 있다. 하지만 이 반복자에는 심각한 문제가 있다. 아래와 같이 해보자.

int compare = naturalOrder.compare(new Integer(42), new Integer(42));
System.out.println(compare);

두 객체는 42라는 동일한 값을 나타내므로 0을 반환해야 된다. 하지만 실제로 반환되는 값은 1이다. 첫번째 Integer 객체가 두 번째 보다 크다고 나온다.
눈치는 챗겠지만 두번째 o1 == 02 ? 0 : 1 삼항 연산자에 있는 ==은 객체 참조를 통해 객체인 경우 == 는 false를 반환할 것이므로 비교자는 1이라는 잘못된 값을 반환한다. 객체화된 기본 자료형에 == 연산자를 사용하는 것은 거의 항상 오류라고 봐야 한다.

이 문제를 고치는 방법으로는 int 변수에 오토박싱해서 담아 비교를 하는 것이다.

private static final Comparator<Integer> naturalOrder1 = new Comparator<Integer>() {
  @Override
  public int compare(Integer o1, Integer o2) {
    int f = o1;
    int s = o2;
    return f < s ? -1 : (f == s ? 0 : 1);
  }
};

위와 같이 하면 == 신원(참조) 비교를 피할 수 있다.

이제 아래의 간단한 프로그램을 보자

static Integer i;

public static void main(String[] args) {
  if(i == 42){
    System.out.println("Unbelievable");
  }
}

이 프로그램은 Unbelievable을 출력하지 않는데 출력하지 않는 것만큼이나 이상한 짓거리를 한다. 문제는 i가 int가 아니라 Integer라는 것이다. 그리고 모든 객체 참조 필드가 그렇듯, 그 초기 값은 null이다. 위의 프로그램이 (i == 42)를 계산할 때 비교되는 것은 Integer 객체와 int 값이다. 거의 모든 경우에, 기본 자료형과 객체화된 기본 자료형을 한 연산 안에 엮어 놓으면 객체화된 기본 자료형은 자동으로 기본 자료형으로 변환된다. 위의 코드도 예외는 아니다. null인 객체 참조를 기본 자료형으로 변환하려 시도하면 NullpointException이 발생한다. 이문제는 간단하게 Integer를 int로 변환하면 잘 동작한다.

우리는 저번에 무시무시할 정도로 느린 프로그램을 봤었다. 쓸데없이 객체를 만들지 말자!

Long startTime = System.currentTimeMillis();
Long sum = 0L;
for (long i = 0; i < Integer.MAX_VALUE; i++) {
  sum += i;
}
System.out.println(sum);
System.out.println(System.currentTimeMillis() - startTime);

이 프로그램은 우리가 예상했던거 보다 훨씬 느리다. 지역변수 sum은 long아니라 Long으로 선언 했기 때문이다. 오류도 없지만 변수가 계속해서 객체화와 비객체화를 반복하기 때문에 성능이 느려진다.

그렇다면 객체화된 기본 자료형은 언제 사용해야 하나? 첫번째는 컬렉션의 요소, 키, 값으로 사용할 때 이다. 컬렉션에는 기본 자료형은 넣을 수 없다. 다시 말해 ThreadLocal<int> 같은 변수는 선언할 수 없다. 대신 ThreadLocal<Integer>를 써야 한다. 리플렉션을 통해 메서드를 호출 할 때도 객체화된 기본 자료형을 사용해야 한다.

요약하자만 가능하다면 기본 자료형을 사용하라는 것이다. 기본 자료형이 더 단순하고 빠르다. 객체화된 기본 자료형을 사용해야 한다면 주의하라! 자동 객체화는 번거러운 일을 줄여주긴 하지만, 객체화된 기본 자료형을 사용할 때 생길 수 있는 문제들까지 없애주진 않는다. 객체화된 기본 자료형 객체 두 개를 ==로 비교한다는 것은 그 두 객체의 신원을 비교한다는 것이며 그 것은 십중팔구 원하는 결과가 나오지 않는다. 객체화된 기본 자료형과 기본형을 한 표현식 안에 뒤섞으면 비객체화가 자동으로 일어나며, 그 과정에서 NullpointException이 발생할 수 있다. 또한 기본 ㅏㅈ료형 값을 객체화하는 과정에서 불필요한 객체들이 만들어지면 성능이 저하될 수 도 있다.

초기화 지연은 신중하게 하라!

이펙이트 자바!

초기화 지연은 신중하게 하라
초기화 지연(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으로 구현하는 것이다.