[Effective Java] 6장 열거 타입과 애너테이션 (2부)


정리
아이템 38. 열거타입 자체는 확장할 수 없지만, 인터페이스와 그 인터페이스를 구현하는 기본 열거타입을 함께 사용해 같은 효과를 낼 수 있다. 그러면 클라이언트는 인터페이스를 구현해 자신만의 열거타입을 만들수 있다. API가 인터페이스 기반으로 작성되었다면 기본 열거타입의 인스턴스가 쓰이는 모든 곳에 새로 확장한 열거타입의 인스턴스로 대체할 수 있다.
아이템 40. 재정의한 모든 메서드에 @Override 애너테이션을 의식적으로 달면 실수했을 때 컴파일러가 알려줄것이다. 구체 클래스에서 상위 클래스의 추상메서드를 재정의한 경우 이 애너테이션을 달지 않아도 된다.(달아도 상관없음)
아이템 41. 마커 인터페이스와 마커 애너테이션은 각자의 쓰임이 있다. 새로 축가하는 메서드 없이 단지 타입 정의가 목적이라면 마커 인터페이스를 선택하자. 클래스나 인터페이스 외의 프로그램 요소에 마킹해야 하거나 애너테이션을 적극 활용하는 프레임워크를 사용한다면 마커 애너테이션이 올바른 선택이다. 단 적용대상이 ElementType.TYPE인 마커 애너테이션을 작성하고 있다면 애너테이션으로 구현하는 게 옳은지, 마커 인터페이스가 더 나은 방법인지 고려하자.


내용
아이템 38. 확장할 수 있는 열거타입이 필요하면 인터페이스를 사용하라
● 열거 타입은 거의 모든 상황에서 안전 열거패턴(typesafe enum pattern)보다 좋다. 하지만 확장을 할 수 없는 단점이 있다.
● 대부분이 열거타입을 확장하는 것은 좋은 생각이 아니지만 연산 코드에서는 확장할 수 있는 열거타입이 필요하다. API가 제공하는 기본연산 외에 사용자 확장연산을 추가할 수 있도록 해야할 때 사용된다.
열거타입 확장 예제 (Interface를 이용)
public interface Operation {
                  double apply(double x, double y);
}
public enum BasicOperation implements Operation {
                  PULS("+") {
                                   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;
                  }
}
● 위의 예제의 장점은 개별 인스턴스 수준에서뿐 아니라 타입 수준에서도, 기본 열거타입 대신 확장된 열거타입을 넘겨 확장된 열거타입의 원소를 모두 사용하게 할 수도 있다.
● 확장 열거타입을 테스트 할 시 2가지 방법이 있다.
한정적 타입 토큰을 사용하던가 한정적 와일드 카드 타입를 사용한다.
한정적 타입 토큰에서 <T extends Enum<T> & Operation> 의 의미는 Class 객체가 열거타입인 동시에 Operation의 하위타입이어야 한다는 뜻이다. 그렇기 때문에 한정적 와일드 카드 타입가 더 유연함을 보여준다.
public static void main(String[] args) {
                  double x = 0.0;
                  double y = 0.0;
                  test(ExtendedOperation.class, x, y); // 한정적 타입 토큰 사용시
                  test(Arrays.asList(ExtendedOperation.values()), x, y); // 한정적 와일드 카드 타입 사용시
}
// 한정적 타입 토큰 사용시
private static <T extends Enum<T> & Operation> void test(
                  Class<T> opEnumType, double x, double y) {
                  for (Operation op : opEnumType.getEnumConstants()) {
                                   sysout(~~);
                  }
}
// 한정적 와일드 카드 타입 사용시
private static  void test(Collection<? extends Operation> opSet,
                  double x, double y) {
                                   for (Operation op : opSet) {
                                   sysout(~~);
                  }
}

아이템 39. 명명 패턴보다 애너테이션을 사용하라
● 애너테이션이 나오기전엔 명명 패턴을 사용했는데, 명명 패턴에는 3가지의 단점이 있다.
1. 오타가 나면 안된다.
2. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
3. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
● 메타애너테이션(meta-annotation)은 애너테이션 선언에 다는 애너테이션을 의미한다.
● 마커애너테이션 : 아무 매개변수 없이 단순히 대상에 마킹한다는 뜻이다.
마커애너테이션은 해당 클래스에 직접적인 영향을 주지 않고, 그저 이 애너테이션에 관심있는 프로그램에게 추가 정보를 제공할 뿐이다.
● 자바 8에서는 여러 개의 값을 받는 애너테이션을 다른 방식으로 만들수 있다. 배열 매개변수를 사용하는 대신 애너테이션에 @Repeatable 메타애너테이션을 다는 방식이다. @Repeatable을 단 애너테이션은 하나의 프로그램 요소에 여러 번 달 수 있다.
@Repeatable 사용시 주의할 점이 있다.
1. @Repeatable을 단 애너테이션을 반환하는 '컨테이너 애너테이션'을 하나 더 정의하고, @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야한다.
2. 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야한다.
3. 컨테이너 애너테이션 타입에는 적절한 보존 정책(@Retention)과 적용 대상(@Target)을 명시해야 한다.
● 반복 가능 애너테이션을 여러 개 달면, 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용된다. getAnnotationsByType 메서드는 이 둘을 구분하지 않아서 반복 가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져오지만, isAnnotationPresent 메서드는 둘을 명확히 구분한다.
isAnnotationPresent로 반복 가능 애너테이션이 달렸는지 검사한다면 아니라고 알려준다. 그래서 모두 검사하려면 둘을 따로따로 확인해야한다.
● 자바가 제공하는 애너테이션 타입들을 사용하는 것이 좋다. 자바에서 제공하는 애너테이션은 다음과 같다.
Bulit-in Annotation
@Override - 메소드가 오버라이드 됐는지 검증합니다. 만약 부모 클래스 또는 구현해야할 인터페이스에서 해당 메소드를 찾을 수 없다면 컴파일 오류가 납니다.
@Deprecated - 메소드를 사용하지 말도록 유도합니다. 만약 사용한다면 컴파일 경고를 일으킵니다.
@SuppressWarnings - 컴파일 경고를 무시하도록 합니다.
@SafeVarargs - 제너릭 같은 가변인자 매개변수를 사용할 때 경고를 무시합니다. (자바7 이상)
@FunctionalInterface - 람다 함수등을 위한 인터페이스를 지정합니다. 메소드가 없거나 두개 이상 되면 컴파일 오류가 납니다. (자바 8이상)
Meta_Annotations
@Retention - 애노테이션의 범위이다. 어떤 시점까지 애노테이션이 영향을 미치는지 결정합니다.
@Documented - 문서에도 애노테이션의 정보가 표현됩니다.
@Target - 애노테이션이 적용할 위치를 결정합니다.
@Inherited - 이 애노테이션을 선언하면 자식클래스가 애노테이션을 상속 받을 수 있습니다.
@Repeatable - 반복적으로 애노테이션을 선언할 수 있게 합니다.

아이템 40. @override 애너테이션을 일관되게 사용하라
● @Override는 메서드 선언에만 달 수 있고, 이 애너테이션이 달렸다는 것은 상위 타입의 메서드를 재정의했을을 뜻한다. @Override를 일관되게 사용하면 여러가지 버그를 예방할 수 있다.
● 상위 클래스의 메서드를 재정의하려는 모든 메서드에 @Override 애너테이션을 사용하자
● 구체 클래스에서 상위 클래스의 추상메서드를 재정의할 때는 굳이 @Override를 달지 않아도 된다.

아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
● 아무 메서드도 담고 있지 않고, 단지 자신을 구현하는 클래스가 특정 속성을 가짐을 표시해주는 인터페이스를 마커 인터페이스(marker interface)라고 한다.
● 마커 인터페이스는 다음과 같은 경우 낫다.
1. 마커 인터페이스는 이를 구현한 클래스의 인스턴스들을 구분하는 타입으로 쓸수 있으나, 마커 애너테이션은 그렇지 않다.
2. 마커 인터페이스가 나은 점은 두 번재는 적용대상이 더 정밀하게 지정할 수 있다는 것이다. 애너테이션은 적용 대상(@Target) ElementType.Type으로 선언한 애너테이션은 모든 타입에 사용할 수 있다.
● 마커 애너테이션이 마커 인페이스보다 나은 점으로는 거대한 애너테이션 시스템의 지원을 받는다는 점이다. 애너테이션을 적극 활용하는 프레임워크에서는 마커 애너테이션을 쓰는 쪽이 일관성을 지키는 데 유리하다.

참고
● 사용자 애노테이션 정의 방법
public @interface MyAnnonation {}
아주 심플한 커스텀 어노테이션입니다.
이제 여기에 입맛대로 몇가지 메타 어노테이션들을 선언해주면 됩니다. 다음은 이것저것 다가져다 붙인 코드입니다.
import java.lang.annotation.*;
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME) // 컴파일 이후에도 JVM에 의해서 참조가 가능합니다.
//@Retention(RetentionPolicy.CLASS) // 컴파일러가 클래스를 참조할 때까지 유효합니다.
//@Retention(RetentionPolicy.SOURCE) // 어노테이션 정보는 컴파일 이후 없어집니다.
@Target({
        ElementType.PACKAGE, // 패키지 선언시
        ElementType.TYPE, // 타입 선언시
        ElementType.CONSTRUCTOR, // 생성자 선언시
        ElementType.FIELD, // 멤버 변수 선언시
        ElementType.METHOD, // 메소드 선언시
        ElementType.ANNOTATION_TYPE, // 어노테이션 타입 선언시
        ElementType.LOCAL_VARIABLE, // 지역 변수 선언시
        ElementType.PARAMETER, // 매개 변수 선언시
        ElementType.TYPE_PARAMETER, // 매개 변수 타입 선언시
        ElementType.TYPE_USE // 타입 사용시
})
public @interface MyAnnotation {
    /* enum 타입을 선언할 수 있습니다. */
    public enum Quality {BAD, GOOD, VERYGOOD}
    /* String은 기본 자료형은 아니지만 사용 가능합니다. */
    String value();
    /* 배열 형태로도 사용할 수 있습니다. */
    int[] values();
    /* enum 형태를 사용하는 방법입니다. */
    Quality quality() default Quality.GOOD;
}
설명이 필요할 것 같은 부분은 주석으로 대체했습니다.
마커 인터페이스와 마커 애너테이션
마커 인터페이스 예
Cloneable interface
// Java program to illustrate Cloneable interface
import java.lang.Cloneable;
// By implementing Cloneable interface
// we make sure that instances of class A
// can be cloned.
class A implements Cloneable {
                  int i;
                  String s;
                  // A class constructor
                  public A(int i,String s) {
                                   this.i = i;
                                   this.s = s;
                  }
                  // Overriding clone() method
                  // by simply calling Object class
                  // clone() method.
                  @Override
                  protected Object clone() throws CloneNotSupportedException {
                                   return super.clone();
                  }
}
public class Test {
                  public static void main(String[] args) throws CloneNotSupportedException {
                                   A a = new A(20, "GeeksForGeeks");
                                   // cloning 'a' and holding
                                   // new cloned object reference in b
                                   // down-casting as clone() return type is Object
                                   A b = (A)a.clone();
                                   System.out.println(b.i);
                                   System.out.println(b.s);
                  }
}
Serializable interface
// Java program to illustrate Serializable interface
import java.io.*;
// By implementing Serializable interface
// we make sure that state of instances of class A
// can be saved in a file.
class A implements Serializable
{
                  int i;
                  String s;
                  // A class constructor
                  public A(int i,String s) {
                                   this.i = i;
                                   this.s = s;
                  }
}
public class Test {
                  public static void main(String[] args) throws IOException, ClassNotFoundException {
                                   A a = new A(20,"GeeksForGeeks");
                                   // Serializing 'a'
                                   FileOutputStream fos = new FileOutputStream("xyz.txt");
                                   ObjectOutputStream oos = new ObjectOutputStream(fos);
                                   oos.writeObject(a);
                                   // De-serializing 'a'
                                   FileInputStream fis = new FileInputStream("xyz.txt");
                                   ObjectInputStream ois = new ObjectInputStream(fis);
                                   A b = (A)ois.readObject();//down-casting object
                                   System.out.println(b.i+" "+b.s);
                                   // closing streams
                                   oos.close();
                                   ois.close();
                  }
}
마커 애너테이션 예
마커 애너테이션 Processor
public class RunTests {
    public static void main(String[] args) {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName(args[0]);
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {
                test++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " 실패: " + exc);
                } catch (Exception e) {
                    System.out.println("잘못 사용한 @Test: " + m);
                }
            }
        }
       
        System.out.printf("성공: %d, 실패: %d%n", passed, tests-passed);
    }
}
Marker 애노테이션의 예 – Spring
@Configurable
public class Account {
  // ...
}



댓글

가장 많이 본 글