ordinal() 메서드를 배열 인덱스로 사용하면 발생하는 문제점
아이템 35에서 ordinal 메서드 사용을 자제하라고 권장했음에도 불구하고, 다음 코드처럼 배열이나 리스트에서 원소를 추출할 때 ordinal 메서드를 사용하여 인덱스를 얻는 경우가 있습니다.
코드 부연 설명
- 정원에 심은 식물들을 배열 하나로 관리하고 이들을 생애주기 별로 묶음
- 생애주기별로 총 3개의 집합을 만들고 정원을 한 바퀴 돌며 각 실물을 해당 집합에 넣음
- 집합들을 배열 하나에 넣고 생애주기의 ordinal 값을 그 배열의 인덱스로 사용
위 코드는 정상적으로 컴파일되고 동작은 하지만 문제가 한가득입니다.
- 배열은 제네릭과 호환되지 않으므로 비검사 형변환을 수행해야 하기 때문에 타입 안전성을 보장할 수 없음
- 배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 함
- 혹여나 ordinal()의 반환 값인 정수가 배열의 범위를 벗어날 경우 ArrayIndexOutOfBoundsException 발생 가능
- 잘못된 값을 반환하는데도 불구하고 정상적으로 동작해도 잘못된 동작을 수행하는 것이므로 문제
해결책은 EnumMap
앞선 코드의 배열은 사실상 Enum 타입 상수를 값으로 매핑했으므로, 동일한 기능을 하는 Map을 사용할 수 있습니다.
자바는 열거 타입을 키로 사용하도록 설계된 아주 효율적인 Map 구현체인 EnumMap을 제공하며, 이를 활용하여 앞선 코드를 다음과 같이 변경할 수 있습니다.
배열을 EnumMap으로 변경하면서 얻은 효과는 다음과 같습니다.
- 안전하지 않은 형변환을 쓰지 않았기 때문에 코드가 안전해졌고
- Map의 키인 Enum 타입이 그 자체로 출력용 문자열을 제공하므로 출력 결과에 별도의 포맷팅이 필요 없어짐
- EnumMap 내부에서 ordinal을 사용한 배열을 사용하기 때문에 성능은 동일
- ordinal() 메서드를 사용하는 것은 마찬가지지만 개발자가 직접 사용하지 않고 자바 유틸 라이브러리에서 제공하는 검증된 자료구조를 사용하는 것이기 때문에 type-safe 하면서 성능상의 이점까지 챙김
스트림을 사용한 코드
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을 사용하는 것을 권장합니다.
참고
이펙티브 자바
'JAVA > Effective Java' 카테고리의 다른 글
[아이템 39] 명명 패턴보다 애너테이션을 사용하라 (0) | 2024.02.28 |
---|---|
[아이템 38] 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2024.02.27 |
[아이템 36] 비트 필드 대신 EnumSet을 사용하라 (0) | 2024.02.26 |
[아이템 35] ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2024.02.26 |
[아이템 34] int 상수 대신 열거 타입을 사용하라 (0) | 2024.02.25 |