2023. 12. 17. 13:20ㆍBook/이펙티브 자바
옛날에는 도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 구분되는 명명 패턴을 적용했다고 한다.
예를 들어 JUnit은 버전 3까지 테스트 메서드 이름을 test로 시작하게끔 했다.
public TestClass extends TestCase {
public void testHelloWorld() {
System.out.println("Hello World");
}
}
단점
1. 오타가 나면 안된다. : 프레임워크가 이 메서드를 무시하고 지나친다.
2. 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
3. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
애너테이션은 이 모든 문제를 해결해준다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
@Retention(RetentionPolicy.RUNTIME) => 런타임에도 @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() { }
}
Sample 클래스에는 정적 메서드가 7개고, 그중 4개에 @Test를 달았다.
@Test 애너테이션이 Sample 클래스의 의미에 직접적인 영향을 주지는 않는다. 이 애터테이션에 관심 있는 프로그램에게 추가 정보를 제공할 뿐이다. 예를 한번 보자
public class RunTests {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Sample.class;
for (Method m : testClass.getDeclaredMethods()) {
//Test 어노테이션이 달려 있는지 확인
if (m.isAnnotationPresent(Test.class)) {
tests++;
try {
//인스턴스를 넘기지 않았으므로 정적 메서드가 아니라면 에러가 발생한다.
//메서드 내에서 에러가 발생한다면 리플렉션 메커니즘이
//InvocationTargetException으로 감싸서 다시 던진다.
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("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
=>
public static void effective.study.chapter06.item39.test.Sample.m3() 실패 java.lang.RuntimeException: 실패
잘못 사용한 @Test: public void effective.study.chapter06.item39.test.Sample.m5()
public static void effective.study.chapter06.item39.test.Sample.m7() 실패 java.lang.RuntimeException: 실패
성공: 1, 실패: 3
다음은 특정 예외를 던져야만 성공하는 테스트를 지원해보자(매개변수 1개)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
이 애너테이션은 Throwable을 확장한 클래스의 Class 객체를 매개변수로 맏는다.
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() { }
}
이 애너테이션을 다룰 수 있는 테스트 도구를 정의해보자
public class RunTests2 {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Sample2.class;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.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 e) {
System.out.println("잘못 사용한 @ExceptionTest: " + m);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
=>
테스트 public static void effective.study.chapter06.item39.test.Sample2.m2() 실패 기대한 예외 java.lang.ArithmeticException 발생한 예외 java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 0
테스트 public static void effective.study.chapter06.item39.test.Sample2.m3() 실패: 예외를 던지지 않음
성공: 1, 실패: 2
이 코드는 애너테이션 매개변수의 값을(value()) 값을 추출하여 테스트 메서드가 올바른 예외를 던지는지 확인하는 데 사용한다.
이 예외 테스트에서 한 걸음 더 들어가, 예외를 여러 개 명시하고 그중 하나가 발생하면 성공하게 만들 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionListTest {
Class<? extends Throwable>[] value();
}
배열 매개변수를 맏는 애너테이션용 문법은 아주 유연하다. 단일 원소 배열에 최적화했지만, 앞서의 @ExceptionTest들도 모두 수정 없이 수용한다.
public class Sample3 {
@ExceptionListTest({ IndexOutOfBoundsException.class,
NullPointerException.class })
public static void m1() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
}
테스트 러너
public class RunTests3 {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Sample3.class;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionListTest.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(ExceptionListTest.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);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
=>
성공: 1, 실패: 0
예외를 순회하면서 메서드 실행시 발생한 예외와 비교한다.
자바 8에서는 여러 개의 값을 받는 애너테이션을 다른 방식으로도 만들 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionContainer {
ExceptionRepeatTest[] value();
}
컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value() 메서드를 정의한다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionContainer.class)
public @interface ExceptionRepeatTest {
Class<? extends Throwable> value();
}
@Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달한다.
public class Sample4 {
@ExceptionRepeatTest(IndexOutOfBoundsException.class)
@ExceptionRepeatTest (NullPointerException.class)
public static void m1() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
}
테스터 러너
public class RunRepeatTests {
public static void main(String[] args) throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Sample4.class;
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionContainer.class) ||
m.isAnnotationPresent(ExceptionRepeatTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (Throwable wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionRepeatTest[] excTests =
m.getAnnotationsByType(ExceptionRepeatTest.class);
for (ExceptionRepeatTest excType : excTests) {
if(excType.value().isInstance(exc)) {
passed++;
break;
}
}
if (passed == oldPassed)
System.out.printf("테스트 %s 실패: %s %n", m, exc);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
}
=>
성공: 1, 실패: 0
반복 가능 애너테이션은 처리할 때 주의를 요한다.
반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 컨테이너 애너테이션 타입이 적용된다.
getAnnotationsByType 메서드는 이 둘을 구분하지 않아서 반복 가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져온다.
isAnnotationPresent 메서드는 둘을 명확히 구분한다.
=> 반복 가능한 애너테이션을 여러번 단 다음 isAnnotationPresent로 반복 가능 애너테이션이 달렸는지 검사한다면 false가 나온다. (컨테이너가 달렸기 때문)
public class Sample4 {
@ExceptionRepeatTest(IndexOutOfBoundsException.class)
@ExceptionRepeatTest (NullPointerException.class)
public static void m1() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
}
if (m.isAnnotationPresent(ExceptionContainer.class)) => true;
if (m.isAnnotationPresent(ExceptionRepeatTest.class)) => false;
=> 반복 가능한 애너테이션을 한번만 단 다음 isAnnotationPresent로 반복 가능 애너테이션이 달렸는지 검사한다면 true가 나온다.
public class Sample4 {
@ExceptionRepeatTest (NullPointerException.class)
public static void m1() {
List<String> list = new ArrayList<>();
list.addAll(5, null);
}
}
if (m.isAnnotationPresent(ExceptionContainer.class)) => false;
if (m.isAnnotationPresent(ExceptionRepeatTest.class)) => true;
'Book > 이펙티브 자바' 카테고리의 다른 글
Item41 - 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2023.12.17 |
---|---|
Item40 - @Override 애너테이션을 일관되게 사용하라 (0) | 2023.12.17 |
Item37 - ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2023.12.13 |
Item36 - 비트 필드 대신 EnumSet을 사용하라 (0) | 2023.12.12 |
Item35 - ordinal 메서드 대신 인스턴스 필드를 사용하라 (0) | 2023.12.11 |