본문 바로가기

Dev Book Review/Effective Java

[Effective Java] item 39. 명명 패턴보다 애너테이션을 사용하라

1. 명명 패턴의 단점

ex) junit3 : 테스트 메서드의 시작을 test로 시작하게 하였다.

  • 오타가 나면 안된다.
  • 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
    메서드가 아닌 클래스 명을 TestSafetyMechanisms으로 지었을 때
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
    예외를 던져야 성공하는 테스트 : 방법이 없다.

 

2. 마커(marker) 애너테이션

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

메타 애너테이션 (meta-annotation) : 애너테이션 선언에 다는 애너테이션

@Retention(RetentionPolicy.RUNTIME) : @Test가 런타임에도 유지되어야한다 - 보존 정책

@Target(ElementType.METHOD) : @Test가 메서드 선언에서만 사용되어야 한다 - 적용 대상

마커 애너테이션 : 아무 매개변수 없이 단순히 대상에 마킹을 한다.

  • 대상 코드의 의미는 그대로 둔채 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회를 준다.
  • 실제 클래스에 영향은 주지 않지만, 애너테이션에 관심있는 프로그램에 추가 정보를 제공한다.
import java.lang.reflect.*;

public class RunTests { // 코드 39-3 마커 애너테이션을 처리하는 프로그램
  public static void main(String[] args) throws Exception {
    int tests = 0;
    int passed = 0;
    Class<?> testClass = Class.forName(args[0]);
    for (Method m : testClass.getDeclaredMethods()) {
      if (m.isAnnotationPresent(Test.class)) {
        tests++;
        try {
          m.invoke(null);
          passed++;
        } catch (InvocationTargetException wrappedExc) {
          Throwable exc = wrappedExc.getCause();
          System.out.println(m + " 실패: " + exc);
        } catch (Exception exc) {
          System.out.println("잘못 사용한 @Test: " + m);
        }
      }
    }
    System.out.printf("성공: %d, 실패: %d%n",
                      passed, tests - passed);
  }
}

리플렉션을 사용하여, @Test 애너테이션이 달린 메서드 찾기, 원래 예외에 담긴 실패 정보 추출하여 출력한다.

 

3. 매개 변수를 가진 애너테이션

예외를 던져야하는 테스트 애너테이션 만들기

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

애터네이션의 필드 : 애터네이션에서 사용하는 옵션값을 의미한다 @Test(value = "RuntimeException.class")

Class<? extends Throwable> : Throwable을 확장한 클래스의 Class 객체 - 모든 예외와 오류 타입을 수용함

if (m.isAnnotationPresent(ExceptionTest.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
  } catch (InvocationTargetException wrappedEx) {
    Throwable exc = wrappedEx.getCause();
    Class<? extends Throwable> excType =
      m.getAnnotation(ExceptionTest.class).value();
    if (excType.isInstance(exc)) {
      passed++;
    } else {
      System.out.printf(
        "테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n",
        m, excType.getName(), exc);
    }
  } catch (Exception exc) {
    System.out.println("잘못 사용한 @ExceptionTest: " + m);
  }
}

위의 메서드를 위와 같이 수정한다.

애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인하는데 사용한다.

  • 테스트 프로그램이 문제 없이 컴파일 되면 애터네이션 매개변수가 가리키는 예외가 올바른 타입일것.
  • 컴파일타임에는 존재했으나 런타임에는 존재하지 않는 경우 : TypeNotPresentException

 

4. 배열 매개변수를 받는 애너테이션 타입

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Exception>[] value();
}
배열 매개변수 애너테이션의 사용
@ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
if (m.isAnnotationPresent(ExceptionTest.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
  } catch (Throwable wrappedExc) {
    Throwable exc = wrappedExc.getCause();
    int oldPassed = passed;
    Class<? extends Throwable>[] excTypes =
      m.getAnnotation(ExceptionTest.class).value();
    for (Class<? extends Throwable> excType : excTypes) {
      if (excType.isInstance(exc)) {
        passed++;
        break;
      }
    }
    if (passed == oldPassed)
      System.out.printf("테스트 %s 실패: %s %n", m, exc);
  }
}

테스트 러너 수정 : for문을 추가하여 throwable 배열을 검증한다.

 

5. 반복 가능한 애너테이션

  • 자바 8부터 가능하다
  • 배열 매개변수 사용 대신 애너테이션에 @Repeatable메타 애너테이션을 단다.

@Repeatable : 하나의 프로그램 요소에 여러개의 애너테이션을 달 수 있다.

  • @Repeatable을 단 애너테이션을 반환하는 '컨테이션 애너테이션' 을 하나 더 정의한다
  • @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다
  • 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야한다
  • 컨테이너 애너테이션 타입에 적절한 @Retention@Target을 명시해야 한다.
@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();
}

적용 방법

@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {

@Repeatable 처리시 주의점

  • 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용된다.
  • isAnnotationPresent : 반복 가능 애너테이션이 달렸는지 검사 (false : 컨테이너 애너테이션)
  • getAnnotationsByType : 반복 가능 애너테이션과 컨테이너 애너테이션을 모두 가져옴
  • 따로따로 확인하여 코딩한다
if (m.isAnnotationPresent(ExceptionTest.class)
    || m.isAnnotationPresent(ExceptionTestContainer.class)) {
  tests++;
  try {
    m.invoke(null);
    System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
  } catch (Throwable wrappedExc) {
    Throwable exc = wrappedExc.getCause();
    int oldPassed = passed;
    ExceptionTest[] excTests =
      m.getAnnotationsByType(ExceptionTest.class);
    for (ExceptionTest excTest : excTests) {
      if (excTest.value().isInstance(exc)) {
        passed++;
        break;
      }
    }
    if (passed == oldPassed)
      System.out.printf("테스트 %s 실패: %s %n", m, exc);
  }
}
  • 애너테이션을 여러번 달아 코드 가독성을 높였다.
  • 애너테이션을 선언하고 처리하는 부분에서 코드양이 늘어난다.
  • 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명시하자

 

6. 결론

애너테이션이 명명패턴보다 낫다

다른 프로그래머가 소스코드에 추가 정보를 제공할 수 있는 도구를 만드는 일을 한다면 적당한 애너테이션 타입을 제공하자

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

자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용하자