이 장에서는 실제로 명명 패턴보다 애너테이션을 사용하는 것을 추천한다기 보다는 (최근에는 명명 패턴을 거의 안 쓰기 때문에) 애너테이션이 어떻게 나오게 된 건지를 설명하는 장이라고 생각하고 보면 편할 것 같다.
명명 패턴
이 뭘까?
전통적인 프로그래밍에서 특별히 다뤄야할 프로그램 요소에는 구분되는 명명 패턴을 사용해왔다.
ex. JUnit 3 에서는 모든 테스트 메서드 이름이 test로 시작해야 했다.
여기에는 3가지 문제점이 존재한다.
→ 애너테이션
을 사용하면 이런 문제를 해결할 수 있다!
JUnit4도 적극 도입했다.
// marker annotation
// 메타 애너테이션들
// 런타임에도 유지
@Retention(RetentionPolicy.RUNTIME)
// 메서드 선언에만 사용
@Target(ElementType.METHOD)
public @interface Test {
}
// 실제 사용
public class Sample {
@Test
public static void m1() { } // Test should pass
public static void m2() { }
@Test public static void m3() { // Test should fail
throw new RuntimeException("Boom");
}
public static void m4() { } // Not a test
@Test public void m5() { } // INVALID USE: nonstatic method
public static void m6() { }
@Test public static void m7() { // Test should fail
throw new RuntimeException("Crash");
}
public static void m8() { }
}
@Test 애너테이션은 클래스의 의미에 직접적인 영향을 주지 않는다.
해당 애너테이션에 관심 있는 프로그램에게만 추가적인 정보 제공할 뿐.
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)) {
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 {
// 이 애너테이션은 Throwable을 확장한 클래스의 객체만 매개변수로 가진다.
Class<? extends Throwable> value();
}
// 실제 사용
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // Test should pass
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // Should fail (wrong exception)
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // Should fail (no exception)
}
// 애너테이션 동작 원리
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)) {
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);
}
}
// 새로운 애너테이션 타입 추가
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test %s failed: no exception%n", m);
} catch (InvocationTargetException wrappedEx) {
// 에러를 받아서 해당 에러가 맞으면 pass
Throwable exc = wrappedEx.getCause();
Class<? extends Throwable> excType =
m.getAnnotation(ExceptionTest.class).value();
if (excType.isInstance(exc)) {
passed++;
} else {
System.out.printf(
"Test %s failed: expected %s, got %s%n",
m, excType.getName(), exc);
}
} catch (Exception exc) {
System.out.println("Invalid @ExceptionTest: " + m);
}
}
}
System.out.printf("Passed: %d, Failed: %d%n",
passed, tests - passed);
}
}
여러 매개 변수를 받고 싶으면 배열을 이용하면 된다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
// 배열 이용
Class<? extends Exception>[] value();
}
배열 대신에 여러개의 애너테이션을 달고 싶다면?
// 그냥 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
// 반복 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
// 반복 가능하게 해줌
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
// 사용 예시
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
List<String> list = new ArrayList<>();
// The spec permits this staticfactory to throw either
// IndexOutOfBoundsException or NullPointerException
list.addAll(5, null);
}
이 때는 애너테이션을 한 개만 달았을 때와 달리 해당 컨테이너 애너테이션 타입이 적용된다.