Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Item 38&39. 열거 타입의 확장과 애너테이션 #23

Open
ChoiSeEun opened this issue Dec 20, 2023 · 0 comments
Open

Item 38&39. 열거 타입의 확장과 애너테이션 #23

ChoiSeEun opened this issue Dec 20, 2023 · 0 comments
Assignees
Labels

Comments

@ChoiSeEun
Copy link
Contributor

section: 6장

  • 아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
  • 아이템 39. 명명 패턴보다 애너테이션을 사용하라

🍵 서론

다민이를 위한 이번 이슈 3줄 요약.

  1. 열거 타입 자체는 확장할 수 없다.
  2. 확장하고 싶다면 인터페이스를 사용해라.
  3. 명명패턴 보다는 애너테이션을 사용해라.

🌒 본론

1️⃣ 열거 타입 자체는 확장할 수 없다.

이미 누군가가 열거 타입이 뭔지, 장점이 뭐가 있는지 설명을 해줬을 것이다. 하지만, 열거 타입은 확장을 할 수 없다는 단점이 있다. 사실 단점도 아닌게, 열거 타입을 확장하는 건 좋지 않은 생각이다.

그 이유는,

  1. 확장 타입 원소는 기반 원소로 취급되지만 반대의 경우는 성립하지 않는다.
  2. 기반 타입과 확장 타입 원소를 모두 순회할 방법이 없다.
  3. 확장성을 높이려면 고려해야 할 요소가 늘어난다.

그래서 대부분의 경우에는 열거 타입의 확장이 필요하지 않으나, 교재에서도 제시한 연산 코드 는 확장이 어울리는 열거 타입이다. 기본으로 제공하는 연산 외에 사용자가 연산을 추가하고 싶다면 확장이 필요하다. 그렇다면 어떻게 확장할 수 있을까?

아래 코드는 타입 안전 열거 패턴이다. 타입 안전 열거 패턴은 모든 면에서 열거 타입보다 좋지 않지만, 확장이 가능하다는 점 하나가 우수하다.

public class TypesafeOperation { 
	private final String type; 
	
	private TypesafeOperation(String type) { 
		this.type = type; 
	} 
	public String toString() { 
		return type; 
	} 
	
	public static final TypesafeOperation PLUS = new TypesafeOperation("+"); 
	public static final TypesafeOperation MINUS = new TypesafeOperation("-"); 
	public static final TypesafeOperation TIMES = new TypesafeOperation("*"); 
	public static final TypesafeOperation DIVIDE = new TypesafeOperation("/"); 
	}

그렇다면, 확장이 필요한 연산 코드 같은 경우 모든 단점을 감수하고 타입 안전 열거 패턴 을 사용해야 할까? 답은 아니다. 다행히도 우리에게 익숙한 인터페이스 를 사용해서 확장의 효과를 낼 수 있다.

2️⃣ 확장하고 싶다면 인터페이스를 사용해라.

연산 코드용 인터페이스를 정의하고, 열거 타입이 해당 인터페이스를 구현하도록 한다.

// 인터페이스
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;
	}
}

여기서 확장이 필요하다면 ? 인터페이스를 구현하는 또 다른 열거 타입을 만들면 된다!

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

	private final String symbol;

	ExtendedOperation(String symbol){
		this.symbol = symbol;
	}
	@Override public String toString(){
		return symbol;
	}
}

기존에 BasicOperation 이 아닌 Operation 을 사용하도록 잘 작성이 되어 있었다면, 아무런 변경 없이 확장한 열거 타입을 사용할 수 있다. 또한, 해당 방식처럼 인터페이스를 사용한다면 별도의 추상 메서드를 정의할 필요가 없다는 점도 장점이다.
image

앞서, 열거 타입에서 확장을 고려하는 건 좋지 못한 생각이라고 한 이유 중 하나가 "기반 타입과 확장 타입 원소를 모두 순회할 방법이 없다"는 점이었다. 인터페이스를 사용하면 이 점도 해결할 수 있다.

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

// 열거 타입인 동시에 Operation의 하위 타입이어야 한다.
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));
}
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);
}

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

마지막으로, 그럼 이 방법은 모든 면에서 완벽할까? 그건 또 아니다. 열거 타입끼리는 구현을 상속할 수 없기 때문에 발생하는 (사소한) 문제가 존재한다. 이런 경우에는 아래와 같은 방법으로 해결할 수 있다.

  1. 아무런 상태도 의존하지 않는 경우, 디폴트 구현으로 인터페이스에 추가한다.
  2. 공유 기능이 많다면, 별도의 도우미 클래스나 정적 도우미 메서드를 분리한다.

3️⃣ 명명패턴 보다는 애너테이션을 사용해라.

💡 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.

명명 패턴은, 변수나 함수의 이름을 일관된 방식으로 작성하는 패턴 을 말한다. 교재에서는 명명 패턴의 3가지 단점을 제시하며 명명패턴보다 애너테이션을 사용하는 것이 좋다고 말한다.

  1. 오타가 나면 안된다.
  2. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
  3. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.

오타가 난 메서드를 테스트 하지 않고 지나쳐서 성공했다고 오해하거나(1), 메서드 이름이 아닌 클래스 이름을 전달해서 테스트가 수행되지 않거나(2), 특정 예외를 던져야 성공하는 테스트에서 원하는 예외를 전달해줄 방법이 없는(3) 등의 문제 상황이 발생할 수 있다.
그리고 이런 문제들은 애너테이션 으로 해결할 수 있다.

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test{

}

교재에 있는 가장 기본적인 마커 애너테이션 @Test 를 정의한 모습이다. 이 안에 @Retention@Target 은 메타 애너테이션이다.

마커 애너테이션 : 아무 매개변수 없이 단순히 대상에 마킹하는 애너테이션
메타 애너테이션 : 애너테이션 선언에 다는 애너테이션

두 가지 메타 애너테이션에 대해 먼저 살펴보면,
@Retention 은 애너테이션이 실제로 적용되고 유지되는 범위를 지정할 수 있으며, 가능한 값은 다음과 같다.

설명
RetentionPolicy.RUNTIME 컴파일 이후에도 JVM에 의해 계속 참조가 가능. 주로 리플렉션이나 로깅에 사용
RetentionPolicy.CLASS 컴파일러가 클래스를 참조할 때까지 유효
RetentionPolicy.SOURCE 컴파일 전까지 유효. 컴파일 이후에는 사라짐

@Target 은 애너테이션이 어디에 적용될지를 결정하며, 가능한 값은 다음과 같다.

설명
ElementType.PACKAGE 패키지 선언
ElementType.TYPE 타입 선언
ElementType.ANNOTATION_TYPE 애너테이션 선언
ElementType.CONSTRUCTOR 생성자 선언
ElementType.FIELD 필드 선언
ElementType.LOCAL_VARIABLE 지역 변수 선언
ElementType.METHOD 메서드 선언
ElementType.PARAMETER 매개변수 선언
ElementType.TYPE_PARAMETER 매개변수 타입 선언
ElementType.TYPE_USE 타입이 사용되는 곳에 선언

추가로, 필요한 매개변수 타입을 애너테이션 타입에 지정해줄 수도 있다.

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
	Class<? extends Throwable> value(); // 모든 예외를 수용
}

이 경우에는, 애너테이션을 활용할 때 필요한 예외를 지정해주면 된다. 지정한 예외가 발생하지 않거나 다른 예외가 발생하는 경우에는 테스트가 실패하고, 지정한 예외가 발생하는 경우에만 테스트가 성공할 것이다.

만약, 여러 개의 예외를 명시하고 싶다면 매개변수타입을 Class 객체의 배열로 수정하면 된다.

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
	Class<? extends Throwable>[] value(); 
}

활용할 때는 필요한 예외들을 중괄호로 감싸서 지정하면 된다. 자바 8부터는 @Repeatable 메타 애너테이션을 통해 여러 개의 값을 받는 애너테이션을 만들 수도 있다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest{
	Class<? extends Throwable> value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer{
	ExceptionTest[] value();
}

이 때 주의할 점은.

  1. @Repeatable 을 단 애너테이션을 반환하는 컨테이너 애너테이션을 정의하고, @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
  2. 컨테이너 애너테이션은 내부 에너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
  3. 컨테이너 애너테이션 타입에는 적절한 보존 정책(@Retention) 과 적용 대상(@Target) 을 명시해야 한다.

또한 활용할 때에도, isAnnotationPresent 로 애너테이션 여부를 검사할 때, 컨테이너 애너테이션과 내부 애너테이션을 모두 확인해야 한다는 점도 주의가 필요하다. 단, getAnnotationsByType 은 컨테이너 애너테이션과 내부 애너테이션을 구분하지 않는다.

🍃 결론

열거 타입을 확장하고 싶을 때는 인터페이스를 활용하고, 이왕이면 애너테이션을 사용해서 코딩하자 !


reference

@ChoiSeEun ChoiSeEun self-assigned this Dec 20, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant