핵심 내용
제목이 다다. 아래 내용은 애너테이션을 만드는 예제를 다룬 것..
명명 패턴의 단점
1. 오타가 나면 안 된다.
2. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
ex. 메소드가 아닌 클래스 명을 TestSafeMechanisms로 지은 경우
3. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
ex. 특정 예외를 던져야 성공하는 테스트
뭐 이런 단점을 모두 해결해주는 것이 애너테이션이다.
마커(marker) 애너테이션
: 아무 매개변수 없이 단순히 대상에 마킹
// 오타나 메소드 선언 외의 프로그램 요소에 달면 컴파일 오류 발생
import java.lang.annotation.*;
/**
* 테스트 메서드임을 선언하는 애너테이션이다.
* 매개변수 없는 정적 메서드 전용이다.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
애너테이션 선언에 다는 애너테이션(@Retention, @Target)을 메타애너테이션(meta-annotation)이라 한다.
@Retention(RetentionPolicy.RUNTIME)
@Test가 런타임에도 유지되어야 한다. 즉, 해당 메타애너테이션을 생략하면 테스트 도구는 @Test를 인식할 수 없다.
@Target(ElementType.METHOD)
@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("실패");
}
public static void m8() { }
}
// 성공: 1, 실패: 3
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);
}
}
위 코드는 테스트 통과한 메소드의 개수와 실패한 메소드 개수를 출력한다.
먼저, Class.forName(args[0])을 통해 테스트를 실행할 클래스를 불러온다. 그 후, @Test 애너테이션이 달린 메소드를 호출한다. @Test를 붙이지 않은 메소드는 테스트 도구는 무시한다.
isAnnotationPresent는 실행할 메소드를 찾아주는 메소드이다. 해당 메소드가 예외를 던지면 InvocationTargetException으로 감싸 다시 던진다. 이후 이 프로그램은 InvocationTargetException을 잡아 원래 예외에 담긴 실패 정보를 getCause해 출력한다.
InvocationTargetException 이외의 예외가 발생한다면 @Test 애너테이션을 잘못 사용한 경우이다.
현재 위 코드는 매개변수 없는 정적 메소드라는 제약을 컴파일러가 강제하지 않는다. m5() 메소드는 어떻게 동작할까?
invoke 메소드는 reflection API가 제공하는 메소드로, 특정 메소드를 동적으로 실행하도록 한다. (리플렉션 알못임)
정적 메소드인 m1의 경우 m.invoke(null)이 m1 메소드를 성공적으로 호출한다. 반면 인스턴스 메소드인 m5는 특정 객체 인스턴스가 필요하다. 그러나 invoke 메소드를 호출할 때, null을 전달하면, 필요한 객체 인스턴스가 없다는 예외가 발생한다. 즉, m.invoke(null)이 m5 메소드를 호출하려고 하면 IllegalArgumentException이 발생한다.
매개변수 하나를 받는 애너테이션
import java.lang.annotation.*;
/**
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value(); // 매개변수
}
ExceptionTest 어노테이션은 Class<? extends Throwable> 타입의 매개변수를 가진다. 이는 ExceptionTest 어노테이션을 사용할 때 어떤 예외를 기대하는지를 지정해줄 수 있게 한다.
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() { } // 실패해야 한다. (예외가 발생하지 않음)
}
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);
}
}
배열 매개변수를 받는 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable>[] value(); // 매개변수
}
@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void doublyBad() { // 성공해야 한다.
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
Class<? extends Throwable>[] excTypes =
m.getAnnotation(ExceptionTest.class).value();
for (Class<? extends Throwable> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("테스트 %s 실패: %s %n", m, exc);
}
}
}
반복 가능한 애너테이션
자바 8에서는 배열 매개변수 대신 @Repeatable 메타애너테이션을 달아 구현할 수 있다.
주의할 점 1. @Repeatable을 단 애너테이션을 반환하는 컨테이너 애너테이션을 하나 더 정의하고, @Repeatavle에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
주의할 점 2. 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메소드를 정의해야 한다.
주의할 점 3. 컨테이너 애너테이션 타입에는 적절한 보존 정책(@Retention)과 적용 대상(@Target)을 명시해야 한다.(그렇지 않으면 컴파일X)
// 반복 가능한 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class) //컨테이너 애너테이션 class 객체
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
// 컨테이너 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value(); // value 메서드 정의
}
// 반복 가능 애너테이션을 두 번 단 코드
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
// 반복 가능 애너테이션 다루기
if (m.isAnnotationPresent(ExceptionTest.class)
|| m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionTest[] excTests =
m.getAnnotationsByType(ExceptionTest.class);
for (ExceptionTest excTest : excTests) {
if (excTest.value().isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("테스트 %s 실패: %s %n", m, exc);
}
}
}
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라. (0) | 2023.07.02 |
---|---|
[effective java] 아이템 40. @Override 애너테이션을 일관되게 사용하라. (0) | 2023.07.02 |
[effective java] 아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라. (0) | 2023.07.02 |
[effective java] 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라. (0) | 2023.07.02 |
[effective java] 아이템 36. 비트 필드 대신 EnumSet을 사용하라. (0) | 2023.05.14 |