item 37 : ordinal 인덱싱 대신 EnumMap을 사용하라
1. ordinal() 을 배열 인덱스로 이용한 예제
과거에는 비트 필드와 ordinal() 메서드를 활용해 Enum을 정수 값으로 다루는 방식이 흔했다. 예를 들어, 식물(Plant) 클래스에서 식물을 생애 주기(한해살이, 여러해살이, 두해살이)별로 관리한다고 할 때, ordinal() 메서드로 Enum의 순서를 정수로 받아 배열의 인덱스로 사용하는 방식이 종종 사용되었다.
1) 비트 필드 및 ordinal() 방식의 주요 문제점
ordinal() 방식의 주요 문제점타입 안전성 문제:
ordinal()은 Enum의 순서 값을 반환하지만,정수이기 때문에 잘못된 인덱스를 사용할 가능성이 있다. 이러한 문제로 ArrayIndexOutOfBoundsException이나 NullPointerException이 발생할 수 있다.명확하지 않은 코드:
ordinal()을 통해 얻은 정수를 배열의 인덱스로 사용하는 방식은 코드의 의미가 불분명해져, 유지보수성과 가독성이 떨어진다.형변환 문제: Set 클래스는 제네릭 타입을 받는데, 제네릭 타입은 배열과 호환성이 좋지 않다. 그래서 비검사 형변환을 수행해야 하고, 깔끔히 컴파일되지 않는다.
배열의 크기 관리:
Enum값이 추가될 때마다 배열의 크기와 값이 일치하도록 조정해야 하므로 실수가 발생하기 쉽다.
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; }
}
// 문제 코드: 생애주기별 식물을 배열로 관리하는 코드
@Test
public void plantsByLifeCycleTest() {
Set<Plant>[] plantsByLifeCycle = (Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
List<Plant> garden = new ArrayList<>(List.of(
new Plant("A", Plant.LifeCycle.ANNUAL),
new Plant("B", Plant.LifeCycle.PERENNIAL),
new Plant("C", Plant.LifeCycle.BIENNIAL),
new Plant("D", Plant.LifeCycle.ANNUAL)
));
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant plant : garden) {
plantsByLifeCycle[plant.lifeCycle.ordinal()].add(plant);
}
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
}위의 코드에서 plantsByLifeCycle 배열은 LifeCycle Enum의 ordinal() 값을 인덱스로 사용하여 생애 주기별로 식물을 분류한다. 하지만 이 코드는 비검사 형변환이 필요하고, 잘못된 ordinal() 값을 사용할 경우 예외가 발생할 수 있다.
2. EnumMap을 사용한 대안
2. EnumMap을 사용한 대안위에서의 배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 일을 한다. 그러니 Map을 사용할 수도 있을 것이다.
EnumMap은 열거 타입을 키로 사용하여 데이터를 매핑할 때 최적의 성능을 제공한다.
EnumMap은 내부적으로 배열을 사용하지만,Enum타입을 키로만 사용할 수 있도록 제한하여 안전하고 효율적인 방식으로 구현되었다.
이를 활용하면 비트 필드 방식에서 발생하던 문제들을 해결할 수 있다.
EnumMap을 사용한 코드
@Test
public void plantEnumMapTest() {
List<Plant> garden = new ArrayList<>(List.of(
new Plant("A", Plant.LifeCycle.ANNUAL),
new Plant("B", Plant.LifeCycle.PERENNIAL),
new Plant("C", Plant.LifeCycle.BIENNIAL),
new Plant("D", Plant.LifeCycle.ANNUAL)
));
Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lifeCycle : Plant.LifeCycle.values()) {
plantsByLifeCycle.put(lifeCycle, new HashSet<>());
}
for (Plant plant : garden) {
plantsByLifeCycle.get(plant.lifeCycle).add(plant);
}
System.out.println("plantsByLifeCycle = " + plantsByLifeCycle);
}타입 안전성:
EnumMap은Enum타입만을 키로 허용하므로 잘못된 인덱스 사용 문제를 방지할 수 있다.명확한 코드: 배열의 인덱스를 사용하지 않으므로 의미가 명확하며, 유지보수하기 쉽다.
제네릭 타입:
EnumMap은 제네릭을 지원하므로 컴파일 경고 없이 사용할 수 있다.효율성: 내부적으로 배열을 사용해 빠른 성능을 제공한다.
3. 스트림과 EnumMap을 함께 사용하는 방식
EnumMap을 함께 사용하는 방식EnumMap을 사용할 때 스트림을 함께 사용하면 코드가 더욱 간결진진다. 특히 Collectors.groupingBy 메서드를 사용하면 각 항목을 EnumMap에 그룹화할 수 있다.
1) 스트림과 EnumMap을 사용한 코드 예제
EnumMap을 사용한 코드 예제 // EnumMap<LifeCycle, Set<Plant>>을 생성하여 생애 주기별로 식물을 그룹화
EnumMap<Plant.LifeCycle, Set<Plant>> lifeCycleSetEnumMap =
garden.stream() // garden 리스트의 요소들을 스트림으로 변환
.collect(
groupingBy(
p -> p.lifeCycle, // 각 Plant 객체의 lifeCycle 속성을 기준으로 그룹화
() -> new EnumMap<>(Plant.LifeCycle.class), // EnumMap을 사용하여 그룹화된 결과를 저장
toSet() // lifeCycle이 같은 Plant 객체들을 Set으로 수집
)
);
// 생애 주기별로 그룹화된 결과 출력
System.out.println("lifeCycleSetEnumMap = " + lifeCycleSetEnumMap);garden.stream():
garden리스트를 스트림으로 변환하여 각Plant객체를 순회할 수 있도록 한다.
collect(groupingBy(...)):
Collectors.groupingBy메서드를 사용하여Plant객체를lifeCycle속성값 기준으로 그룹화한다.
p -> p.lifeCycle:
각
Plant객체의lifeCycle속성을 기준으로 그룹화한다.lifeCycle값이 동일한Plant객체들이 한 그룹으로 모인다.
() -> new EnumMap<>(Plant.LifeCycle.class):
결과를 저장할 맵의 구현체로
EnumMap을 사용한다.EnumMap은LifeCycleEnum을 키로 가지는 맵을 생성하여, 그룹화된 데이터를EnumMap에 저장하도록 한한다.
toSet():
같은
lifeCycle을 가진Plant객체들을Set으로 수집하여EnumMap의 값으로 저장한한다.
System.out.println(...):
lifeCycleSetEnumMap을 출력하여, 각LifeCycleEnum 값에 해당하는Plant객체들의 그룹을 확인
이 코드는
EnumMap을 사용하여Plant객체들을 생애 주기(LifeCycle)에 따라 그룹화하므로, 타입 안전성과 효율성을 모두 갖춘 코드이다.스트림과
EnumMap을 조합하여 작성한 코드는 더 짧고 가독성이 뛰어나며, EnumMap이 제공하는 성능 이점을 유지할 수 있다.
4. 상태 전이 관리에 중첩된 EnumMap 사용하기
EnumMap 사용하기
상태 전이(Transition)를 Enum으로 관리할 때, 열거형 Enum 간의 전이 상태를 중첩된EnumMap을 통해 관리할 수 있다.
1) ordinal()을 이용한 2차원 배열 인덱스 예제
ordinal()을 이용한 2차원 배열 인덱스 예제 enum Phase {
SOLID, LIQUID, GAS;
enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
// 2차원 배열을 사용하여 전이 관계를 정의
private static final Transition[][] TRANSITIONS = {
{null, MELT, SUBLIME}, // SOLID
{FREEZE, null, BOIL}, // LIQUID
{DEPOSIT, CONDENSE, null} // GAS
};
// Phase 상태 전이 메서드
public static Transition from(Phase from, Phase to) {
return TRANSITIONS[from.ordinal()][to.ordinal()];
}
}
}Phase는SOLID,LIQUID,GAS라는 세 가지 상태를 정의한 열거형각 상태 전이에 해당하는
Transition값들을2차원 배열 TRANSITIONS로 정의하고,from과to의ordinal()값을 사용해 배열 인덱스로 전이 상태를 가져온다.예를 들어,
Phase.SOLID에서Phase.LIQUID로 전이 시MELT상태가 반환된된다.
문제점
타입 안전성:
ordinal()은 정수 값을 반환하기 때문에 인덱스 오류 가능성이 있다.유지보수성: 배열의 인덱스와
ordinal()의 관계를 컴파일러가 인식하지 못하므로,Phase나Transition수정 시TRANSITIONS배열을 함께 수정해야 한다.비효율성: 테이블이 커질수록
null로 채워지는 공간이 증가하여 메모리 낭비가 발생할 수 있다.
2) EnumMap을 사용한 개선 예제
2차원 배열 대신 EnumMap을 사용하여 이러한 문제를 해결할 수 있다.
2중 중첩
import java.util.*;
import java.util.stream.*;
import static java.util.stream.Collectors.*;
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 final Phase from;
private final Phase to;
Transition(Phase from, Phase to) {
this.from = from;
this.to = to;
}
// EnumMap을 중첩하여 두 상태 간의 전이를 매핑
private final static Map<Phase, Map<Phase, Transition>> map =
Stream.of(values()).collect(
groupingBy(
t -> t.from, // 'from' Phase를 기준으로 그룹화
() -> new EnumMap<>(Phase.class), // 외부 EnumMap 생성
toMap(t -> t.to, t -> t,
(x, y) -> y,
() -> new EnumMap<>(Phase.class)) // 내부 EnumMap 생성
)
);
public static Transition from(Phase from, Phase to) {
return map.get(from).get(to);
}
}
}코드 설명
TransitionEnum 정의:각 전이의 시작 상태 (
from)와 끝 상태 (to)를 나타냅니다. 예를 들어,MELT는SOLID에서LIQUID로의 전이를 의미한다.
중첩
EnumMap사용:EnumMap을 중첩하여from상태와to상태 간의 전이를 매핑합Stream.of(values())와Collectors.groupingBy를 사용하여from상태를 기준으로 그룹화하고,toMap으로to상태와Transition을 매핑하여 중첩EnumMap을 생성한다.(x, y) -> y는 중복 키가 없기 때문에 병합 함수로 단순히 기존 값 반환을 설정한 것이다.
전이 상태 가져오기:
from(Phase from, Phase to)메서드는from과to상태 간의 전이를 반환한다. 예를 들어,Phase.from(SOLID, LIQUID)는MELT를 반환한다.
개선된 코드의 장점
타입 안전성:
ordinal()대신 Enum을 직접 사용하므로 타입 안전성을 확보할 수 있다.유지보수성:
Phase나Transition이 변경되더라도EnumMap의 키로 관리되므로 코드 변경 범위가 줄어든다.효율성:
EnumMap은 내부적으로 배열을 사용하여 빠르면서도 메모리를 절약할 수 있다.(x, y) -> y 부분은 원래 중복된 키에 값이 들어왔을 때 어떻게 합칠까를 관여하는 부분인데, 여기서는 중복된 키가 없으므로 쓰이지 않고 있다.
하드 코딩으로 어려움을 겪기에 EnumMap을 씀
public enum Phase {
SOLID, LIQUID, GAS;
public enum Transition {
MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT;
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()];
}
}
}핵심 정리
비트 필드나
ordinal()을 사용하는 배열 기반 방식은 오류 발생 가능성이 높고, 코드의 가독성과 안전성이 떨어진다. 즉, 배열의 인덱스를 위해ordinal()을 쓰는 것은 일반적으로 좋지 않다.대신
EnumMap을 사용하자.EnumMap은 열거 타입을 키로 사용하여 데이터를 관리하는 최적의 방식으로, 명확하고 성능이 우수하다.다차원 관계는
EnumMap<..., EnumMap<...>>으로 표기하자.ordinal()은 웬만해선 쓰지 말자.
출처
Last updated