본문 바로가기
JAVA/Effective Java

item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라

by Garonguri 2022. 6. 3.
728x90

책에 의하면,

열거 타입은 거의 모든 상황에서 타입 안전 열거 패턴보다 우수하다.

 

그러나 예외 상황이 하나 있다. 확장 가능해야 하는 상황 이다.

 

확장 가능한 상황에서는 타입 안전 열거 패턴이 더 우수하다.

타입 안전 열거 패턴은 열거 타입과 달리 열거한 값들을 그대로 가져온 다음, 값을 더 추가하여 다른 목적으로 쓸 수 있다.

 

열거 타입이 확장하기 힘들게 설계된 이유는 무엇일까?

- 기반 타입과 확장된 타입들의 원소를 모두 순회할 방법이 마땅치 않기 때문이다.

- 확장성을 높이려면 고려할 요소가 늘어나 설계나 구현이 더 복잡해진다.


! 확장된 열거 타입이 필요한 경우 !

- 연산 코드 (opCode)

- 연산 코드의 각 원소는 특정 기계가 수행하는 연산을 뜻한다. API가 제공하는 기본 연산 외에 사용자 확장 연산을 추가해야 할 경우가 존재한다.

 

그럼 이런 경우에는 어떻게 구현해야 할까?

- 연산 코드용 인터페이스를 정의하고, 열거 타입이 이 인터페이스를 구현하게끔 하면 도니다.

- 열거 타입은 그 인터페이스의 표준 구현체 역할을 하게된다.

 

//연산 인터페이스
public interface Operation {
    double apply(double x, double y);
}
//인터페이스를 이용한 확장 가능한 열거 타입 만들기
"BasicOperation은 확장할 수 없지만, 인터페이스인 Operation은 확장 가능하기 때문이다.
이 인터페이스 Operation을 연산 타입으로 사용하면 된다."
public enum BasicOperation implements Operation{
    PLUS("+") {
        @Override
        public double apply(double x, double y) { return x + y; }
    },
    MINUS("-") {
        @Override
        public double apply(double x, double y) { return x - y; }
    },
    TIMES("*") {
        @Override
        public double apply(double x, double y){ return x * y; }
    },
    DIVIDE("/") {
        @Override
        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 "BasicOperation{" +
                "symbol='" + symbol + '\'' +
                '}';
    }
}

이렇게 Operation을 구현한 또 다른 열거 타입을 정의하여, 기본 타입인 BasicOperation을 대체하였다.

더 확장할 수도 있다.

public enum ExtendedOperation implements Operation{
    EXP("^") {
        @Override
        public double apply(double x, double y) {
            return Math.pow(x, y);
        }
    },
    REMAINDER("%") {
        @Override
        public double apply(double x, double y) {
            return x % y;
        }
    };

    private final String symbol;

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

새로 추가한 Extended 연산은, Basic 연산이 쓰이던 곳 어디에서든 쓸 수 있다.

  • 기존에 쓰던 BasicOperation의 enum 타입이 인터페이스인 Operation 으로 선언되어 있기만 하면.
  • 구현체 객체만 BasicOperation 상수에서, ExtendedOperation 상수로 바꿔주면 된다.

타입 수준으로 다형성을 적용할 수 있다.

테스트를 해보자.

- 첫 번째 방법 : Class 리터럴을 넘기는 방법.

public class item38 {
    public static void main(String[] args) {

        double x = 3.8;
        double y = 2.6;

"여기서 클래스 리터럴은 한정적 타입 토큰이다. Class 객체가 열거 타입인 동시에,
Operation의 하위 타입이어야 한다는 의미이다.

열거 타입이어야만 원소를 순회할 수 있고, Operation이어야 원소가 의미하는 연산을 수행할 수 있다.
"
        test(BasicOperation.class, x, y);
        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.println(x + " " + op + " " + y + " = " + op.apply(x,y));
        }
    }
}
/*
3.8 + 2.6 = 6.4
3.8 - 2.6 = 1.1999999999999997
3.8 * 2.6 = 9.879999999999999
3.8 / 2.6 = 1.4615384615384615
3.8 ^ 2.6 = 32.16898448161271
3.8 % 2.6 = 1.1999999999999997
 */

Good


- 두 번째 방법 : 한정적 와일드카드 타입인 Collection<? extends Operation>을 넘기는 방법

public class item38 {
    public static void main(String[] args) {

        double x = 3.8;
        double y = 2.6;

        test(Arrays.asList(BasicOperation.values()), x, y);
        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.println(x + " " + op + " " + y + " = " + op.apply(x,y));
        }
    }
}
/*
3.8 + 2.6 = 6.4
3.8 - 2.6 = 1.1999999999999997
3.8 * 2.6 = 9.879999999999999
3.8 / 2.6 = 1.4615384615384615
3.8 ^ 2.6 = 32.16898448161271
3.8 % 2.6 = 1.1999999999999997
 */

test 메서드는 조금 더 유연해졌지만, 특정 연산에서는 EnumSet과 EnumMap을 사용하지 못한다.

또한 이 방식에는 열거 타입끼리는 구현을 상속할 수 없다는 한 가지 문제점이 있다.

  • 이 경우, 두 열거 타입이 아무 상태에도 의존하지 않는 경우 디폴트 구현을 이용해 인터페이스에 추가하면 된다.
  • 두 열거 타입이 공유하는 기능이 많을 경우 별도의 도우미 클래스나 정적 도우미 메서드를 사용해 코드 중복을 피한다.

열거 타입을 확장한것과 같은 효과를 내기 위하여 자바에서는 다음과같이 구현하고 있다.

java.nio.file.LinkOption 열거 타입은 CopyOption과 OpenOption 인터페이스를 구현했다.

 

핵심 정리

열거 타입은 확장할 수 없다. 이와 같은 효과를 내기 위하여,
인터페이스와 인터페이스를 구현하는 열거 타입을 함께 사용해 자신만의 열거 타입(다른 타입)을 만들 수 있다.

API가 기본 열거 타입을 명시하지 않고 인터페이스 기반으로 작성되었다면 기본 열거 타입의 인스턴스가 쓰이는 모든 곳에서, 새로 확장한 열거 타입의 인스턴스를 대신 사용할 수 있다.
728x90

댓글