[Effective Java] 6 - item39 명명 패턴보다 애너테이션을 사용하라.
명명 패턴
전통적으로 도구나 프레임워크가 특별히 다루어야 하는 프로그램 요소에는 명명 패턴을 사용해 왔다. JUnit 3까지는 테스트 메서드 이름을 test로 시작하게 하는 식이다.
이 방법은 효과적으로 보일 수 있으나…
- 오타가 나면 안된다.
- 올바른 프로그램 요소에서만 사용되리라 보증할 수 없다.
- 메서드가 아니라, 클래스 이름을 Test로 시작하게 하면 테스트가 수행되지 않을 것이다.
- 프로그램 요소를 매개변수로 전달할 방법이 없다.
- 특정 예외를 던져야 성공하는 테스트가 존재할 때, 기대하는 예외 타입을 테스트에 매개변수로 전달할 방법이 없다.
애너테이션
애너테이션은 앞서 언급되었던 명명 패턴의 단점을 해결할 수 있다.
마커(marker) 애너테이션
다음은 아무런 메서드를 선언하지 않은 인터페이스로, 타입체크를 하기위해 사용하는 마커 애너테이션을 선언하는 코드이다.
/**
* 테스트 메서드임을 선언하는 애너테이션이다.
* 매개변수 없는 정적 메서드 전용이다. */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}
@Test
애너테이션 타입 선언에도 두 가지 다른 애너테이션이 달려 있는데, 이와 같이 애너테이션 선언에 다는 애너테이션을 메타 애너테이션이라 한다.
@Retention(RetentionPolicy.RUNTIME)
:@Test
가 런타임에도 유지되어야 한다.
RetentionPolicy
class 내부는 다음과 같이 되어 있고, 다양한 설정을 제공한다.
public enum RetentionPolicy {
/**
* Annotations are to be discarded by the compiler. */ SOURCE,
/**
* Annotations are to be recorded in the class file by the compiler * but need not be retained by the VM at run time. This is the default * behavior. */ CLASS,
/**
* Annotations are to be recorded in the class file by the compiler and * retained by the VM at run time, so they may be read reflectively. * * @see java.lang.reflect.AnnotatedElement
*/ RUNTIME
}
@Target(ElementType.METHOD)
:@Test
가 메서드 선언에만 사용할 것을 명시한다.
public enum ElementType {
/** Class, interface (including annotation interface), enum, or record
* declaration */ TYPE,
/** Field declaration (includes enum constants) */
FIELD,
/** Method declaration */
METHOD,
/** Formal parameter declaration */
PARAMETER,
/** Constructor declaration */
CONSTRUCTOR,
/** Local variable declaration */
LOCAL_VARIABLE,
/** Annotation interface declaration (Formerly known as an annotation type.) */
ANNOTATION_TYPE,
/** Package declaration */
PACKAGE,
/**
* Type parameter declaration * * @since 1.8
*/ TYPE_PARAMETER,
/**
* Use of a type * * @since 1.8
*/ TYPE_USE,
/**
* Module declaration. * * @since 9
*/ MODULE,
/**
* Record component * * @jls 8.10.3 Record Members
* @jls 9.7.4 Where Annotations May Appear
* * @since 16
*/ RECORD_COMPONENT;
}
다음은 선언한 마커 애너테이션을 사용한 프로그램 예시이다.
@Test
애너테이션이 Sample에 직접 영향을 끼치지는 않고, 이 애너테이션에 관심 있는 프로그램에게 추가 정보를 제공하고 처리가 가능하게 한다.
public class Sample {
@Test
public static void m1() {} // 성공
public static void m2() {}
@Test
public static void m3() {
throw new RuntimeException("Fail"); // 실패
}
public static void m4() {}
@Test
public void m5() {} // 정적 메서드가 아니므로 잘못 사용한 예시이다.
public static void m6() {}
}
매개변수 하나를 받는 애너테이션
/**
* 명시한 예외를 던져야만, 성공하는 테스트케이스 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
// 한정적 와일드카드를 통해 Throwable을 상속한 모든 타입을 지정한다.
Class<? extends Throwable> values();
}
위와 같이 선언한 애너테이션은 다음과 같이 사용할 수 있다.
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() {
int i = 0;
i = i / i; // divide by zero. ArithmeticException 예외를 발생시킴 -> 성공
}
@ExceptionTest(ArithmeticException.class)
public static void m2() {
int[] a = new int[0];
int i = a[1]; // IndexOutOfBoundsException 발생 -> ArithmeticException가 아니므로 실패한다.
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {} // 아무 Exception도 발생하지 않음 -> 실패
}
여러 매개변수를 받는 애너테이션
배열을 이용한 구현
여러 매개변수를 받아 처리하는 것도 가능하며, 다음은 배열을 통해 구현한 방식이다.
/**
* 명시한 예외를 던져야만, 성공하는 테스트케이스 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest{
//한정적 와일드카드를 통해 Throwable을 상속한 모든 타입을 지정
Class<? extends Throwable>[] values();
}
public class Sample3 {
@ExceptionTest({IndexOutOfBoundsException.class,
NullPointerException.class})
public static void m1() {
List<String> list = new ArrayList<>();
//자바 명세에 따르면, 다음 메서드는 IndexOutOfBoundsException이나,
//NullPointerException을 던질 수 있다.
//예외 발생 시 성공
list.addAll(5, null);
}
}
@Repeatable
애너테이션
Java 8에서는 @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 m1() {...}
반복 가능 애너테이션은 처리할 때 주의를 요한다. 반복 가능 애너테이션을 여러개 달면, 하나만 달았을 때와 구분하기 위해 해당 컨테이너 애너테이션 타입이 적용된다.
getAnnotationByType
메서드는 이 둘을 구분하지 않아@ExceptionTest
와@ExceptionTestContainer
를 모두 가져온다.isAnnotationPresent는
둘을 구분한다.- 만약
@ExceptionTest
를 여러번 단 다음,isAnnotationPresent
로ExceptionTest
를 검사하면false
가 나온다.
(@ExceptionTestContainer
로 인식하기 때문) - 반대로
@ExceptionTest
를 한번 만 단 다음,isAnnotationPresent
로ExceptionTestContainer
를 검사하면false
가 나온다.
(@ExceptionTest
가 적용되었기 때문)
- 만약
때문에 모두 검사하려면 두 가지 메서드 모두 확인해야 한다.
Leave a comment