/**
* 식물을 나타내는 클래스
*/
public class Plant {
// 식물의 생애 주기를 관리하는 열거 타입
enum LifeCycle {
ANNUAL, // 한해살이
PERENNIAL, // 여러해살이
BIENNIAL // 두해살이
}
final String name;
final LifeCycle lifeCycle;
Plant(String name, LifeCycle lifeCycle) {
this.name = name;
this.lifeCycle = lifeCycle;
}
@Override
public String toString() {
return name;
}
}
정원에 심은 식물들을 배열 하나로 관리하고, 이들을 생애주기(한해살이, 여러해살이, 두해살이)별로 묶는다.
생애주기 별로 총 3개의 집합을 만들고 정원을 한 바퀴 돌며 각 식물을 해당 집합에 넣는다.
이때, 집합들을 배열 하나에 넣고, 생애주기의 ordinal 값을 그 배열의 인덱스로 사용할 수 있다.
class Client {
public void addPlant(List<Plant> garden) {
// 생애주기 3가지로 만들어지는 Set
Set<Plant>[] plantsByLifeCycle = new Set[Plant.LifeCycle.values().length];
for (int i = 0; i < plantsByLifeCycle.length; i++) {
plantsByLifeCycle[i] = new HashSet<>();
}
for (Plant p : garden) {
plantsByLifeCycle[p.lifeCycle.ordinal()].add(p);
}
// 인덱스의 의미를 알 수 없어 직접 레이블을 달아 데이터 확인 작업 필요
for (int i = 0; i < plantsByLifeCycle.length; i++) {
System.out.printf("%s: %s%n", Plant.LifeCycle.values()[i], plantsByLifeCycle[i]);
}
}
}
정원의 식물 입력 테스트
class ClientTest {
private List<Plant> garden;
@BeforeEach
void setUp() {
garden = Arrays.asList(
new Plant("ANNUAL_TREE_1", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_2", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_3", Plant.LifeCycle.ANNUAL),
new Plant("BIENNIAL_TREE_1", Plant.LifeCycle.BIENNIAL),
new Plant("PERENNIAL_TREE_1", Plant.LifeCycle.PERENNIAL)
);
}
// ANNUAL: [ANNUAL_TREE_3, ANNUAL_TREE_2, ANNUAL_TREE_1]
// PERENNIAL: [PERENNIAL_TREE_1]
// BIENNIAL: [BIENNIAL_TREE_1]
@DisplayName("정원에 있는 식물 등록하기")
@Test
void testCase1() {
// given
Client client = new Client();
// then
client.addPlant(garden);
}
}
위 코드에 문제가 한가득이다.
배열은 각 인덱스의 의미를 모르니 출력 결과에 직접 레이블을 달아야 한다.
가장 심각한 문제는 인수 값으로 정확한 정숫값을 사용하고 있다는 것을 개발자가 직접 보장해야 한다.
정수는 열거 타입과 달리 타입 안전하지 않기 때문이다.
잘못된 값을 사용하는 경우 배열의 인덱스에 따른 값이 의도한 값을 보장할 수 없고, ArrayIndexOutOfBoundsException을 발생할 수 있다.
해결책
배열은 실질적으로 열거 타입 상수를 값으로 매핑하는 역할을 한다.
이는 Map을 사용할 수 있도록 한다.
열거 타입을 키로 사용하도록 설계 한 아주 빠른 Map의 구현체(EnumMap) 가 존재한다.
EnumMap을 사용 예시
Enum 타입을 Key 값으로 하는 EnumMap 구현체
class Client {
public void addPlantTypeEnumMap(List<Plant> garden) {
Map<Plant.LifeCycle, Set<Plant>> plantByLifeCycle = new EnumMap<>(Plant.LifeCycle.class);
for (Plant.LifeCycle lc : Plant.LifeCycle.values()) {
plantByLifeCycle.put(lc, new HashSet<>());
}
for (Plant p : garden) {
plantByLifeCycle.get(p.lifeCycle).add(p);
}
System.out.println(plantByLifeCycle);
}
}
EnumMap 을 사용하여 구현된 테스트
class ClientTest {
private List<Plant> garden;
@BeforeEach
void setUp() {
garden = Arrays.asList(
new Plant("ANNUAL_TREE_1", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_2", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_3", Plant.LifeCycle.ANNUAL),
new Plant("BIENNIAL_TREE_1", Plant.LifeCycle.BIENNIAL),
new Plant("PERENNIAL_TREE_1", Plant.LifeCycle.PERENNIAL)
);
}
// {ANNUAL=[ANNUAL_TREE_3, ANNUAL_TREE_2, ANNUAL_TREE_1], PERENNIAL=[PERENNIAL_TREE_1], BIENNIAL=[BIENNIAL_TREE_1]}
@DisplayName("EmumMap을 이용해 정원의 식물 등록하기")
@Test
void testCase2() {
Client client = new Client();
// then
client.addPlantTypeEnumMap(garden);
}
}
Set[] 을 사용하여 관리하는 것보다 EnumMap을 사용하는 것이 간결하고 성능도 비슷하다.
안전하지 않은 형변환을 쓰지 않고, Map의 Key인 열거 타입이 그 자체로 출력용 문자열을 제공하기 때문에 출력 결과에 레이블을 달 일도 없다.
배열 인덱스를 계산하는 과정에서 오류가 날 가능성도 배제할 수 있다.
EnumMap의 성능이 ordinal을 쓴 배열에 비견되는 이유는 그 내부에서 배열을 사용하기 때문이다.
내부 구현 방식을 안으로 숨겨서 Map의 타입 안전성과 배열의 성능을 모두 얻어낸 것이다.
Prototype 의 스트림 기반 코드
class ClientTest {
private List<Plant> garden;
@BeforeEach
void setUp() {
garden = Arrays.asList(
new Plant("ANNUAL_TREE_1", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_2", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_3", Plant.LifeCycle.ANNUAL),
new Plant("BIENNIAL_TREE_1", Plant.LifeCycle.BIENNIAL),
new Plant("PERENNIAL_TREE_1", Plant.LifeCycle.PERENNIAL)
);
}
// {ANNUAL=[ANNUAL_TREE_1, ANNUAL_TREE_2, ANNUAL_TREE_3], BIENNIAL=[BIENNIAL_TREE_1], PERENNIAL=[PERENNIAL_TREE_1]}
@DisplayName("Stream 기반으로 정원의 식물을 생애주기 별로 나열하기")
@Test
void testCase3() {
Map<Plant.LifeCycle, List<Plant>> garden =
this.garden.stream()
// ANNUAL -> BIENNIAL -> PERENNIAL 순으로 확인하기 위한 설정
.sorted((o1, o2) -> o2.lifeCycle.ordinal() - o1.lifeCycle.ordinal())
.collect(groupingBy(p -> p.lifeCycle));
System.out.println(garden);
}
}
이 코드는 EnumMap이 아닌 고유한 맵 구현체를 사용하기 때문에 EnumMap을 써서 얻은 공간과 성능 이점이 사라진다는 문제가 발생한다.
위 문제점을 최적화 하기
매개변수 3개 짜리 Collections.groupingBy 메서드는 mapFactory 매개변수에 원하는 맵 구현체를 명시해 호출할 수 있다.
아래 코드는 Map을 빈번하게 사용하는 프로그램에서 최적화하는 방법이다.
class ClientTest {
private List<Plant> garden;
@BeforeEach
void setUp() {
garden = Arrays.asList(
new Plant("ANNUAL_TREE_1", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_2", Plant.LifeCycle.ANNUAL),
new Plant("ANNUAL_TREE_3", Plant.LifeCycle.ANNUAL),
new Plant("BIENNIAL_TREE_1", Plant.LifeCycle.BIENNIAL),
new Plant("PERENNIAL_TREE_1", Plant.LifeCycle.PERENNIAL)
);
}
// {ANNUAL=[ANNUAL_TREE_2, ANNUAL_TREE_1, ANNUAL_TREE_3], PERENNIAL=[PERENNIAL_TREE_1], BIENNIAL=[BIENNIAL_TREE_1]}
@DisplayName("EnumMap을 이용해 데이터와 열거 타입을 매핑하는 테스트")
@Test
void testCase4() {
EnumMap<Plant.LifeCycle, Set<Plant>> garden = this.garden.stream()
.collect(groupingBy(
p -> p.lifeCycle, // Function<? super T, ? extends K> classifier
() -> new EnumMap<>(Plant.LifeCycle.class), // Supplier<M> mapFactory,
toSet() // Collector<? super T, A, D> downstream
)
);
System.out.println(garden);
}
}