Item 39 : 명명 패턴보다 어노테이션을 사용하라
명명 패턴은 테스트 메소드 이름을 test + xxx 처럼 짓는 것을 의미한다.
명명 패턴의 단점은 다음과 같다
- 오타가 나면 안된다.
- 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다
- 메소드가 아닌 클래스이름을 Test로 짓고 사용하면 JUnit은 구별하지 못한다.
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다는 것이다.JUnit이 구분을 하지 못한다.
- 특정 예외를 던져야하는 테스트에서 예외 타입을 테스트에 매개변수로 던지고 싶지만
annotation을 통해 모든 문제를 해결할 수 있다.
Test라는 이름의 어노테이션을 정의한다고 해보자
예외가 발생하면 해당 테스트를 실패로 처리한다.
import java.lang.annotation.*;
/**
* 테스트 메서드임을 선언하는 애너테이션이다.
* 매개변수 없는 정적 메서드 전용이다.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Test 어노테이션에 @Retention과 @Target 어노테이션이 선언되어 있는데
이처럼 어노테이션 선언에 다는 어노테이션을 메타어노테이션이라 한다.
Retention은 유지라는 의미로
@Retention(RetentionPolicy.RUNTIME) 메타 어노테이션은 @Test가 런타임에도 유지되어야 함을 나타내며
이 메타어노테이션이 없으면 테스트 도구는 @Test를 인식할 수 없다.
@Target(ElementType.METHOD) 메타 어노테이션은 @Test가 반드시 메소드 선언에만 사용되야 함을 나타내며
클래스 선언이나 필드 선언등 다른 프로그램 요소에는 달 수 없다는 것을 의미한다.
다음 코드는 @Test 어노테이션을 적용한 모습이며 아무 매개변수 업이 단순히 대상에 마킹한다는 뜻에서
마커 어노테이션이라 한다.
이 어노테이션을 사용하면 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("Crash");
}
public static void m8() { }
}
우리가 Test 어노테이션을 만들때 정적 메소드 전용이라 주석으로 표시해놨지만
실제 별도의 로직을 구현해놓은것이 아니라 m5()와 같은 오류가 발생할 수 있다.
@Test 어노테이션이 Sample 클래스의 의미에 직접적인 영향을 주지는 않는다. 그저 이 어노테이션에 관심 있는 프로그램에게 추가 정보를 제공할 뿐이다.
다음 예시를 봐보자
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);
}
}
이 테스트 러너는 명령줄로부터 완전 정규화된 클래스 이름을 받아,
그 클래스에서 @Test 어노테이션이 달린 메소드를 차례로 호출한다.
isAnnotationPresent가 실행할 메소드를 찾아주는 메소드이다.
만약 테스트가 예외를 던지면 java reflection이 InvocationTargetException으로 감싸서 다시 던진다.
이로써 예외에 담긴 실패 정보를 추출해(getCause) 출력한다.
하지만 이 코드는 모든 예외에 대해 받아들이는데 특정 예외에만 반응하게는 어떻게 만들까?
import java.lang.annotation.*;
/**
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value(); //매개변수타입은 Class<...>이다.
}
이 어노테이션은 Throwable을 확장한 클래스의 Class 각체라는 뜻이며 모든 예외 타입을 수용한다.
import java.util.*;
// 코드 39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램 (241쪽)
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]; //아마 IndexOutofRange일것이다.
}
@ExceptionTest(ArithmeticException.class)
public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음)
}
이제 이 어노테이션을 다루는 테스트 도구이다.
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);
}
}
이 코드는 어노테이션 매개변수의 값을 추출하여 테스트 메소드가 올바른 예외를 던지는지 확인하는데 사용한다.
예외를 매개변수 하나가 아닌 배열로 받을 수도 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
자바 8에서는 여러개의 값을 받는 어놑에ㅣ션을 다른 방식으로도 만들 수 있는데
배열 매개변수를 사용하는 대신 어노테이션에 @Repeatable 메타어노테이션을 다는 방식이다.
주의할점은
- @Repeatable을 단 어노테이션을 반환하는 컨테이너 어노테이션을 하나더 정의해야하고
- @Repeatable에 이 컨테이너 어노테이션의 class 객체를 매개변수로 전달해야 한다.
- 컨테이너 어노테이션은 내부 어노테이션 타입의 배열을 반환하는 value 메소드를 정의해야한다.
- 컨테이너 어노테이션 타입에는 적절한 보존 정책(Retention)과 적용대상(Target)을 명시해야한다.
import java.lang.annotation.*;
@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();
}
정리
이러한 예시들을 통해 우리는 어노테이션으로 처리할 수 있는 일을
명명패턴으로 사용할 필요가 없다는 것을 알 수 있다.
어노테이션의 정의를 한번더 살펴보고 이번 아이템을 마무리한다.
어노테이션 : 데이터를 위한 데이터, 즉 메타 데이터(Meta data)이며 주석역할을 하는 동시에 추가적인 기능을 제공한다.
컴파일 과정에서 추가적으로 처리할 작업을 알려주는 컴퓨터를 위한 주석인 것이다.
'책 > 이펙티브자바' 카테고리의 다른 글
Item 41 : 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2021.08.09 |
---|---|
Item 40 : @Override 어노테이션을 일관되게 사용하라 (0) | 2021.08.09 |
Item 38 : 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2021.08.06 |
Item 37 : ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2021.08.05 |
Item 36 : 비트 필드 대신 EnumSet을 사용하라 (0) | 2021.08.05 |