전통적으로 프로그램이 수행될 때 특별한 처리를 제공하기 위하여 명명패턴
을 사용했다.
이는 메서드나 타입의 이름을 특정 규칙으로 짓고, 이 규칙을 지켜 만든 메소드나 타입 등에 추가적인 처리를 제공하는 것이다.
하지만 이런 방법에느 다음의 3가지 문제가 있다.
이름으로 구분하기 때문에 오타가 나면 무시된다.
예를 들어 Junit 3버전까지는 테스트 메서드 이름을 test로 시작하게 했다. 실수로 test를 오타낸다면 Junit은 해당 메서드가 테스트 메서드인지 모르고 지나친다.
Junit3에서는 테스트 메서드 이름을 test로 시작해야 할뿐, 클래스 이름은 이를 따를 필요가 없다.
하지만 사용자는 테스트 클래스에 이 규칙을 적용하고 테스트가 되길 기대할 수 있다.
이 때, 사용자는 어떠한 경고 메시지도 받지 못할 뿐더러 당연히 의도한 테스트를 수행할 수 없다.
특정 예외를 던질 때 성공하는 테스트를 작성한다고 가정하면, 기대하는 예외 타입을 테스트에 전달해야 한다.
명명 패턴 방식으로는 이것이 현실적으로 불가능하다.
하지만 애너테이션을 사용하면 위의 모든 문제를 멋지게 해결할 수 있다.
작은 테스트 프레임워크를 만들면서 애너테이션이 어떻게 사용되는지 살펴보자. 다음은 테스트 수행을 알리기 위해 사용되는 Test 애너테이션의 예이다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
- @Retention : 애너테이션의 스코프를 결정한다.
- @Target : 애너테이션이 적용될 대상을 결정한다.
위의 두 애너테이션은 메타애너테이션
이다.
메타애너테이션이란 애너테이션의 선언에 사용되는 애너테이션을 뜻한다.
Test 애너테이션을 작성했으니, 이를 사용하는 코드를 살펴보자.
public class Sample {
@Test public static void m1(){}
public static void m2(){}
@Test public static void m3(){
throw new RuntimeException("fail");
}
}
Sample 클래스에 선언된 메서드 3개를 살펴보자.
- m1 메서드는 @Test가 선언되었으므로 테스트에 통과할 것이다.
- m2 메서드는 @Test가 선언되지 않았기 때문에 테스트를 수행하지 않는다.
- m3 메서드는 @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)) { //Test 애너테이션이 선언된 메서드만을 호출한다.
tests++;
try {
m.invoke(null);
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " 실패 : " + exc);
} catch (Exception e) {
System.out.println("잘못 사용한 테스트 @Test : " + m);
}
}
}
System.out.printf("suc :: %d, fail : %d%n", passed, tests - passed);
}
}
@Test 애너테이션이 선언된 메서드만을 선별하여 호출한다. 이때 예외가 나지 않으면 테스트는 성공한다.
그렇다면 특정한 예외를 던져야 성공하는 테스트를 지원하려면 어떻게 해야 할까?
우선 매개변수를 받을 수 있는 새로운 애너테이션 타입
이 필요하다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value(); //매개변수 선언
}
이 ExceptionTest 애너테이의 매개변수는 Throwable을 확장한 모든 클래스가 될 수 있도록 선언하였다.
다음은 @exceptionTest를 사용하는 프로그램이다.
@ExcaeptionTest은 매개변수를 통해 어떤 예외를 발생시켜야 하는지 정의하고 있다.
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 1 / 0;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] arr = new int[0];
arr[1] = 1; // outOfIndex 예외 발생
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {}
}
- m1 메서드는 @ExceptionTest에 전달된 ArithmeticException을 던지므로 테스트에 성공한다.
- m2 메서드는 다른 예외를 던지므로 테스트에 실패한다.
- m3 메서드는 예외를 던지지 않으므로 테스트에 실패한다.
public class RunExceptionTests {
public static void main(String[] args) throws Exception{
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("chapter6.Sample2");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) { //ExceptionTest 애너테이션을 사용한 메서드 선별
tests++;
try {
m.invoke(null);
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value(); // 애너테이션의 매개변수 타입 확인
if (excType.isInstance(exc)) { // 애너테이션의 매개변수 타입과 같을 경우 통과
passed++;
}
} catch (Exception e) {
System.out.println("잘못 사용한 테스트 @Test : " + m);
}
}
}
System.out.printf("suc :: %d, fail : %d%n", passed, tests - passed);
}
}
프로그램 요소에 특별한 정보를 제공하여 처리하기 위해서
명명 패턴을 사용할 이유는 없다.
애너테이션을 사용하자.