JAVA/Effective Java

[아이템 37] ordinal 인덱싱 대신 EnumMap 사용하라

꾸준함. 2024. 2. 27. 09:00

ordinal() 메서드를 배열 인덱스로 사용하면 발생하는 문제점

아이템 35에서 ordinal 메서드 사용을 자제하라고 권장했음에도 불구하고, 다음 코드처럼 배열이나 리스트에서 원소를 추출할 때 ordinal 메서드를 사용하여 인덱스를 얻는 경우가 있습니다.

 

 

 

코드 부연 설명

  • 정원에 심은 식물들을 배열 하나로 관리하고 이들을 생애주기 별로 묶음
  • 생애주기별로 총 3개의 집합을 만들고 정원을 한 바퀴 돌며 각 실물을 해당 집합에 넣음
  • 집합들을 배열 하나에 넣고 생애주기의 ordinal 값을 그 배열의 인덱스로 사용

 

위 코드는 정상적으로 컴파일되고 동작은 하지만 문제가 한가득입니다.

  • 배열은 제네릭과 호환되지 않으므로 비검사 형변환을 수행해야 하기 때문에 타입 안전성을 보장할 수 없음
  • 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 함
  • 혹여나 ordinal()의 반환 값인 정수가 배열의 범위를 벗어날 경우 ArrayIndexOutOfBoundsException 발생 가능
    • 잘못된 값을 반환하는데도 불구하고 정상적으로 동작해도 잘못된 동작을 수행하는 것이므로 문제

 

해결책은 EnumMap

앞선 코드의 배열은 사실상 Enum 타입 상수를 값으로 매핑했으므로, 동일한 기능을 하는 Map을 사용할 수 있습니다.

자바는 열거 타입을 키로 사용하도록 설계된 아주 효율적인 Map 구현체인 EnumMap을 제공하며, 이를 활용하여 앞선 코드를 다음과 같이 변경할 수 있습니다.

 

 

 

 

배열을 EnumMap으로 변경하면서 얻은 효과는 다음과 같습니다.

  • 안전하지 않은 형변환을 쓰지 않았기 때문에 코드가 안전해졌고
  • Map의 키인 Enum 타입이 그 자체로 출력용 문자열을 제공하므로 출력 결과에 별도의 포맷팅이 필요 없어짐
  • EnumMap 내부에서 ordinal을 사용한 배열을 사용하기 때문에 성능은 동일
  • ordinal() 메서드를 사용하는 것은 마찬가지지만 개발자가 직접 사용하지 않고 자바 유틸 라이브러리에서 제공하는 검증된 자료구조를 사용하는 것이기 때문에 type-safe 하면서 성능상의 이점까지 챙김

 

EnumMap 내부적으로 ordinal() 사용

 

스트림을 사용한 코드

stream을 사용해 맵을 관리하면 코드를 더 줄일 수 있습니다.

 

 

 

 

위와 같이 코드를 작성할 경우 코드는 짧아지지만 EnumMap이 아닌 고유의 맵 구현체를 사용했기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라집니다.

자바 유틸 라이브러리에서 제공하는 Collectors의 groupingBy 메서드는 mapFactory 매개변수에 원하는 맵 구현체를 명시적으로 호출할 수 있어 위 문제점을 해결할 수 있습니다.

 

 

 

스트림 vs EnumMap

 

garden 배열에 ANNUAL 타입 식물이 없고 PERENNIAL, BIENNIAL 식물만 있을 때

  • 스트림은 해당 생애주기에 속하는 식물만 만들기 때문에 Map을 두 개 생성하지만
  • EnumMap은 모든 키를 생성하기 때문에 Map을 세개 만듦

 

위 내용을 정리하는 추가 예제

다음 코드는 두 가지 상태(Phase)를 전이(Transition)와 매핑하도록 구현한 프로그램입니다.

 

 

 

위 코드의 문제점

  • 컴파일러는 ordinal()과 배열 인덱스의 관계를 알 방법이 없음
    • Phase나 Phase.Transition 열거 타입을 수정하면서 TRANSITIONS를 함께 수정하지 않거나 실수로 잘 못 수정할 경우 런타임 오류 발생할 것
    • ArrayIndexOutOfBoundsException 혹은 NullPointerException 던질 수 있고
    • 혹은 정상적으로 동작하는 것처럼 보이지만 오동작할 수 있음

 

  • TRANSITIONS의 크기는 상태의 가짓수의 제곱에 비례해서 커질 것
    • Sparse Matrix를 사용하는 방식으로 해결 가능할 수도?

 

위 코드의 문제에 대한 해결사는 EnumMap이며 EnumMap을 적용하면 코드가 다음과 같이 변합니다.

 

 

EnumMap을 사용하면 from 메서드는 간단해지지만, TRANSITIONS 이차원 배열이 Map<Phase, Map<Phase, Transition>>으로 변경되면서 초기화 과정이 다소 복잡해집니다.

초기화 과정을 간단히 설명하면 다음과 같습니다.

  • groupingBy에서는 전이를 이전 상태를 기준으로 묶고
  • toMap에서는 이후 상태를 전이에 대응시키는 EnumMap 생성
    • 첫 번째 인자는 Map의 key를 설정하는 함수
    • 두 번째 인자는 Map의 value를 설정하는 함수
    • 세 번째 인자는 합병 함수로 선언되어 있지만, EnumMap을 생성하기 위해 필수적으로 필요한 맵 팩토리를 지정한 것일 뿐, 실질적으로는 사용되지 않음
    • 네 번째 인자는 EnumMap으로 내부 Map을 선언

 

 

위 초기화 과정을 거치면 from 메서드를 통해 상태 별 전이를 쉽게 구할 수 있습니다.

 

상태를 추가할 경우

 

ordinal() 메서드를 사용한 코드

  • 새로운 상태를 추가할 때 새로운 상수를 Phase에 1개, Phase.Transition에 2개를 추가한 뒤 원소 9개짜리인 이차원 배열을 원소 16개짜리로 교체 필요
  • 원소 수를 너무 적거나 많이 기입하거나, 잘못된 순서로 나열할 경우 위 프로그램은 런타임 에러를 발생

 

EnumMap을 사용한 코드

  • Phase 목록에 상태 하나 추가 (PLASMA)
  • Phase.Transition에 상태 두 개 추가하면 끝 (IONIZE, DEIONIZE)


 

정리

배열과 인덱스를 얻기 위해 ordinal()를 사용하는 것은 일반적으로 좋지 않으므로 EnumMap을 사용하는 것을 권장합니다.

 

참고

이펙티브 자바

 

반응형