티스토리 뷰

Item37은 enum에서 ordinal 사용을 자제하고 컬렉션을 이용하라는 것의 확장판이다

책에서 상전이를 예로 들었는데 OrdinalTransition은 가장 간단하게 생각할 수 있는 코드로 작성됐다

ordinal을 사용해 2차원 배열로 만들었고 예제에서는 상전이가 일어나지 않았을 때 null을 넣어줬지만

살짝 변경해서 NONE을 추가하고 null을 대체했다

 

반면 Transition은 Item37의 조언을 따라 ordinal을 사용하지 않고

코드가 꽤나 복잡해졌지만 enum의 values()를 이용하기 때문에 타입 안전하며

Collectors.groupingBy()와 중첩 맵을 활용 해 상전이를 나타냈다

뭐든 간에 depth가 깊어질수록 이해하기 쉽지 않은데 TRANSITION_MAP은 난해하기 그지없다

이럴 때마다 느끼는 것이 알고리즘의 중요성이 생각보다 작지 않다는 것이다

 

코딩 테스트에 대해서 실용성과 동떨어져 있다는 느낌이 들어 부정적인 생각이 있었다

특히 알고리즘은 1~2달 벼락치기로 실력 향상이 바로 나타나는 게 아니니 그 시간에 다른 걸 공부하는 게 낫지 않나? 싶었다

요즘 생각하기로는 문제 해결을 위해 특정 알고리즘이 필요하다기보다

어떠한 문제든 분할 정복 식으로 생각하면 복잡한 문제도 의외로 간단하게 풀 수 있다고 느낀다

 

public enum Phase {

  SOLID, LIQUID, GAS;

  public enum OrdinalTransition {

    MELT, FREEZE, BOIL, CONDENSE, SUBLIME, DEPOSIT, NONE;

    private static final OrdinalTransition[][] TRANSITIONS = {
      {NONE, MELT, SUBLIME},
      {FREEZE, NONE, BOIL},
      {DEPOSIT, CONDENSE, NONE}
    };

    public static OrdinalTransition from(Phase from, Phase to) {
      return TRANSITIONS[from.ordinal()][to.ordinal()];
    }
  }

  @RequiredArgsConstructor
  public enum Transition {

    MELT(SOLID, LIQUID),
    FREEZE(LIQUID, SOLID),
    BOIL(LIQUID, GAS),
    CONDENSE(GAS, LIQUID),
    SUBLIME(SOLID, GAS),
    DEPOSIT(GAS, SOLID);

    private static final Map<Phase, Map<Phase, Transition>> TRANSITION_MAP =
      Arrays.stream(values())
        .collect(groupingBy(
            t -> t.from,
            () -> new EnumMap<>(Phase.class),
            toMap(
              t -> t.to, t -> t,
              (x, y) -> y, () -> new EnumMap<>(Phase.class))
          )
        );

    private final Phase from;
    private final Phase to;

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

 

 

Collectors.groupingBy API의 javadocs를 보면서 이해해보자

groupingBy는 오버 로드된 메서드고 그중 인자가 세 개인 메서드를 살펴보자

- classifier는 input elements를 키로 매핑하는 function이다

- mapFactory는 우리가 원하는 타입의 맵을 만들어주기 위한 supplier다

- downstream은 요소를 특정 방식으로 소비해 모아주는 수집기다

 

values()로 돌려서 MELT, FREEZE, BOIL.. 등을 스트림으로 만든다

이후 classifier에서 t는 Trasition을 의미한다 (MELT, FREEZE 등등이 Transition의 요소이므로)

MELT(SOLID, LIQUID)을 예로 들면 t.from은 SOLID이고 t.to는 LIQUID다

classifier는 Function<? super Transition, ? extends Phase>이고 Transition -> Phase로 변환하는 것이다

PECS 공식을 상기하면 소비할 땐 <? super 어쩌구>, 생산할 땐 <? extends 저쩌구>니 자연스러운 형태다

따라서 MELT(SOLID, LIQUID)에서 MELT.from으로 Phase 타입의 SOLID를 키로 매핑했다

 

enum을 다룰 땐 일반 맵보다 EnumMap을 사용하는 것이 성능 상 유리하니 

mapFactory에서는 () -> new EnumMap<>(Phase.class);로 명시적으로 EnumMap을 사용해준다

어떤 Map이 만들어질 것이다라고 선언만 하고 실제 모으는 과정은 downstream에서 수행된다

따라서 생성된 Map의 키는 Phase이고 값은 downstream에서 모은 결과로 나타난다

 

downstream에는 수집기라 부르는 Collectors.toList, toSet, toMap 등의 API를 이용한다

여기서는 EnumMap을 만들어야 하니 toMap을 사용했다

 

 

Collectors.toMap() 또한 오버 로드된 메서드고 인자가 4개인 메서드를 사용한다

- keyMapper 키를 지정한다

- valueMapper 값을 지정한다

- mergeFunction 같은 키에 매핑된 값이 2개 이상일 때 사용된다

- mapSupplier 어떤 맵을 만들지 지정한다

 

위 예제에서 mergeFunction은 (x, y) -> y로 선언만 되고 키에 중복으로 매핑된 값이 없어서 사용되지는 않는다

나머지는 간단하게 키를 MELT.to로 Phase 타입으로 매핑했고 값은 Transition 그 자체를 매핑했다

MELT(SOLID, LIQUID)의 경우는 <LIQUID, MELT>로 매핑된 것이다

mapSupplier에서 () -> new EnumMap<>(Phase.class);로 키가 Phase, 값이 Transition인 EnumMap을 만들었다

 

최종적으로 만들어진 EnumMap은 Map<Phase, Map<Phase, Transition>> 형태가 된다

사용은 아래와 같이 간단하게 해 주면 되는데 from의 값으로 매핑된 Map<Phase, Transition>을 꺼내고

거기에서 to의 값으로 매핑된 Transition을 다시 한번 꺼내 준다

즉 SOLID와 매핑된 상전이 맵에서 LIQUID와 매핑된 값을 꺼내는 것이다

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

 

여기까지 읽은 사람이 있다면 진정한 용자다

인자가 3개인 Colllectors.groupingBy API, 인자가 4개인 Collectors.toMap API 가 짬뽕된 메서드도

분할 정복으로 접근하면 그다지 어렵지 않음을 알았을 것이다

내 설명이 이해되지 않더라도 위 과정을 직접 해보고 Javadocs까지 찾아가 보면 금방 이해될 것이다

 

어찌 보면 ordinal()을 사용한 2차원 배열 형태가 직관적으로 이해되고 작성하기도 쉬워 보이는데

뭐하러 이해하기도 어려운 중첩 EnumMap을 만들어 사용해야 하는가?

위 예제는 상전이를 나타낸 것이므로 과학적인 사실을 반영해서 변경 가능성이 거의 없으니 2차원 배열이 나을 수도 있다

enum을 직접 정의해서 애플리케이션에 맞는 형태로 사용할 때는 언제든 변경될 수 있으니 EnumMap을 쓰는 것이 낫다

EnumMap에서 변경이 일어난다면 요소를 추가하거나 Transition에서 매핑된 from, to를 변경해주면 끝나지만

2차원 배열에서는 2차원 배열의 값을 새로 추가하거나 빼줘야 하고, 순서를 바꿔 넣어야 할 수도 있다

 

이 역시 트레이드오프의 문제로 생각된다

간단하게 해결할 수 있는 문제를 중첩 EnumMap으로 작성해 다른 사람의 혼란을 가중시키지 말고 2차원 배열 쓰자

언제나 그렇듯 변경 가능성을 고려하여 EnumMap의 효용이 높겠다 싶을 때 중첩 EnumMap을 사용하자

'Java > Effective Java' 카테고리의 다른 글

[Item39] Lombok 알아보기  (0) 2022.03.22
[Item38] Enum 확장 시켜버리기  (0) 2022.03.21
[Item36] 비트 필드와 EnumSet  (0) 2022.03.19
[Item35] ordinal 금지  (0) 2022.03.15
[Item34] 항상 enum만 사용할 순 없으니  (0) 2022.03.14
댓글
링크
글 보관함
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Total
Today
Yesterday