Item 37 - ordinal 인덱싱 대신 EnumMap을 사용하라

ordinal 인덱싱 대신 EnumMap을 사용하라

Intro

  • 배열이나 리스트에서 원소를 꺼낼 때 ordinal 메서드 아이템 35로 인덱스를 얻는 코드가 있다.

/**
 * 식물을 나타내는 클래스
 */
public class Plant {
    // 식물의 생애 주기를 관리하는 열거 타입
    enum LifeCycle {
        ANNUAL, // 한해살이
        PERENNIAL, // 여러해살이
        BIENNIAL // 두해살이
    }

    final String name;
    final LifeCycle lifeCycle;

    Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }

    @Override
    public String toString() {
        return name;
    }
}
  • 정원에 심은 식물들을 배열 하나로 관리하고, 이들을 생애주기(한해살이, 여러해살이, 두해살이)별로 묶는다.

  • 생애주기 별로 총 3개의 집합을 만들고 정원을 한 바퀴 돌며 각 식물을 해당 집합에 넣는다.

이때, 집합들을 배열 하나에 넣고, 생애주기의 ordinal 값을 그 배열의 인덱스로 사용할 수 있다.

  • 정원의 식물 입력 테스트

위 코드에 문제가 한가득이다.

  • 배열은 제네릭과 호환되지 않으니 비검사 형변환을 수행해야 하고, 깔끔하게 컴파일되지 않는다. 아이템 28

  • 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.

  • 가장 심각한 문제는 인수 값으로 정확한 정숫값을 사용하고 있다는 것을 개발자가 직접 보장해야 한다.

    • 정수는 열거 타입과 달리 타입 안전하지 않기 때문이다.

  • 잘못된 값을 사용하는 경우 배열의 인덱스에 따른 값이 의도한 값을 보장할 수 없고, ArrayIndexOutOfBoundsException을 발생할 수 있다.

해결책

  • 배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 역할을 한다.

    • 이는 Map을 사용할 수 있도록 한다.

    • 열거 타입을 키로 사용하도록 설계 한 아주 빠른 Map의 구현체(EnumMap) 가 존재한다.

EnumMap을 사용 예시

  • Enum 타입을 Key 값으로 하는 EnumMap 구현체

  • EnumMap 을 사용하여 구현된 테스트

  • Set[] 을 사용하여 관리하는 것보다 EnumMap을 사용하는 것이 간결하고 성능도 비슷하다.

  • 안전하지 않은 형변환을 쓰지 않고, Map의 Key인 열거 타입이 그 자체로 출력용 문자열을 제공하기 때문에 출력 결과에 레이블을 달 일도 없다.

  • 배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 배제할 수 있다.

  • EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다.

  • 내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성배열의 성능을 모두 얻어낸 것이다.

  • EnumMap의 생성자가 받는 키 타입의 Class 객체는 한정적 타입 토큰으로, 런타임 제네릭 타입 정보를 제공한다. 아이템 33

스트림 아이템 45을 사용하여 맵을 관리하면 코드를 더 줄일 수 있다.

  • Prototype 의 스트림 기반 코드

  • 이 코드는 EnumMap이 아닌 고유한 맵 구현체를 사용하기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 발생한다.

  • 위 문제점을 최적화 하기

    • 매개변수 3개 짜리 Collections.groupingBy 메서드는 mapFactory 매개변수에 원하는 맵 구현체를 명시해 호출할 수 있다.

    • 아래 코드는 Map을 빈번하게 사용하는 프로그램에서 최적화하는 방법이다.

스트림과 EnumMap 비교

  • 스트림을 사용하면 EnumMap만 사용했을 때와는 다르게 동작한다.

    • EnumMap 버전은 언제나 식물의 생애 주기당 하나씩의 중첩 맵을 만든다.

    • Stream은 해당 생애주기에 속하는 식물이 있을 때만 만든다.

    • EnumMap 버전에서는 Map을 3개 만들고, Stream 버전에서는 2개만 만든다.

  • EnumMap 버전 사용 및 결과 값

  • Collections.groupingBy enum 필드 값으로 그룹핑 및 결과 값

  • groupingBy의 최적화 및 결과 값

두 열거 타입의 값들을 매핑하느라 ordinal을 (두 번이나) 쓴 배열들의 배열의 상황

  • 두 개의 열거 타입을 억지로 매핑하기 위해 ordinal을 두 번이나 쓴 잘못된 방법

  • 문제점

    • 컴파일러가 ordinal과 배열 인덱스의 관계를 알 수 없다.

    • 즉, Phase나 Phase.Transition 열거 타입을 수정하면서 표 TRANSITIONS 를 함께 수정하지 않거나 실수로 잘못 수정하면 런타임 오류가 발생할 것이다.

    • ArrayIndexOutOfBoundsException 이나 NullPointerException 을 던질 수도 있고, 예외없이 의도하지 않도록 동작할 수 있다.

    • 표의 크기는 상태의 가짓수가 늘어나면 제곱해서 커지며, null로 채워지는 칸도 늘어날 것이다.

EnumMap으로 해결하기

  • 전이 하나를 얻기 위해 이전 상태(from)와 이후 상태(to)가 필요

  • Map 2개를 중첩하여 쉽게 해결해보기

    • 안쪽 Map은 이전 상태와 TRANSITION 을 연결

    • 바깥 Map은 이후 상태와 안쪽 Map을 연결

    • OuterMap -> 이후 상태 & InnerMap -> 이전 상태 & TRANSITION

  • 전이 전후의 두 상태를 전이 열거 타입 Transition의 입력으로 받아, 이 Transition 상수들로 중첩된 EnumMap을 초기화 한다.

  • 외부 Map의 타입인 Map> 의 의미

    • "이전 상태에서 '이후 상태에서 전이로의 Map' 에 대응시키는 Map"이라는 뜻

    • 이러한 Map의 Map을 초기화하기 위해 수집기(java.util.stream.Collector) 2개를 차례로 사용되었다.

    • 첫 번째 수집기인 groupingBy에서는 전이를 이전 상태를 기준으로 묶었다.

    • 두 번째 수집기인 toMap 에서는 이후 상태를 전이에 대응시키는 EnumMap을 생성한다.

      • 두 번째 수집기의 병합 함수인 (x, y) -> y는 선언만 하고 실제로는 쓰이지 않는다.

      • 이는 단지 EnumMap을 얻으려면 MapFactory가 필요하고 수집기들은 점층적 팩터리(telescoping factory)를 제공하기 때문이다.

새로운 상태를 추가하는 경우

  • 새로운 상태인 플라즈마(PLASMA) 추가

    • 이 상태와 연결된 전이는 2가지

      • 첫 번째는 기체에서 플라즈마로 변하는 이온화(IONIZE)

      • 두 번째는 플라즈마에서 기체로 변하는 탈이온화(DEIONIZE)

배열로 만든 코드를 수정하는 경우

  • 새로운 상수를 Phase에 1개, Phase.Transition 에 2개를 추가

  • 원소 9개 짜리인 배열들의 배열을 원소 16개짜리로 교체해야 한다.

  • 문제점

    • 원소 수가 너무 적거나 또는 많이 기입하거나, 잘못된 순서로 나열하는 경우 프로그램은 런타임에 문제을 일으킬 것이다.

      (컴파일은 통과)

EnumMap 버전으로 코드를 수정하는 경우

  • 상태 목록에 PLASMA를 추가하고, 전이 목록에 IONIZE(GAS, PLASMA)와 DEIONIZE(PLASMA, GAS)만 추가하면 끝이다.

  • enum 두 개를 사용하여 데이터를 조합하여 사용하는 경우 2차원 배열을 사용하는 것보다 EnumMap을 사용하는 것이 좋다.

    • Collectors.groupingBy와 EnumMap의 조합으로 조회가 편리해지고, 성능 면에서도 이점이 있다.

    • 실제 내부에서는 Map의 Map이 배열의 배열로 구현되어 낭비되는 공간과 시간도 거의 없이 명확하고 안전하고 유지보수에 좋다.

정리

  • enum에 null을 사용하는 경우 NullPointerException 을 발생시켜 문제가 발생한다.

  • 여기 예제는 설명을 위해서 사용되었다.

  • 배열의 인덱스를 얻기 위해 ordinal 을 쓰는 것을 일반적으로 좋지 않으니 EnumMap을 사용해야 한다.

  • 다차원 관계는 EnumMap>으로 표현하는 것이 좋다.

  • Enum.ordinal()을 사용해서는 안된다. 아이템 35 사용하는 것은 일반 원칙의 특수한 사례이다.

Last updated

Was this helpful?