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

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

핵심 요약

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

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

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


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

정의

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

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

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

예시 코드

특징

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

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

단점

  • 타입 안전성 부재

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

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

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

      • ArrayIndexOutOfBoundsException

  • 유지보수 취약성

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

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

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

  • 가독성 저하

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

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

  • 제네릭과의 비호환성

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

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


개선된 방식: EnumMap 사용

정의

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

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

  • java.util 패키지

예시 코드

특징

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

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

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

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

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

장점

  • 타입 안전성 보장

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

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

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

      • ArrayIndexOutOfBoundsException

  • 유지보수 용이성

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

    • 배열 크기 관리 필요 X

  • 가독성 향상

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

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

  • 고성능

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

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

  • 안전하고 편리한 사용

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

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

단점

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

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

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


추가 고려 사항

스트림 활용

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

다차원 매핑

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

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

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

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

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

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

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

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

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

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

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


핵심 정리

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

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

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


Last updated