Item 39. Annotation

명명 패턴보다 애너테이션을 사용하라.

예전엔 명명 패턴을 사용해 프로그램 요소에 정보를 표시했다. 하지만 명명 패턴은 프로그램 요소를 매우 불편하게 만든다. 예를들어 이전의 Junit은 테스트 메서드를 test로 시작하는 이름으로 지어야 테스트를 실행했기 때문에, 오타나 의도하지 않은 메서드가 테스트 대상이 되는 경우가 많았다. 애너테이션 방법으로 명명 패턴을 완전히 대체할 수 있기 때문에 명명 패턴을 사용하지 말고 애너테이션을 사용하는 것이 좋다.

Annotation

애너테이션은 클래스, 메서드, 필드 등의 프로그램 요소에 부가 정보를 덧붙이는 방법이다. 이 방법을 사용하면 위의 명명 패턴의 단점을 모두 해결하면서 간결하고 명확하게 프로그램 요소의 의미를 전달할 수 있다.

마커 애너테이션


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

@Test 애너테이션을 살펴보면 @interface라는 키워드로 선언되어 있고, @Retention@Target이라는 애너테이션을 가지고 있다. 이와 같이 애너테이션 선언에 다는 애너테이션을 메타 애너테이션(meta-annotation)이라고 한다.

  • @Retention: 해당 애너테이션을 언제까지 유지할 것인지를 지정, 여기서는 RetentionPolicy.RUNTIME으로 지정되어 있어 @Test가 런타임에도 유지되어야 한다는 것을 의미함

  • @Target: 해당 애너테이션을 어디에 사용할 수 있는지를 지정, 여기서는 ElementType.METHOD로 지정되어 있어 @Test가 메서드 선언에만 사용할 수 있음

이렇게 생성된 @Test 애너테이션은 다음과 같이 사용하는데, 이처럼 매개변수 없이 사용할 수 있는 애너테이션을 마커(marker) 애너테이션이라고 한다.

class Sample {
    @Test
    public static void m1() {
        // Test m1
    }
}

애너테이션은 말 그대로 부가 정보를 덧붙이는 것이기 때문에, 해당 요소에 직접적인 영향을 주지 않고, 이 애너테이션을 처리할 프로그램에게 추가 정보를 제공하는 것이 전부이다. 이러한 마커 애너테이션을 처리하는 코드 예시는 다음과 같다.

public class RunTests {
    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)) { // @Test 애너테이션이 존재하는지 확인
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

매개변수를 받는 애너테이션

애너테이션에도 매개변수를 받아 정의할 수 있는데, 이를 사용한 예시는 다음과 같다.


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

@ExceptionTest 애너테이션은 @Test 애너테이션과 비슷하지만, value라는 매개변수를 받아서 테스트 메서드가 특정한 예외를 던져야만 성공하는 테스트임을 명시한다.

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

해당 애너테이션을 처리하는 코드는 다음과 같다.

public class RunTests {
    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(ExceptionTest.class)) { // @ExceptionTest 애너테이션이 존재하는지 확인
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (Throwable wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    int oldPassed = passed;
                    Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value(); // @ExceptionTest의 value를 가져옴
                    for (Class<? extends Throwable> excType : excTypes) { // @ExceptionTest의 value를 순회
                        if (excType.isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed) {
                        System.out.printf("Test %s failed: %s %n", m, exc);
                    }
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

@Repeatable

Java 8부터는 애너테이션을 반복해서 적용하여 여러 개의 값을 받는 방법을 제공한다. 기존 애너테이션 정의에 @Repeatable을 추가하고, 애너테이션을 담을 컨테이너 애너테이션을 정의하면 된다.

// @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();
}

이렇게 정의된 애너테이션을 사용하기 위해선 단순히 여러 개의 @ExceptionTest 애너테이션을 적용하면 된다.

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

추가적으로 애너테이션을 처리하는 코드도 하나만 달았을 때와 여러 개를 달았을 때를 구분하여 처리해야 한다.

public class RunTests {
    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(ExceptionTest.class)
                || m.isAnnotationPresent(ExceptionTestContainer.class)) { // @ExceptionTest 또는 @ExceptionTestContainer 애너테이션이 존재하는지 확인
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (Throwable wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    int oldPassed = passed;
                    ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class); // @ExceptionTest 애너테이션을 가져옴
                    for (ExceptionTest excTest : excTests) { // @ExceptionTest 애너테이션을 순회
                        if (excTest.value().isInstance(exc)) {
                            passed++;
                            break;
                        }
                    }
                    if (passed == oldPassed) {
                        System.out.printf("Test %s failed: %s %n", m, exc);
                    }
                }

            }
        }
        System.out.printf("Passed: %d, Failed: %d%n", passed, tests - passed);
    }
}

Last updated

Was this helpful?