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

아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라

핵심 요약

enum 상수에 따른 값들을 배열에 저장하고 ordinal() 메서드가 반환하는 정수 값을 인덱스로 사용하려는 유혹이 있을 수 있습니다.

하지만 이는 타입 안전성이 없고 유지보수에 취약한 방식입니다.

대신 enum을 키로 사용하도록 특별히 설계된 EnumMap 을 사용하면 타입 안전성, 명확성, 유지보수성, 그리고 우수한 성능까지 모두 확보할 수 있습니다.


기존 방식: ordinal()을 배열 인덱스로 사용

정의

  • enum 상수가 정의된 순서를 반환하는 ordinal() 메서드

  • ordinal() 결과를 배열의 인덱스로 직접 사용하는 방식

  • 특정 enum 상수에 해당하는 데이터를 배열의 특정 위치에 저장하거나 조회할 때 사용

예시 코드

class Plant {
    // 식물을 생애주기(ANNUAL, PERENNIAL, BIENNIAL)별로 그룹화
    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;
    }
}

Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[LifeCycle.values().length];

for (int i = 0; i < plantsByLifeCycle.length; i++) {
    plantsByLifeCycle[i] = new HashSet<>();
}
// ordinal()을 배열 인덱스로 사용 - 따라 하지 말 것!
for (Plant p : garden) {
    plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}

// 출력 시 인덱스(ordinal)를 다시 LifeCycle 상수로 변환해야 함
for (int i = 0; i < plantsByLifeCycle.length; i++) {
    System.out.printf("%s: %s%n", LifeCycle.values()[i], plantsByLifeCycle[i]);
}

특징

  • enum 상수와 배열 인덱스의 간단한 연결

  • 배열을 사용하므로 특정 위치 빠른 접근

단점

  • 타입 안전성 부재

    • ordinal()int를 반환 -> 배열에는 어떤 정수 값이든 인덱스로 사용 가능

    • 컴파일러는 그 정수가 유효한 ordinal 값인지 보장 X

    • 잘못된 정수 사용 시 런타임 오류 발생 가능성

      • ArrayIndexOutOfBoundsException

  • 유지보수 취약성

    • enum에 상수 추가 or 순서 변경 -> ordinal() 값들이 달라져 코드가 오동작 or 예외 발생

    • 배열 크기를 수동으로 관리

    • enum 변경 시 배열 관련 로직 전체 점검 및 수정해야 할 위험 ↑

    enum LifeCycle { PERENNIAL, ANNUAL, BIENNIAL } // 순서 변경
    enum LifeCycle { ANNUAL, PERENNIAL, BIENNIAL, EVERGREEN } // 요소 추가
    enum LifeCycle { ANNUAL, PERENNIAL } // 요소 제거
  • 가독성 저하

    • 숫자로부터 enum 상수 유추의 어려움

    • 출력이나 로깅 시, ordinal 값을 다시 enum 상수로 변환하는 번거로운 과정 필요

  • 제네릭과의 비호환성

    • 배열은 제네릭과 잘 맞지 않아 비검사 형변환((Set<Plant>[]))과 컴파일 경고 유발

    • 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블 작성


개선된 방식: EnumMap 사용

정의

  • enum 타입을 키로 사용하도록 최적화된 고성능 Map 구현체

  • ordinal() 인덱싱을 대체하는 가장 이상적인 현대적 대안

  • java.util 패키지

예시 코드

import java.util.*;

// EnumMap을 사용해 데이터와 열거 타입을 매핑
Map<LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(LifeCycle.class);


for (LifeCycle lc : LifeCycle.values()) {
    plantsByLifeCycle.put(lc, new HashSet<>());
}

for (Plant p : garden) {
    plantsByLifeCycle.get(p.lifeCycle).add(p);
}

// Map 형태로 바로 출력 가능 (키가 enum 상수이므로 명확함)
System.out.println(plantsByLifeCycle);
// 출력 예시: {ANNUAL=[...], PERENNIAL=[...], BIENNIAL=[...]}

특징

  • 키로 사용할 enumClass 객체(타입 토큰)를 인자로 받아 맵 초기화

  • 내부적으로 배열을 사용하여 데이터를 저장

    • -> ordinal()을 직접 사용하는 것과 비견될 만한 성능 제공

  • Map 인터페이스를 구현하여 기존 컬렉션 프레임워크와 완벽하게 호환

  • 맵의 키인 열거 타입이 그 자체로 출력용 문자열 제공

장점

  • 타입 안전성 보장

    • 키는 명시된 enum 타입만 가능, 값은 해당 enum 상수

    • 컴파일 시점에 타입 오류 검출

    • 런타임 오류 발생 가능성 원천적으로 차단

      • ArrayIndexOutOfBoundsException

  • 유지보수 용이성

    • enum에 상수 추가 및 순서 변경 시 EnumMap 코드 영향없음

    • 배열 크기 관리 필요 X

  • 가독성 향상

    • 훨씬 명확하고 직관적인 코드

    • enum 상수 자체가 키로 사용되므로, 출력 및 디버깅 시 의미를 바로 파악할 수 있습니다.

  • 고성능

    • 내부 구현 최적화 덕분에 일반 HashMap보다 빠름

    • ordinal() 인덱싱 방식과 성능 차이가 거의 없음

  • 안전하고 편리한 사용

    • 복잡하고 오류 가능성이 있는 ordinal() 및 배열 인덱스 계산 로직을 작성할 필요 없음

    • 비검사 형변환이 필요 없어 코드가 깔끔하고 안전

단점

  • 굳이 따지자면 Map 객체 생성 및 메서드 호출 오버헤드가 이론적으로 존재

    • 내부 최적화로 인해 실제 성능 차이는 미미

    • 안전성과 유지보수성 이점이 이를 압도


추가 고려 사항

스트림 활용

  • Stream API의 Collectors.groupingByCollectors.toSetEnumMap 생성자 팩토리와 함께 사용하면 EnumMap을 더 간결하게 생성하고 초기화 가능

import static java.util.stream.Collectors.*;

// 식물 그룹화 예시 (스트림 + EnumMap)

Map<LifeCycle, Set<Plant>> plantsByLifeCycle =
        Arrays.stream(garden).collect(groupingBy(
                        p -> p.lifeCycle,
                        () -> new EnumMap<>(LifeCycle.class),
                        toSet()
        ));

다차원 매핑

public enum Phase {
    SOLID, LIQUID, GAS; //PLASMA

    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT; // IONIZE, DEIONIZE

        // 행은 from의 ordinal을, 열은 to의 ordinal을 인덱스로 쓴다.
        private static final Transition[][] TRANSITIONS = {
            { null, MELT, SUBLIME },
            { FREEZE, null, BOIL },
            { DEPOSIT, CONDENSE, null }
        };

        // 한 상태에서 다른 상태로의 전이를 반환
        public static Transition from (Phase from, Phase to) {
            return TRANSITIONS[from.ordinal()][to.ordinal()];
        }
    }
}
public enum Phase {
    SOLID, LIQUID, GAS, PLASMA;

    public enum Transition {
        MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT, IONIZE, DEIONIZE

        private static final Transition[][] TRANSITIONS = {
            { null, MELT, SUBLIME, null },
            { FREEZE, null, BOIL, null },
            { DEPOSIT, CONDENSE, null, IONIZE },
            { null, null, DEPOSIT, null }
        };
    }
}

  • 두 개의 enum 값에 따라 데이터를 매핑해야 할 때, ordinal()을 이중으로 사용하는 배열은 앞서 언급한 모든 단점을 증폭

  • 컴파일러는 ordinal과 배열 인덱스의 관계를 모른다

    1. Phase나 Phase.Transition 열거 타입을 수정한다면

    2. 상전이 표 TRANSITIONS를 함께 수정하지 않거나 실수로 잘못 수정하면 런타임 오류 발생

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

    • 런타임에 NullPointerException을 일으키는 안 좋은 습관

public enum Phase {
    SOLID, LIQUID, GAS, PLASMA

    public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS), CONDENSE(GAS, LIQUID),
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID);
        IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);

        //...

        // 상전이 맵을 초기화한다.
        private static final Map<Phase, Map<Phase, Transition>> m =
                Stream.of(values()).collect(groupingBy(
                        t -> t.from,
                        () -> new EnumMap<>(Phase.class),
                        toMap(
                                t -> t.to,
                                t -> t,
                                (x, y) -> y,
                                () -> new EnumMap<>(Phase.class)
                        )));

    }
}

  • 이 경우 중첩 EnumMap (EnumMap<Enum1, EnumMap<Enum2, Value>>) 을 사용하는 것이 훨씬 안전하고 유연하며 관리하기 쉬운 해결책.

  • 새로운 상태 추가 시, 상태 목록에 추가하고, 전이 목록에 전이 상태만 추가

  • 나머지는 기존 로직에서 잘 처리해주어 잘못 수정할 가능성이 극히 낮음

  • 실제 내부에서는 맵들의 맵이 배열들의 배열로 구현되어 낭비되는 공간과 시간도 거의 없음

  • 명확하고 안전하고 유지보수하기 좋음


핵심 정리

배열의 인덱스를 얻기 위해 ordinal을 쓰는 것은 일반적으로 좋지 않으니, 대신 EnumMap을 사용하라.

다차원 관계는 EnumMap<..., EnumMap<...>>으로 표현하라

"애플리케이션 프로그래머는 Enum.ordinal을 (웬만해서는) 사용하지 말아야 한다(item35)" 는 일반 원칙의 특수한 사례이다.


Last updated