37. ordinal 인덱싱 대신 EnumMap을 사용하라
아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라
핵심 요약
enum
상수에 따른 값들을 배열에 저장하고 ordinal()
메서드가 반환하는 정수 값을 인덱스로 사용하려는 유혹이 있을 수 있습니다.
하지만 이는 타입 안전성이 없고 유지보수에 취약한 방식입니다.
대신 enum
을 키로 사용하도록 특별히 설계된 EnumMap
을 사용하면 타입 안전성, 명확성, 유지보수성, 그리고 우수한 성능까지 모두 확보할 수 있습니다.
기존 방식: ordinal()
을 배열 인덱스로 사용
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
사용
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=[...]}
특징
키로 사용할
enum
의Class
객체(타입 토큰)를 인자로 받아 맵 초기화내부적으로 배열을 사용하여 데이터를 저장
->
ordinal()
을 직접 사용하는 것과 비견될 만한 성능 제공
Map
인터페이스를 구현하여 기존 컬렉션 프레임워크와 완벽하게 호환맵의 키인 열거 타입이 그 자체로 출력용 문자열 제공
장점
타입 안전성 보장
키는 명시된
enum
타입만 가능, 값은 해당enum
상수컴파일 시점에 타입 오류 검출
런타임 오류 발생 가능성 원천적으로 차단
ArrayIndexOutOfBoundsException
유지보수 용이성
enum
에 상수 추가 및 순서 변경 시EnumMap
코드 영향없음배열 크기 관리 필요 X
가독성 향상
훨씬 명확하고 직관적인 코드
enum
상수 자체가 키로 사용되므로, 출력 및 디버깅 시 의미를 바로 파악할 수 있습니다.
고성능
내부 구현 최적화 덕분에 일반
HashMap
보다 빠름ordinal()
인덱싱 방식과 성능 차이가 거의 없음
안전하고 편리한 사용
복잡하고 오류 가능성이 있는
ordinal()
및 배열 인덱스 계산 로직을 작성할 필요 없음비검사 형변환이 필요 없어 코드가 깔끔하고 안전
단점
굳이 따지자면
Map
객체 생성 및 메서드 호출 오버헤드가 이론적으로 존재내부 최적화로 인해 실제 성능 차이는 미미
안전성과 유지보수성 이점이 이를 압도
추가 고려 사항
스트림 활용
Stream
API의Collectors.groupingBy
와Collectors.toSet
을EnumMap
생성자 팩토리와 함께 사용하면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과 배열 인덱스의 관계를 모른다
Phase나 Phase.Transition 열거 타입을 수정한다면
상전이 표 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