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

들어가며

JUnit4부터 애너테이션을 이용한 테스트 프레임워크 지원을 시작했다. 이는 기존의 명명패턴의 단점을 제거해주며 효과적으로 동작한다. 다음은 Test라는 애너테이션을 정의한 코드이다.

1
2
3
4
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}

@Retention@Test가 런타임에도 유지되어야 한다는 의미이다. @Target@Test가 반드시 메소드 선언에만 사용되야 한다는 의미이다. 다음은 애너테이션을 적용한 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Sample {
@Test
public static void m2() {}
@Test
public static void m3() {
throw new RuntimeException("Failure");
}
public static void m4() {}
@Test
public void m5() {}
public static void m6() {}
@Test
public static void my() {
throw new RuntimeException("Failure");
}
public static void m8() {}
}

@Test 애너테이션이 Sample 클래스의 의미에 직접적인 영향을 주진 않는다. 그저 이 애너테이션에 관심있는 프로그램에게 추가 정보를 줄 뿐이다. 즉, 이 에너테이션에 관심이 있는 프로그램에게 특별한 처리를 할 기회를 준다. 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " Failure : " + exc);
} catch (Exception exc) {
System.out.println("Wrong Useage @Test : " + m);
}
}
}
System.out.printf("Success: %d, Fauilure: %d%n", passed, tests - passed);

}
}

이 코드에서 테스트가 예외를 던지면 리플렉션 메커니즘에 의해 InvocationTargetException으로 감싸서 던져준다. 그리고 이 예외를 잡아 실패한 원래 예외를 던져준다. 다음은 특정 예외를 던져야만 성공하는 테스트를 위한 애너테이션 코드이다.

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 0;
i = i / 1;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class<?> testClass = Sample2.class;

for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("Test Failed %s : Did not Throw Exception");
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();
if(excType.isInstance(exc))
passed++;
else
System.out.printf("Test %s Failed : Expected %s but %s%n",m, excType.getName(), exc);
} catch (Exception exc) {
System.out.println("Wrong Useage @ExceptionTest : " + m);
}
}
}

이 애너티이션의 타입은 Throwalbe을 확장한 클래스의 Class 객체를 모두 받을 수 있으며 따라서 모든 예외 타입을 다 수용한다. 여러개의 예외를 받는 에너테이션은 어떻게 만들까? 다음 코드를 보자.

1
2
3
4
5
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable>[] value();
}
1
2
3
4
5
6
7
public class Sample3 {
@ExceptionTest({ArithmeticException.class, SomeOtherException.class})
public static void m1() {
int i = 0;
i = i / 1;
}
}
1
Class<? extends Throwable>[] excType = m.getAnnotation(ExceptionTest.class).value();

자바 8에서는 다른 방식으로도 만들 수 있다. @Repeatable 메타 애너테이션을 달면 된다. 단 주의할점이 있다. 첫째로 @Repetable을 단 애너테이션을 반환하는 컨테이너 애너테이션을 하나 더 정의하고 @Repetable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다. 둘째로 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메소드를 정의해야 한다. 마지막으로 보전 정책과 적용 대상을 명시해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repetable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable>[] value();
}


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
ExceptionTest[] value();
}
1
2
3
@ExceptionTest(SomeException1.class)
@ExceptionTest(SomeException2.class)
public static void someMethod() { ... }

처리도 주의를 요한다. 반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 컨테이너 애너테이션 타입이 적용된다. getAnnotationsByType 메소드는 이 둘을 구분하지 않아 반복가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져오지만 isAnnotationPresent 메소드는 이를 구분한다. 따라서 반복가능 애너테이션을 여러번 단 다음 isAnnotationPresent로 반복 가능 애너테이션이 달렸는지 검사하면 아니라고 말한다. 컨테이너가 달렸기 때문이다. 그래서 애너테이션을 여러 번 단 메소드를 무시하고 건너뛴다. 같은 이유로 isAnnotationPreset으로 컨테이너 애너테이션이 달렸는지 검사하면 반복 가능 애너테이션을 단 하나만 단 메소드들은 무시하고 지나친다. 이를 방지하려면 이 둘을 따로 처리해야 한다.

1
if(m.isAnnotationPresent(ExceptionTest.class), m.isAnnotationPresent(ExceptionTestController.class)) { ... }
Author: Song Hayoung
Link: https://songhayoung.github.io/2020/08/14/Languages/Effective%20JAVA/item39/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.