[이펙티브 자바] Item37 - ordinal 인덱싱 대신 EnumMap을 사용하라

이성훈·2022년 6월 18일
0

이펙티브 자바

목록 보기
17/17
post-thumbnail

<"ordinal 인덱싱 대신 EnumMap을 사용하라">


이번 아이템은 열거 타입의 ordinal 사용에 대해서 말할 것이다.

그런데 사실 이미 앞선 ITEM35에서 열거 타입의 ordinal에 대해서 말하면서 사용하지 않는게 좋다고 말한 바가 있다.

그렇기에 이번 ITEM37은 ITEM35를 상기하면서 보면 되겠다.


혹시 까먹은 사람들을 위해 다시 한번 언급하자.

< Ordinal >

  • 열거 타입에 존재하는 필드
  • 각 상수가 가지고 있으며, 일종의 순서를 의미하는 숫자이다.
  • 선언된 순서대로 0부터 값을 가지며, 선언 순서를 바꾸면 ordinal 필드 값도 달라진다.




#   ordinal을 배열의 인덱스로 사용하는 경우


이번 아이템에서 말하고자 하는 궁극적인건,

열거 타입이 들어간 클래스의 배열을 반복문으로 순회 할 때, ordinal을 인덱싱으로 사용하지 말라는 것이다.


앞에 아이템에서 ordinal 쓰지 말래놓고 새삼... 이라고 생각이 들긴 하지만,
쓰는 사람이 많으니 하는 말이 아닌가 생각이 든다.


그런 사람을 다음의 예시로 보자.

public class Plant {

    enum LifeCycle { ANNUAL, PERNNIAL, BIENNIAL}

    final String name;
    final LifeCycle lifeCycle;

    public Plant(String name, LifeCycle lifeCycle) {
        this.name = name;
        this.lifeCycle = lifeCycle;
    }

    @Override
    public String toString() {
        return name;
    }
}

위 코드는 식물에 대한 정보를 담을 클래스이다.

단순히 식물의 이름과, 생명주기를 담는데 생명주기는 하루살이, 여러해살이, 두해살이로 구별된다.


이제 이 식물들이 담긴 배열에서, 생명주기로 집합을 묶어보려 한다.

생명주기가 열거 타입으로서 3개의 상수 밖에 없으니 집합은 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.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}

내 생각일지는 모르겠지만, 실제로 이렇게 하는 사람이 나는 더 신기해보인다...

ordinal의 개념을 모른다면 코드가 이해가 안갈순 있지만, ordinal이 뭔지만 알면 코드 이해는 쉽다.

대충 내가 봐도 프로그래머가 원하는 바가 무엇이였는지는 알 것 같다.


그런데 위 코드는 다음과 같은 문제점을 가진다.


< ordinal 사용 코드의 문제점 >

  • 배열을 사용하고 있는데, 애초에 배열은 제네릭과 호환되지 않는다. 그렇기에 비검사 형변환이 필요하며, 이는 위험하다.
  • 배열은 각 인덱스의 의미를 알지 못하기 때문에, 출력 과정에서 직접 의미를 달아줄 필요가 생긴다.
  • 정수는 타입 안전하지 않다. (ordinal을 말하고 있는거다.) 그렇기에 프로그래머가 직접 그 안정성에 대해 보증을 해줘야 한다. (Ex. 우리는 확실히 정수만 사용하고 있답니다!!)

이러한 문제점 때문에 orinal을 사용한 배열 인덱싱은 좋지 않고,

그에 대한 대용으로 EnumMap이 존재한다.





#   EnumMap


이제 그럼 위 예제 코드를 EnumMap을 사용하여 다시 쓴 코드를 보자.


public static void usingEnumMap(List<Plant> garden) {

    Map<LifeCycle, Set<Plant>> plantsByLifeCycle 
  							= new EnumMap<>(LifeCycle.class);

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

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

    //EnumMap은 toString을 재정의하였다.
    System.out.println(plantsByLifeCycle);
}

이번에는 ordinal도, 배열도 사용하지 않는다.

Map을 사용하고 있고, LifeCycle마다 HashSet을 만들어 반복문에서 Key가 일치하면 Value를 넣어주는 방식으로 사용이 되고 있다.

코드가 간결해졌으며 가독성도 높아진게 보인다.


구체적으로 장점은 다음과 같다.

< EnumMap 사용 코드의 장점 >

  • 안전하지 않은 형변환 (비검사 형변환)을 사용하지 않는다.
  • EnumMap 자체가 toString을 제공하기에 결과 출력 과정이 단순하다.
  • 정수 인덱싱을 하지 않으니 오류가 날 가능성이 적어진다.
  • EnumMap은 그 내부에서 배열을 사용하기 때문에, Map의 타입 안정성과 배열의 성능을 모두 얻어낸다.

여기서 Stream을 사용하면 코드를 더욱 단순화 하는게 가능해진다.

public static void streamV1(List<Plant> garden) {
    Map plantsByLifeCycle = garden
  					.stream()
  					.collect(Collectors.groupingBy(plant -> plant.lifeCycle));
    System.out.println(plantsByLifeCycle);
}```

코드는 단순해졌지만, 여기서는 EnumMap이 아닌 고유의 Map 구현체를 사용하고 있다.

그렇기에 EnumMap을 쓰면 얻을 수 있는 공간과 성능 이점이 사라진다.


다시 EnumMap을 쓰면 다음과 같다.


public static void streamV2(List<Plant> garden) {
    Map plantsByLifeCycle = garden.stream().collect(Collectors.groupingBy(plant -> plant.lifeCycle,
                    () -> new EnumMap<>(LifeCycle.class),Collectors.toSet()));
    System.out.println(plantsByLifeCycle);
}```




#   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()];
        }
    }
}

코드가 조금 이해하기 어려울 수가 있어 설명을 조금 붙여보자면 이렇다.

위 코드는 물체가 가질 수 있는 3가지 상태 (고체, 액체, 기체)를 담고 있으며,
각 상태로 전이되는 상태변화 조건을 Transition이라는 배열로 가지고 있다.

예컨대, 고체에서 액체 (SOLID -> LIQUID)로 상태변화 하기 위해 필요한 조건은 융해 (MELT)가 된다.


위 코드에서는 ordinal 값을 사용하고 있는데,
이는 상수의 순서 변경이나 상수 추가 등에 의한 위험성을 노출하고 있다.


EnumMap을 사용하여 변경한 코드는 다음과 같다.

enum Phase {

    SOLID, LIQUID, GAS;

    public enum Transition {

        MELT(SOLID, LIQUID),
        FREEZE(LIQUID, SOLID),
        BOIL(LIQUID, GAS),
        CONDENSE(GAS, LIQUID),
        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;
        }

        private static final Map<Phase, Map<Phase, Transition>> transitionMap = Stream.of(values())
                .collect(Collectors.groupingBy(t -> t.from, // 바깥 Map의 Key
                        () -> new EnumMap<>(Phase.class), // 바깥 Map의 구현체
                        Collectors.toMap(t -> t.to, // 바깥 Map의 Value(Map으로), 안쪽 Map의 Key
                                t -> t, // 안쪽 Map의 Value
                                (x,y) -> y, // 만약 Key값이 같은게 있으면 기존것을 사용할지 새로운 것을 사용할지
                                () -> new EnumMap<>(Phase.class)))); // 안쪽 Map의 구현체;

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

}

코드 자체는 훨씬 복잡해지긴 했다.

하지만 이렇게 할 경우 새로운 상태 (상수)가 추가 되더라도 문제가 전혀 없게 된다.

예를 들어서 물체의 상태에 플라즈마 (PLASMA)를 추가하더라도,
그냥 전이 목록에만 추가하면 된다.

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);
   }

   //나머지 코드는 그대로

}

이렇게 함으로써 확장 가능성이 매우 높아지고, 로직적으로 문제가 생길 가능성은 낮아진다.

이는 운영 측면에서 많은 이점을 얻는다.

profile
IT 지식 공간

0개의 댓글