[Effective Java] 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

들어가며

대부분의 상황에서 열거 타입을 확장하는건 좋지 않은 생각이다. 확장한 타입의 원소는 기반 타입의 원소로 취급하지만 그 역은 성립하지 않기 때문이다. 기반 타입과 확장된 타입들의 원소 모두를 순회할 방법도 마땅하지 않다. 마지막으로 확장성을 높이려면 고려할 요소가 늘어나 설계와 구현이 더 복잡해진다.

하지만 확장할 수 있는 열거 타입이 어울리는 쓰임이 최소한 하나는 있다. 연산 코드가 이에 해당한다. 연산 코드의 각 원소는 특정 기계가 수행하는 연산을 뜻한다. 이따끔 API가 제공하는 기본 연산 외에 사용자 확장 연산을 추가할 수 있도록 열어줘야 할 때가 있다. 이럴때 사용할만한 방법이 있다. 기본 아이디어는 열거 타입이 임의의 인터페이스를 구현할 수 있다는 사실을 이용하는 것이다. 연산 코드용 인터페이스를 정의하고 열거 타입이 이 인터페이스를 구현하면 된다. 다음 코드를 보자.

1
2
3
public interface Operation {
double apply(double x, double y);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum BasicOperation implements Operation{
PLUS("+") {
public double apply(double x, double y) {
return x + y;
}
},
MINUS("-") {
public double apply(double x, double y) {
return x - y;
}
};

private final String symbol;

BasicOperation(String symbol) {
this.symbol = symbol;
}

@Override public String toString() {
return symbol;
}
}

확장은 간편하다. Operation 인터페이스를 구현한 열거 타입을 작성하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public enum ExtendedOperation implements Operation{
EXP("^") {
public double apply(double x, double y) {
return Math.pow(x, y);
}
},
REMAINDER("%") {
public double apply(double x, double y) {
return x % y;
}
};

private final String symbol;

ExtendedOperation(String symbol) {
this.symbol = symbol;
}

@Override public String toString() {
return symbol;
}
}

새로 작성한 연산은 기존 연산을 쓰는 Operation 인터페이스를 사용하는 어디든 적용이 가능하다. 개별 인스턴스 수준에서뿐 아니라 타입 수준에서도 기본 열거 타입 대신 확장된 열거 타입을 넘겨 확장된 열거 타입의 원소를 모두 사용하게 할 수도 있다. 방법은 두 가지다. 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
double x = 4.0;
double y = 5.0;
test(ExtendedOperation.class, x, y);
}

private static <T extends Enum<T> & Operation> void test(Class<T> opEnumType, double x, double y) {
for(Operation op : opEnumType.getEnumConstants())
System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
}

<T extends Enum<T> & Operation>는 Class 객체가 enum인 동시에 Operation임을 만족시켜야 한다는 의미다. 다음은 두번째 방법이다.

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
double x = 4.0;
double y = 5.0;
test(Arrays.asList(ExtendedOperation.values()), x, y);
}

private static void test(Collection<? extends Operation> opSet, double x, double y) {
for(Operation op : opSet)
System.out.printf("%f %s %f = %f%n",x,op,y,op.apply(x,y));
}

Collection<? extends Operation>으로 와일드카드 타입을 넘겼다. 코드가 간편해졌지만 특정 연산에서 EnumSet이나 EnumMap을 사용하지 못한다.

인터페이스를 이용해 확장 가능한 열거 타입을 흉내내는 방식에도 단점은 있다. 열거 타입끼리 구현을 상속할 수 없다는 점이다. 디폴트 메소드를 인터페이스에 추가하는 방법도 있지만 위 코드에서는 연산 기호를 저장하고 찾는 로직이 구현체에 모두 들어가야한다. 이럴 때 공유하는 로직이 많다면 helper 클래스나 static helper 메소드로 분리하는 방식을 사용하자.

Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/14/Languages/Effective%20JAVA/item38/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.