도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에 전통적으로는 딱 구분되는 명명 패턴을 적용해왔다. 이번 아이템에서는 애너테이션의 동작 방식을 보여주고자 직접 제작한 작은 테스트 프레임워크를 사용한다.
이 테스트 프레임워크는 아주 간단하지만 애너테이션이 명명 패턴보다 낫다는 점을 확실히 보여준다.
여러분이 다른 프로그래머가 소스코드에 추가 정보를 제공할 수 있는 도구를 만드는 일을 한다면 적당한 애너테이션 타입도 함께 정의해 제공하자. 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.
명명 패턴애너테이션마커 애너테이션 메타 애너테이션해당 애너테이션을 활용하는 프로그램 예시매개변수를 받는 애너테이션 해당 애너테이션을 활용하는 프로그램 예시배열 매개변수를 받는 애너테이션여러 개 값을 받는 애너테이션의 다른 방식 : @Repetable 메타애너테이션
명명 패턴
- 테스트 프레임워크인 Junit 버전 3까지 테스트 메서드 이름을 test로 시작하게끔 했음
- 단점
- 오타가 나면 Junit 3은 이 메서드를 무시하고 지나치기 때문에 개발자는 이 테스트가 (실패하지 않았으니) 통과했다고 오해할 수 있음
- 올바른 프로그램 요소(메서드, 클래스 등)에서만 사용되리라 보증할 방법이 없음
- 예를 들어, 메서드가 아닌 클래스 이름을 TestSafetyMechanisms 로 지어 Junit에 주면, Junit은 아무 동작을 수행하지 않음
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없음
- 예 : 특정 Exception이 발생하는 상황을 테스트 하기 위해 그 Exception을 매개변수로 넘겨주어야 할 때
- 애너테이션이 이 모든 문제를 해결해주는 멋진 개념으로 Junit 4 부터 도입되었음
애너테이션
마커 애너테이션
package effectivejava.chapter6.item39.markerannotation; import java.lang.annotation.*; // 코드 39-1 마커(marker) 애너테이션 타입 선언 (238쪽) import java.lang.annotation.*; /** * 테스트 메서드임을 선언하는 애너테이션이다. * 매개변수 없는 정적 메서드 전용이다. -- 애너테이션에 이것을 강제하는 내용은 전혀 없음. 활용 예시에서 * m.invoke(null)로 호출하면서 정적 메서드가 아니면 에러가 발생하도록 활용함 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { }
- @Retention과 @Target 처럼 애너테이션 선언에 다는 애너테이션을 메타애너테이션 이라 함
- 주석에 “매개변수 없는 정적 메서드 전용" 이라는 제약을 강제하기 위해서는 annotationProcessor를 직접 구현해야 함 ( javax.annotation.processing API 문서를 참고해보라 함)
- “아무 매개변수 없이 단순히 대상에 마킹한다” ⇒ 마커 애너테이션
- Test 이름(애너테이션 이름)에 오타를 내거나 메서드 선언 외의 프로그램 요소에 달면 컴파일 오류를 내줌
메타 애너테이션
@Retention(RetentionPolicy.RUNTIME)
- @Test 가 런타임에도 유지되어야 한다는 표시임
- 이 메타애너테이션을 생략하면 테스트 도구는 @Test를 인식할 수 없음
@Target(ElementType.METHOD)
- 해당 메타애너테이션은 @Test가 반드시 메서드 선언에서만 사용돼야 한다고 알려줌
- 클래스 선언, 필드 선언 등에 대해서는 해당 애너테이션 달 수 없음
해당 애너테이션을 활용하는 프로그램 예시
- @Test 애너테이션은 해당하는 메서드의 의미에 직접적인 영향을 주지는 않고, 해당 애너테이션에 관심 있는 프로그램에게 추가 정보를 제공할 뿐임 ↔ 대상 코드의 의미는 그대로 둔 채 그 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회를 줌
package effectivejava.chapter6.item39.markerannotation; 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() { } }
package effectivejava.chapter6.item39.markerannotation; import java.lang.reflect.*; 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 + " 실패: " + exc); } catch (Exception exc) { System.out.println("잘못 사용한 @Test: " + m); } } } System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed); } }
매개변수를 받는 애너테이션
package effectivejava.chapter6.item39.annotationwithparameter; // 코드 39-4 매개변수 하나를 받는 애너테이션 타입 (240-241쪽) import java.lang.annotation.*; /** * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable> value(); }
- 매개변수 타입이
Class<? extends Throwable>
- Throwable을 확장한 클래스의 Class 객체. 모든 예외(와 오류) 타입을 다 수용함
- 한정적 타입 토큰의 활용 사례
해당 애너테이션을 활용하는 프로그램 예시
package effectivejava.chapter6.item39.annotationwithparameter; import effectivejava.chapter6.item39.annotationwithparameter.ExceptionTest; import java.util.*; public class Sample2 { @ExceptionTest(ArithmeticException.class) public static void m1() { // 성공해야 한다. int i = 0; i = i / i; } @ExceptionTest(ArithmeticException.class) public static void m2() { // 실패해야 한다. (다른 예외 발생) int[] a = new int[0]; int i = a[1]; } @ExceptionTest(ArithmeticException.class) public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음) }
package effectivejava.chapter6.item39.annotationwithparameter; import effectivejava.chapter6.item39.markerannotation.Test; import java.lang.reflect.*; // 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)) { 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); } } } System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed); } }
배열 매개변수를 받는 애너테이션
package effectivejava.chapter6.item39.annotationwitharrayparameter; import java.lang.annotation.*; // 코드 39-6 배열 매개변수를 받는 애너테이션 타입 (242쪽) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Exception>[] value(); } // 사용 시 @ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class }) public static void doublyBad() { // 성공해야 한다. List<String> list = new ArrayList<>(); // 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나 // NullPointerException을 던질 수 있다. list.addAll(5, null); }
여러 개 값을 받는 애너테이션의 다른 방식 : @Repetable 메타애너테이션
package effectivejava.chapter6.item39.repeatableannotation; import java.lang.annotation.*; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Repeatable(ExceptionTestContainer.class) public @interface ExceptionTest { Class<? extends Throwable> value(); }
package effectivejava.chapter6.item39.repeatableannotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTestContainer { ExceptionTest[] value(); }
주의점
- @Repetable을 단 애너테이션을 반환하는 ‘컨테이너 애너테이션'을 하나 더 정의하고 @Repetable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 함
- 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 함
- 컨테이너 애너테이션 타입에는 적절한 보존 정책(@Retention)과 적용 대상(@Target)을 명시해야 함
반복 가능 애너테이션 처리 시 주의점
- 반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 ‘컨테이너' 애너테이션 타입이 적용됨
- 하나만 달면
ExceptionTest
애너테이션으로 정의됨 - 여러개 달면
ExceptionTestContainer
애너테이션으로 정의됨
- getAnnotationsByType 메서드로는 이 둘을 구분하지 않아서 반복 가능 애너테이션(
ExceptionTest
)과 그 컨테이너 애너테이션(ExceptionTestContainer
) 둘다 가져옴
- isAnnotationPresent 메서드는 둘을 명확히 구분함
- 그래서 둘다 검사하려면 이렇게 체크
if (m.isAnnotationPresent(ExceptionTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class))
- 이렇게 처리 코드가 복잡해져서 오류가 날 가능성이 커짐