[번역] Memory-Hogging Enum.values() Method(메모리를 과도하게 사용하는 Enum.values() 메서드)
원본글: https://dzone.com/articles/memory-hogging-enumvalues-method
Java Enum도 주의해야 할 점이 있습니다. 메모리를 잡아먹는 상황과 이를 막는 방법에 대해 이야기 해보겠습니다.
저는 Java Enum의 팬입니다.J2SE 5에서 Enum 도입되기까지 기다림이 있었지만, 출시되었을 때 C와 C++에서 제공하는 것 보다 훨씬 나았기 떄문에 기다릴 만한 가치가 있었습니다. Java의 Enum은 좋은 점이 있지만, 문제가 없는건 아닙니다. 특히 Java에서 values() 메서드는 호출할 때마다 값을 나타내는 배열의 새 복사본을 반환합니다.
Java 언어 사양에는 Enum의 동작에 대한 설명이 나와있습니다. Java의 언어 사양 SE 10 Edition에서 Enum을 다루는 섹션은 8.9입니다. 섹션 8.9.3(Enum Member)에는 두 가지 암시적으로 선언된 메서드인 public static E[] values() 와 public static E valueOf(String name)이 설명되어 있습니다. 예제 8.9.3-1(향상된 for 반복문을 사용해 Enum 상수를 반복하기)에서는 Enum.values()를 호출해 Enum을 반복하는 방법을 보여줍니다. 그러나 문제는 Enum.values()가 배열을 반환하고 Java의 배열은 변경 가능하다는 점입니다. [섹션 10.9 (문제 배열은 문자열이 아닙니다.)은 Java 문자열과 Java 문자 배열의 차이점에 대한 부분을 설명합니다.] Java의 Enum은 불변이므로 열거형과 연관된 배열이 변경되지 않도록 하기 위해 해당 메서드가 호출될 때마다 values() 메서드는 배열의 복제본을 반환하는 것은 합리적인 선택입니다.
최근 OpenJDK 컴파일러 개발자 메일링 리스트의 “Enum.values() 메모리 할당에 관하여”라는 제목의 게시물에 따르면 “Enum.values()는 상수 값 배열을 복제할 때 반복문에서 호출될 때 상당한 양의 메모리를 할당한다.”라고 합니다. 또한, “아마도 불변성을 위한 것일 것”이라고 덧붙이며 “이해할 수 있습니다.”라고 말합니다. 이 메시지 또한 같은 메일링 리스트의 2012년 3월 메시지와 관련 스레드를 참조하고 있습니다.
이 두개의 스레드에서는 이 문제에 대해 현재 사용 가능한 몇가지 흥미로운 해결방법이 포함되어 있습니다.
Brian Goetz의 메시지에는 “이것은 본질적으로 API 설계 버그입니다: values()는 배열을 반환하고 배열은 변경 가능하므로 매번 배열을 복사 해야 합니다.”라는 문장으로 시작됩니다. [Goetz는 이 메시지에서 ‘고정 배열(불변으로 만든 Java 배열)라는 개념도 이야기하고 있습니다.
이 문제는 새로운 문제가 아닙니다. 2009년 12월에 William Shieds’s가 작성한 변경 가능성, 배열 및 Java의 임시 객체 비용이라는 글에서 “이 모든 것의 가장 큰 문제는 Java 배열은 변경 가능하다는 점입니다.”라고 언급하였습니다. Shields는 Java Date 클래스의 오래되고 잘 알려진 가변성 문제를 설명한 후에 Enum.values()의 특정 문제에 대해 설명합니다.
Java Enum에는 해당 Enum의 모든 인스턴스의 배열을 반환하는 values()라는 정적 메서드가 있습니다. Date 클래스의 교훈을 얻은 후, 이 특별한 결정은 충격 그 자체였습니다. List가 훨씬 더 현명한 선택이었을 겁니다. 내부적으로 이것은 인스턴스 배열이 호출될 때마다 방어적으로 복사되어야 함을 의미합니다….
이 문제에 대한 다른 참고 자료는 “Enums.values() 메서드 (Guava thread)와 Java의 Enum.values()의 숨겨진 할당 (“Enum.values()가 반환한 배열 캐싱을 보여줍니다.) 등이 있습니다. 이와 관련된 JDK 버그도 있습니다: JDK-8073381 (”새 배열을 만들지 않고 열거형 값을 가져오기 위한 API 필요”)
이 글에서 설명한 현재 사용 가능한 해결 방법 중 일부는 다음 코드 목록에 설명되어 있으며, 이 코드 목록은 세 가지 형식으로 열거형 값을 캐싱하는 간단한 Fruit Enum 입니다.
세 개의 캐시된 값의 세트가 있는 *Fruit.java* Enum
package dustin.examples.enums;
import java.util.EnumSet;
import java.util.List;
/**
* Fruit enum that demonstrates some currently available
* approaches for caching an enum's values so that a new
* copy of those values does not need to be instantiated
* each time .values() is called.
*/
public enum Fruit
{
APPLE("Apple"),
APRICOT("Apricot"),
BANANA("Banana"),
BLACKBERRY("Blackberry"),
BLUEBERRY("Blueberry"),
BOYSENBERRY("Boysenberry"),
CANTALOUPE("Cantaloupe"),
CHERRY("Cherry"),
CRANBERRY("Cranberry"),
GRAPE("Grape"),
GRAPEFRUIT("Grapefruit"),
GUAVA("Guava"),
HONEYDEW("Honeydew"),
KIWI("Kiwi"),
KUMQUAT("Kumquat"),
LEMON("Lemon"),
LIME("Lime"),
MANGO("Mango"),
ORANGE("Orange"),
PAPAYA("Papaya"),
PEACH("Peach"),
PEAR("Pear"),
PLUM("Plum"),
RASPBERRY("Raspberry"),
STRAWBERRY("Strawberry"),
TANGERINE("Tangerine"),
WATERMELON("Watermelon");
private String fruitName;
Fruit(final String newFruitName)
{
fruitName = newFruitName;
}
/** Cached fruits in immutable list. */
private static final List<Fruit> cachedFruitsList = List.of(Fruit.values());
/** Cached fruits in EnumSet. */
private static final EnumSet<Fruit> cachedFruitsEnumSet = EnumSet.allOf(Fruit.class);
/** Cached fruits in original array form. */
private static final Fruit[] cachedFruits = Fruit.values();
public static List<Fruit> cachedListValues()
{
return cachedFruitsList;
}
public static EnumSet<Fruit> cachedEnumSetValues()
{
return cachedFruitsEnumSet;
}
public static Fruit[] cachedArrayValues()
{
return cachedFruits;
}
}
Enum.values()가 호출 될 때마다 배열을 복사해야 한다는 사실은 실제로 많은 상황에서 큰 문제가 되지 않습니다. 반복문으로 Enum.values()를 반복적으로 호출하는 것이 유용한 경우를 상상하기에는 어렵지 않지만, 메번 Enum 값의 배열을 복사하는 것이 메모리 사용량에 눈에 띄는 영향을 미치기 시작하면 메모리 사용량 증가와 관련된 문제가 발생할 수 있습니다. 열거형 값에 보다 메모리 효율적인 방식으로 엑세스하는 표준 접근 방식이 있으면 좋을 것입니다. 앞서 언급한 두 개의 스레드에서 이 기능을 잠재적으로 구현하기 위해 몇 가지 아이디어를 논의하였습니다.