enum 을 확장할 때는 interface 를 사용하라

수박참외메론·2023년 2월 12일
0

타입안전 열거패턴과는 다르게 열거 타입은 확장이 불가능하다.
확장은 보통 기존의 값들에 추가적인 값을 추가하여 다른 목적으로 쓸 때 사용하지만, 열거타입은 이렇게 하지 못한다는 점이 타입안전 열거타입과 다른 점이다.

사실 대부분의 상황에서 열거타입을 확장하는 건 좋은 방법이 아니다. 그 이유는 확장한 타입의 원소는 기반타입의 원소로 취급하지만 그 반대는 그렇지 않고, 이를 한꺼번에 순회할 방법도 마땅치 않고, 마지막으로 확장성을 높이려면 고려해야될 요소가 많으므로 열거타입의 확장 자체가 고려하기 힘든 선택지이다.

열거타입을 확장하는 케이스

하지만 opcode, 즉 연산코드를 구현할 때는 열거타입을 확장하는 것이 어울리긴 한다.
왜냐하면 API 가 제공하는 기본연산 외에도 사용자가 임의적으로 확장해서 연산을 추가할 수 있도록 열어줘야 할 때가 있기 때문이다.

이를 그래서 열거타입 자체를 확장하는 것이 아닌 다른 방법으로 비슷한 효과를 내게끔 할 수 있다. 바로 임의의 인터페이스를 구현하여 해당 interface 를 구현하는 열거타입이 확장되는 개념으로 하게끔 하는 것이다.

public interface Operation {
	double apply(double x, double y);
}
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; }
    },
    TIMES("*") {
    	public double apply(double x, double y) { return x*y; }
    },
    DIVIDE("/") {
        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;
    }
}

열거타입을 확장하는 방법

위에 기본 opcode 들이 api 로 제공이 되었을 때 user specific 하게 기능을 추가하고 싶을때 아래와 같이 코드를 작성할 수 있다.

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 toString(){...}
}

확장된 연산의 사용

이렇게 새로 작성한 연산은 기존 연산을 쓰던 곳이면 어디든 쓸 수 있다. Basic Operation 이 아닌 Operation 인터페이스를 사용하도록 작성되어 있으면 된다.

개별 인스턴스 수준에서 뿐 아니라 타입 수준에서도 기본 열거타입 대신 확장된 열거타입을 넘겨 확장된 열거 타입의 원소 모두를 사용하게 할 수도 있다.
아래 코드는 앞서 설명했었던 테스트 프로그램을 가져와 ExtendedOperation 의 모든 원소를 테스트하도록 수정한 모습이다.

public static void main (String[] args) {
	double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
   	test(ExtendedOperation.class, x, y);
}

public 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));
}
  • main 메서드는 test 메서드에 ExtendedOperation 의 class 리터럴을 넘겨 확장된 연산들이 무엇인지 알려준다.
  • 이 때 opEnumType 매개변수의 선언은 class 객체가 열거타입인 동시에 operation의 하위 타입임을 한정지은 한정적 타입 토큰이다.
  • 이는 열거 타입이어야 원소를 순회할 수 있고, operation 이어야 원소가 뜻하는 연산을 수행할 수 있기 때문이다.
public static void main (String[] args) {
	double x = Double.parseDouble(args[0]);
    double y = Double.parseDouble(args[1]);
   	test(Arrays.asList(ExtendedOperation.values()), x, y);
}

public 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));
}

이 코드는 그나마 덜 복잡하고, test 메서드가 살짝 더 유연해지지만 특정 연산에서는 EnumSet과 EnumMap을 사용하지 못하게 된다.

자바 라이브러리 중에서 java.nio.file.LinkOption 열거 타입은 CopyOptionOpenOption 인터페이스를 구현하여 이 패턴을 사용하여 구현했다.

profile
하루하루는 성실하게 인생전체는 되는대로

0개의 댓글