1. 명명 패턴의 단점
ex) junit3 : 테스트 메서드의 시작을 test로 시작하게 하였다.
- 오타가 나면 안된다.
- 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
메서드가 아닌 클래스 명을 TestSafetyMechanisms으로 지었을 때 - 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
예외를 던져야 성공하는 테스트 : 방법이 없다.
2. 마커(marker) 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
메타 애너테이션 (meta-annotation) : 애너테이션 선언에 다는 애너테이션
@Retention(RetentionPolicy.RUNTIME) : @Test가 런타임에도 유지되어야한다 - 보존 정책
@Target(ElementType.METHOD) : @Test가 메서드 선언에서만 사용되어야 한다 - 적용 대상
마커 애너테이션 : 아무 매개변수 없이 단순히 대상에 마킹을 한다.
- 대상 코드의 의미는 그대로 둔채 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회를 준다.
- 실제 클래스에 영향은 주지 않지만, 애너테이션에 관심있는 프로그램에 추가 정보를 제공한다.
import java.lang.reflect.*;
public class RunTests { // 코드 39-3 마커 애너테이션을 처리하는 프로그램
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 애너테이션이 달린 메서드 찾기, 원래 예외에 담긴 실패 정보 추출하여 출력한다.
3. 매개 변수를 가진 애너테이션
예외를 던져야하는 테스트 애너테이션 만들기
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
애터네이션의 필드 : 애터네이션에서 사용하는 옵션값을 의미한다 @Test(value = "RuntimeException.class")
Class<? extends Throwable>
: Throwable을 확장한 클래스의 Class 객체 - 모든 예외와 오류 타입을 수용함
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);
}
}
위의 메서드를 위와 같이 수정한다.
애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인하는데 사용한다.
- 테스트 프로그램이 문제 없이 컴파일 되면 애터네이션 매개변수가 가리키는 예외가 올바른 타입일것.
- 컴파일타임에는 존재했으나 런타임에는 존재하지 않는 경우 : TypeNotPresentException
4. 배열 매개변수를 받는 애너테이션 타입
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
배열 매개변수 애너테이션의 사용
@ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
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);
}
}
테스트 러너 수정 : for문을 추가하여 throwable 배열을 검증한다.
5. 반복 가능한 애너테이션
- 자바 8부터 가능하다
- 배열 매개변수 사용 대신 애너테이션에
@Repeatable
메타 애너테이션을 단다.
@Repeatable : 하나의 프로그램 요소에 여러개의 애너테이션을 달 수 있다.
@Repeatable
을 단 애너테이션을 반환하는 '컨테이션 애너테이션' 을 하나 더 정의한다@Repeatable
에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다- 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야한다
- 컨테이너 애너테이션 타입에 적절한
@Retention
과@Target
을 명시해야 한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
적용 방법
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void doublyBad() {
@Repeatable 처리시 주의점
- 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용된다.
isAnnotationPresent
: 반복 가능 애너테이션이 달렸는지 검사 (false : 컨테이너 애너테이션)getAnnotationsByType
: 반복 가능 애너테이션과 컨테이너 애너테이션을 모두 가져옴- 따로따로 확인하여 코딩한다
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);
}
}
- 애너테이션을 여러번 달아 코드 가독성을 높였다.
- 애너테이션을 선언하고 처리하는 부분에서 코드양이 늘어난다.
- 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명시하자
6. 결론
애너테이션이 명명패턴보다 낫다
다른 프로그래머가 소스코드에 추가 정보를 제공할 수 있는 도구를 만드는 일을 한다면 적당한 애너테이션 타입을 제공하자
애너테이션으로 할 수 있는 일을 명명패턴으로 처리할 이유는 없다.
자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용하자
'Dev Book Review > Effective Java' 카테고리의 다른 글
[Effective Java] item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2020.06.27 |
---|---|
[Effective Java] item 40. @Override 애너테이션을 일관되게 사용하라 (0) | 2020.06.27 |
[Effective Java] item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2020.06.27 |
[Effective Java] item 37. ordinal 인덱싱 대신 EnumMap을 사용하라 (1) | 2020.06.27 |
[Effective Java] item 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2020.06.27 |