아이템 39. 명명 패턴보다 어노테이션을 사용하라. - ksw6169/effective-java GitHub Wiki

명명 패턴의 단점

전통적으로 라이브러리, 프레임워크가 다뤄야 할 프로그램 요소에는 명명 패턴을 적용해왔다.

(ex. JUnit 3는 테스트 메소드 이름을 test로 시작해야 한다.)


1. 오타가 나면 안된다.

예를 들어 JUnit 3에서 테스트 코드를 작성할 때 메소드 이름을 실수로 작성할 경우 JUnit은 이를 무시하고 지나친다.


2. 올바른 프로그램 요소에서만 사용되리라는 보장이 없다.

예를 들어 클래스 이름을 TestSafetyMechanisms 과 같이 지은 다음 JUnit에 주면 개발자는 이 클래스에 정의된 테스트 메소드들을 수행해주길 기대하겠지만 JUnit은 클래스 이름에는 관심이 없기 때문에 개발자가 의도한 테스트는 전혀 수행되지 않는다.


3. 프로그램 요소를 매개변수로 전달할 방법이 없다.

예를 들어 특정 예외를 던져야만 성공하는 테스트가 있다고 하면 기대하는 예외 타입을 테스트에 매개변수로 전달할 수 있어야 한다. 이 때 예외의 이름을 테스트 메소드 이름에 덧붙이는 방법도 있지만 컴파일러는 덧붙여진 문자열이 예외를 가리키는지 알 수가 없다.


어노테이션

어노테이션은 명명 패턴의 단점을 해결해주는 개념으로 JUnit도 버전 4부터 전면 도입하였다. 다음은 예외가 발생하면 실패하는 마커(marker) 어노테이션 타입을 하나 선언한 것이다.

/**
 * 테스트 메소드임을 선언하는 어노테이션이다.
 * 매개변수 없는 정적 메소드 전용이다.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

메타 어노테이션(meta-annotation)

어노테이션 선언에 다는 어노테이션을 말한다.


@Retention 메타 어노테이션

  • @Retention 은 어노테이션이 어느 시점까지 유지될지 결정하는 메타 어노테이션이다.
  • 예를 들어 @Retention(RetentionPolicy.RUNTIME)@Test 가 런타임에도 유지되어야 한다는 표시다.
  • 만약 이 어노테이션을 생략하면 해당 어노테이션 타입은 인식되지 않는다.

@Target 메타 어노테이션

  • 어노테이션이 적용될 대상을 지정하는 메타 어노테이션이다.
  • 클래스, 메소드, 필드 등으로 어노테이션이 적용될 수 있는 범위를 제한할 수 있다.

마커 어노테이션

  • 아무 매개변수 없이 단순히 대상에 마킹한다는 의미로 마커 어노테이션이라 한다.
  • 이러한 어노테이션을 사용하면 예를 들어 프로그래머가 Test 이름에 오타를 내거나 메소드 선언 외의 프로그램 요소에 달면 컴파일 오류를 낸다.
  • 마커 어노테이션은 사용된 클래스에 직접적인 영향을 주지는 않는다. 다만 이 어노테이션에 관심있는 프로그램에게 추가적인 정보를 제공할 뿐이다. 더 넓게 이야기 하면 그 어노테이션에 관심있는 도구에서 특별한 처리를 할 기회를 준다.

예제 1 - 일반 어노테이션

/**
 * 테스트 메소드임을 선언하는 어노테이션이다.
 * 매개변수 없는 정적 메소드 전용이다.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
public class Sample {

    @Test
    public static void m1() { }

    public static void m2() { }

    @Test
    public static void m3() {  // 실패해야 한다.
        throw new RuntimeException("실패");
    }

    public static void m4() { }

    @Test
    public void m5() { }   // 정적 메소드가 아니므로 잘못 사용하였다.

    public static void m6() { }

    @Test
    public static void m7() {  // 실패해야 한다.
        throw new RuntimeException("실패");
    }

    public static void m8() { }
}
/**
 * 마커 어노테이션을 처리하는 프로그램
 */
public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Sample.class;

        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(Test.class)) {
                tests++;

                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException e) {
                    System.out.println(m + " 실패: " + e.getCause());
                } catch (Exception e) {
                    System.out.println("잘못 사용한 @Test: " + m);
                }
            }
        }

        System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
    }
}

예제 2 - 매개변수로 예외를 받는 어노테이션

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

예제 3 - 배열 매개변수를 받는 어노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable>[] value();
}
@ExceptionTest(ArithmeticException.class)
public static void m1() {
    int i = 0;
    i = i / i;
}

@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class })
public static void doublyBad() {
    List<String> list = new ArrayList<>();
    list.addAll(5, null);   // NPE 발생
}

@Repeatable

  • 자바 8 부터는 여러 개의 값을 받는 어노테이션을 다른 방식으로도 만들 수 있다.
  • 배열 매개변수를 사용하는 대신 어노테이션에 @Repeatable 메타 어노테이션을 다는 방식이다.
  • @Repeatable 을 사용한 어노테이션은 하나의 프로그램 요소에 여러 번 달 수 있다.
  • 단, 주의할 점이 있다.
    • @Repetable 을 단 어노테이션을 반환하는 컨테이너 어노테이션을 하나 더 정의해야 한다.
    • 정의한 컨테이너 어노테이션을 @Repeatable 의 값으로 제공해야 한다.
    • 컨테이너 어노테이션에는 적절한 보존 정책(@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() { ... }

Container annotation

어노테이션을 래핑하는 어노테이션을 말한다. @Repeatable이 선언된 어노테이션이 하나의 프로그램 요소에 반복 사용되었을 경우 이는 컴파일러에 의해 컨테이너 어노테이션에 저장된다. 이렇게 저장된 어노테이션은 컨테이너 어노테이션의 value 메소드를 통해 얻어낼 수 있다.


Repeatable annotation 처리 시 주의 사항

getAnnotationByType()
- 반복 가능 어노테이션과 컨테이너 어노테이션 둘을 구분없이 모두 가져온다.

isAnnotationPresent()
- 반복 가능 어노테이션을 여러 번 달았을 때 반복 가능 어노테이션이 달렸는지 검사하면 조회되지 않음
- 컨테이너 어노테이션이 달렸는지 검사하면 반복 가능 어노테이션이 한 번만 쓰인 메소드를 무시하고 조회

결론
- isAnnotationPresent() 를 통해 Repeatable 어노테이션 사용 여부를 조회하려면 
  두 케이스를 모두 고려해야 한다.
if (m.isAnnotationPresent(ExceptionTest.class) 
    || m.isAnnotationPresent(ExceptionTestContainer.class)) {
    tests++;

    ...
}

참고 자료

  • Effective Java 3/E
⚠️ **GitHub.com Fallback** ⚠️