본문 바로가기
프로그래밍/자료구조

[자료구조] 코드로 알아보는 java의 EnumMap

by 사바라다 2020. 12. 6.

안녕하세요. 오늘은 코르로 알아보는 java의 자료구조 시간으로 돌아왔습니다. 오늘 여러분들께 소개시켜드리고자 하는 자료구조는 EnumMap입니다. 이름에서 알 수 있듯이 Enum을 Key로 하는 자료구조인데요. HashMap을 평소에 사용하다 Sonar Lint의 정적분석에서 Enum을 Key로 사용한다면 EnumMap을 사용하는 것을 추천하기에 저도 EnumMap의 존재를 알게 되었습니다.

오늘은 제가 EnumMap 자료구조를 사용한 상황과 EnumMap은 HashMap과 어떻게 다른지 알아보는 시간을 가져보도록 하겠습니다.

문제의 상황

아래 소스는 제가 진행하고 있는 프로젝트의 코드 중 일부입니다. Game에 여러 Review를 남길 수 있도록 DB의 구조가 되어있습니다. 여기서 아래 메서드는 Review 데이터를 이용하여 Game의 여러 종류의 평균 평점을 만들어 반환하는 역할을 하고 있습니다. Key 로는 ReviewScoreType 이라는 Enum 값을 이용하고 Value로는 계산된 결과를 저장하고 있습니다.

public ReviewSummary createReviewSummary(long gameId) {

    // Enum을 Key로 하는 EnumMap 선언
  Map<ReviewScoreType, List<Integer>> scoreTypeListMap =
      new EnumMap<>(Map.of(TOTAL, initCountList(), GRAPHIC, initCountList(), SOUND, initCountList(),
          CONTROL, initCountList(), INTENSIVE, initCountList(), ORIGINALITY, initCountList()));

    // game에 대한 Review를 가져와 Enum의 Value에 계산
  reviewRepositoryV1.findAllByGameId(gameId).forEach(review -> {
      scoreTypeListMap.computeIfPresent(TOTAL, (k, v) -> reviewMapCount(review.getTotalScore(), v));
      scoreTypeListMap.computeIfPresent(GRAPHIC, (k, v) -> reviewMapCount(review.getGraphicScore(), v));
      scoreTypeListMap.computeIfPresent(SOUND, (k, v) -> reviewMapCount(review.getSoundScore(), v));
      scoreTypeListMap.computeIfPresent(CONTROL, (k, v) -> reviewMapCount(review.getControlScore(), v));
      scoreTypeListMap.computeIfPresent(INTENSIVE, (k, v) -> reviewMapCount(review.getIntensiveScore(), v));
      scoreTypeListMap.computeIfPresent(ORIGINALITY, (k, v) -> reviewMapCount(review.getOriginalityScore(), v));
  });

    // EnumMap을 통해 결과 값 생성 후 반환
  return ReviewSummary.builder()
      .gameId(gameId)
      .totalScoreSpecific(scoreTypeListMap.get(TOTAL))
      .graphicScoreSpecific(scoreTypeListMap.get(GRAPHIC))
      .soundScoreSpecific(scoreTypeListMap.get(SOUND))
      .controlScoreSpecific(scoreTypeListMap.get(CONTROL))
      .intensiveScoreSpecific(scoreTypeListMap.get(INTENSIVE))
      .originalityScoreSpecific(scoreTypeListMap.get(ORIGINALITY))
      .build();
}

왜 위 메서드에서 저는 HashMap을 그냥 사용하지 않고 EnumMap을 사용했을까요 ? 이제 EnumMap에 대해서 알아보도록 하겠습니다.

EnumMap

EnumMap은 Java에서 Enum을 Map의 Key로 사용하는 자료구조를 말합니다. Java docs에 보면 HashMap은 내부적으로 배열로 나타되어 있다고 합니다. 왜냐하면 EnumMap의 Key는 Enum의 순서인데 이 말인 즉슨 Key 충돌이 일어나지 않기 때문에 이런 구조가 가능한 것입니다. 반면에 HashMap은 제가 이전에 올린 포스팅을 보신 분들은 아시겠지만 Key 충돌이 발생하기 때문에 Node<K,V>[] 의 Hash Table의 형식을 가지고 있었습니다. 이렇기 때문에 EnumMap이 HashMap에 비해서 효율성을 가질 수 있는 것입니다.

아래는 HashMap과 EnumMap을 비교한 표 입니다.

EnumMap과 HashMap의 비교

코드를 통한 확인

EnumMap의 기본적인 이론에 대해 알아보았으니 이제 EnumMap의 구현 코드를 보면서 위의 이론이 정확한 것인지 한번 확인해보도록 하겠습니다.

아래는 EnumMap의 선언부입니다. HashMap과 동일하게 AbstractMap을 상속받고 있다는 사실을 알 수 있습니다. 하지만 HashMap과는 다르게 Map intterface는 implements 받고 있지 않습니다.

public class EnumMap<K extends Enum<K>, V> extends AbstractMap<K, V>
    implements java.io.Serializable, Cloneable

필드를 한번 보도록 하겠습니다. 아래를 보시면 필드가 Key 이며 아래가 Value입니다. Key와 Value 모두 이론에서 이야기 했듯이 단순한 배열로 되어있는것을 알 수 있었습니다.


/**
* The {@code Class} object for the enum type of all the keys of this map.
*
* @serial
*/
private final Class<K> keyType;

// key와 value
/**
* All of the values comprising K.  (Cached for performance.)
*/
private transient K[] keyUniverse;

/**
* Array representation of this map.  The ith element is the value
* to which universe[i] is currently mapped, or null if it isn't
* mapped to anything, or NULL if it's mapped to null.
*/
private transient Object[] vals;

초기화 코드를 보도록 하겠습니다. 기본적인 초기화 코드 및 사용방법은 아래와 같습니다. Class Type을 받아 keyType, key, value에 맞게 세팅해주고 있습니다. key와 value 모두 현재 Enum의 갯수만큼 각 배열의 크기를 선정해줍니다. 그리고 사용방법은 아래에 있습니다.

public EnumMap(Class<K> keyType) {
    this.keyType = keyType;
    keyUniverse = getKeyUniverse(keyType);
    vals = new Object[keyUniverse.length];
}

// 사용
Map<ReviewScoreType, List<Integer>> scoreTypeListMap = new EnumMap<>(ReviewScoreType.class);

초기화와 동시에 기본적인 값을 세팅할 수 있습니다. 그때는 아래의 생성자를 사용할 수 있습니다. Map interface를 사용하는 HashMap과 같은 Class로 초기화 할 수 있습니다. 그리고 더 아래에는 해당 생성자를 사용하는 방법을 나타냈습니다. 사용방법에 제시아래의 코드는 Java 9 부터 나온 Map.of를 사용했기 때문에 Java 8에서는 동작하지 않습니다.

public EnumMap(Map<K, ? extends V> m) {
    if (m instanceof EnumMap) {
        EnumMap<K, ? extends V> em = (EnumMap<K, ? extends V>) m;
        keyType = em.keyType;
        keyUniverse = em.keyUniverse;
        vals = em.vals.clone();
        size = em.size;
    } else {
        if (m.isEmpty())
            throw new IllegalArgumentException("Specified map is empty");
        keyType = m.keySet().iterator().next().getDeclaringClass();
        keyUniverse = getKeyUniverse(keyType);
        vals = new Object[keyUniverse.length];
        putAll(m);
    }
}


// 선언과 동시에 초기화
Map<ReviewScoreType, List<Integer>> scoreTypeListMap =
      new EnumMap<>(Map.of(TOTAL, initCountList(), GRAPHIC, initCountList(), SOUND, initCountList(),
          CONTROL, initCountList(), INTENSIVE, initCountList(), ORIGINALITY, initCountList()));

이제 데이터를 넣고 빼는 메서드를 확인해보도록 하겠습니다. 먼저 아래는 EnumMap에 값을 넣는 put 메서드입니다. key와 value는 이미 size가 고정되어있는 배열이기 때문에 Enum의 index만을 ordinal() 메서드를 통해 가져와서 index의 value 배열에 값을 변경하는 것입니다. 이렇기 때문에 O(1)의 시간복잡도를 항상 만족할 수 있습니다.

각 메서드의 자세한 설명은 주석으로 달아두었습니다.

// put
public V put(K key, V value) {
    typeCheck(key); // 동일한 key Type인지 확인

    int index = key.ordinal(); // enum의 순번 확인
    Object oldValue = vals[index]; // enum의 순번을 key index로 기존의 값 확인
    vals[index] = maskNull(value); // 새로운 값 넣기
    if (oldValue == null) // key에 value가 비어있었다면 size 증가
        size++;
    return unmaskNull(oldValue);
}

아래는 EnumMap에서 key를 이용하여 Value를 가져오는 get 메서드입니다. 찾고자 하는 value는 enum의 ordinal()를 이용한 index에 있기 때문에 항상 O(1)로 찾을 수 있습니다.

// get
public V get(Object key) {
    return (isValidKey(key) ? // 들어온 key가 정상인지 판단
            unmaskNull(vals[((Enum<?>)key).ordinal()]) : null); 
}

private boolean isValidKey(Object key) {
    if (key == null)
        return false;

    // Cheaper than instanceof Enum followed by getDeclaringClass
    Class<?> keyClass = key.getClass();
    return keyClass == keyType || keyClass.getSuperclass() == keyType;
}

private V unmaskNull(Object value) {
    return (V)(value == NULL ? null : value);
}

마무리

오늘은 이렇게해서 EnumMap에 대해서 알아보는 시간을 가져보았습니다.

HashMap을 통해서 동일하게 구현못하는 것은 아니지만 성능 그리고 가독성을 위해서 EnumMap을 적절하게 적용해보는 건 어떨까요 ?

감사합니다.

참조

oracle_EnumMap

tutorialspoint_difference-between-enummap-and-hashmap-in-java

댓글