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

신명철·2022년 3월 6일
0

Effective Java

목록 보기
35/80

Ordinal 을 사용한 인덱싱의 문제점

이따금 배열이나 리스트에서 원소를 꺼낼 때 ordinal 을 사용해 인덱스를 얻는 코드가 있다. 다음 코드를 보자.

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 값을 그 배열의 인덱스로 사용하려 할 것이다.

Set<Plant>[] plantsByLifeCycle =
	(Set<Plant>[]) new Set[Plant.LifeCycle.values().length];
for(int i=0; i < plantsByLifeCycle.length; i++)
	plantsByLifeCycle[i] = new HashSet<>();
for(Plant p : garden)
	plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);

// 결과출력
for(int i=0; i<plantsByLifeCycle.length; i++){
	System.out.println("%s: %s%n",
    	Plants.LifeCycle.values()[i], plantsByLifeCycle[i]);
}

동작은 하지만 문제가 가득하다.

  • 배열은 제네릭과 호환되지 않기 때문에 비검사 형변환을 수행해야 한다.
  • 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
  • 정확한 정숫값(인덱스)을 사용한다는 점을 사용자가 직접 보증해야 한다. 정수는 열거 타입과 달리 타입 안전하지 않기 때문이다.

EnumMap

위 코드에서 배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 일을 한다. 그렇기 때문에 Map을 사용한다면 이를 효과적으로 대체할 수 있을 것이다. 사실 열거 타입을 키로 사용하도록 설계한 아주 빠른 Map 구현체인 EnumMap이 그 주인공이다.

Map<Plant.LifeCycle, Set<Plant>> plantsByLifeCycle =
	new EnumMap<>(Plant.LifeCycle.class);
for(Plant.LifeCycle lc : Plant.LifeCycle.values()) 
	plantsByLifeCycle.put(lc, new HashSet<>());
for(Plant p : garden)
	plantsByLifeCycle.get(p.lifeCycle).add(p);
System.out.println(plantsByLifeCycle.toString());
  • 안전하지 않은 형변환을 쓸 필요가 없다.
  • 맵의 키인 열거 타입이 그 자체로 출력용 문자열을 제공하기 때문에 출력 결과에 레이블을 달 필요가 없다.
  • 배열 인덱스를 계산하는 과정에서 발생할 오류를 원천봉쇄한다.

위 코드에 Stream 을 사용해 Map을 관리하면 코드를 더 줄일 수도 있다.

System.out.println(Arrays.stream(garden)
				.collect(Collectors.groupingBy(p -> p.lifeCycle)));

위 코드에 EnumMap을 이용해 데이터와 열거 타입을 매핑해보자.

System.out.println(Arrays.stream(garden)
				.collect(Collectors.groupingBy(p -> p.lifeCycle,
						() -> new EnumMap<>(LifeCycle.class), Collectors.toSet())));

Stream 을 사용하면 EnumMap 만 사용했을 때와는 조금 다르게 동작한다. EnumMap 버전은 언제나 식물의 생애주기당 하나씩의 중첩 맵을 만들지만, 스트림 버전에서는 해당 생애주기에 속하는 식물이 있을 때만 만든다.

두 열거 타입 값들을 매핑하기 위해서 ordinal 을 쓴 2차원 배열을 사용하는 경우도 있다. 다음 코드를 보자.

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 과 배열 인덱스의 관계를 알 도리가 없고 열거 타입을 수정하면서 표의 관계도 함께 수정해줘야 한다. 만약 열거 타입이 늘어나기라도 한다면 배열은 제곱해서 커지게 된다.

EnumMap을 이용해 수정한 다음 코드를 보자.

public enum Phase {
    SOLID, LIQUID, GAS;

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


        private final Phase from;
        private final Phase to;

        Transition(Phase from, Phase to) {
            this.from = from;
            this.to = to;
        }

        public static final Map<Phase, Map<Phase, Transition>> m = Stream.of(values())
                .collect(groupingBy(t -> t.from, () -> new EnumMap<>(Phase.class),
                        toMap(t -> t.to, //key-mapper
                                t -> t,  //value-mapper
                                (x, y) -> y, //merge-function
                                () -> new EnumMap<>(Phase.class))));

        public static Transition from(Phase from, Phase to) {
            return m.get(from).get(to);
        }
    }
}

첫 번째 수집기에서는 이전 상태를 기준으로 묶었고, 두 번째 수집기인 toMap에서는 이후 상태를 전이에 대응 시키는 EnumMap을 생성했다. 이렇게 구성하면 새로운 상태를 추가하는 것도 간단하다.

public enum Phase {

    SOLID, LIQUID, GAS, PLASMA;

    public enum Transition {
        MELT(SOLID, LIQUID), FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS), CONDENSE(GAS, SOLID),
        SUBLIME(SOLID, GAS), DEPOSIT(GAS, SOLID),
        // 새로운 상태 
        IONIZE(GAS, PLASMA), DEIONIZE(PLASMA, GAS);
        ...
}

결론
배열의 인덱스를 얻기 위해서 ordinal을 쓰지 말고 EnumMap을 사용하자

profile
내 머릿속 지우개

0개의 댓글