전통적으로 도구나 프레임워크가 특별히 다뤄야 할 프로그램 요소에는 딱 구분되는 명명 패턴을 적용
ex) JUnit3에서는 테스트 메서드 이름을 test로 시작하게 지어야 했음
단점
오타가 나면 안 된다.
실수로 이름을 tset~라고 지으면, JUnit 3은 이 메서드를 무시하고 지나침
올바른 프로그램 요소에서만 사용되리란 보증이 없다.
(메서드가 아닌) 클래스명을 TestSafetyMechanisms로 지어 JUnit에 던져줘도, JUnit은 무시
프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
(특정 예외를 던져야만 성공하는 테스트가 있다고 가정) 기대하는 예외 타입을 테스트에 매개변수로 전달해도, 컴파일러는 예외를 가리키는지 알 리가 없음
JUnit은 버전 4에서 애너테이션 전면 도입
1. 마커 애너테이션 (매개변수가 없는)
39-1 마커(marker) 애너테이션 타입 선언
/**
* 테스트 메서드임을 선언하는 애너테이션
* 매개변수 없는 정적메서드 전용
*/
@Retention(RetentionPolicy.RUNTIME) // 메타애너테이션, @CustomTest가 런타임에도 유지되어야 한다는 표시
@Target(ElementType.METHOD) // 메타애너테이션, @CustomTest가 메서드 선언에서만 사용돼야 한다는 표시
public @interface CustomTest{
}
@CustomTest애너테이션 실제 적용 모습 (적절한 애너테이션 처리기 없음)
= 애너테이션을 아무 매개변수 없이 단순히 대상에 마킹(Marking)한다는 뜻에서 마커 애너테이션 (Marker Annotation)이라 부름
@Documented: 문서에도 애너테이션 정보가 표현되게 함
@Inherited: 자식클래스가 애너테이션을 상속받을 수 있게 함
@Repeatable: 애너테이션을 반복적으로 사용할 수 있게 함
@Retention(RetentionPolicy): 애너테이션의 범위를 지정
RetentionPolicy.RUNTIME: 컴파일 이후에도 JVM에 의해 참조가 가능
RetentionPolicy.CLASS: 컴파일러가 클래스를 참조 할때 까지 유효
RetentionPolicy.SOURCE: 애너테이션 정보가 컴파일 이후 사라짐
@Target(ElementType[]): 애너테이션이 적용될 위치를 선언
ElementType.PACKAGE: 패키지 선언시
ElementType.TYPE: 타입 선언시
ElementType.CONSTRUCTOR: 생성자 선언시
ElementType.FIELD: 멤버 변수 선언시
ElementType.METHOD: 메소드 선언시
ElementType.ANNOTATION_TYPE: 어노테이션 타입 선언시
ElementType.LOCAL_VARIABLE: 지역 변수 선언시
ElementType.PARAMETER: 매개 변수 선언시
ElementType.TYPE_PARAMETER: 매개 변수 타입 선언시
ElementType.TYPE_USE: 타입 사용시
- `@Retention(RetentionPolicy.RUNTIME)`
- RetentionPolicy는 애너테이션의 범위를 지정
- `@CustomTest`가 런타임에도 유지되어야 한다는 메타애너테이션
- `@Target(ElementType.METHOD)`
- Target은 애너테이션이 적용될 위치를 선언
- `@CustomTest`가 메서드 선언에서만 사용돼야 한다는 메타애너테이션
- ![image](https://user-images.githubusercontent.com/7028121/54006546-0fac4900-41a1-11e9-9ac5-f38e87840e72.png)
- 메서드 주석에는 "매개변수 없는 정적 메서드 전용이다" 라고 쓰여있지만, 이 제약을 컴파일러가 강제할 수 없음
- 적절한 애너테이션 처리기를 직접 구현해야 함
- [javax.annotation.processing API 문서 참고](https://docs.oracle.com/javase/7/docs/api/javax/annotation/processing/package-summary.html)
39-2 마커 애너테이션을 사용한 프로그램 예
public class Sample {
@CustomTest
public static void m1() {
// 성공
}
public static void m2() {
//실행 (X)
}
@CustomTest
public static void m3() {
// 실패
throw new RuntimeException("실패");
}
public static void m4() {
// 실행 (X)
}
@CustomTest
public void m5() {
// 잘못 사용한 예: 정적 메서드가 아니다.
}
public static void m6() {
// 실행 (X)
}
@CustomTest
public static void m7() {
// 실패
throw new RuntimeException("실패");
}
public static void m8() {
// 실행 (X)
}
}
- 총 8개의 메서드
- `@CustomTest`를 단 4개의 메서드 실행
- m1 성공
- m3 / m7 실패
- m5 잘못 사용
- 나머지는 무시
> 39-3 마커 애너테이션을 처리하는 프로그램
```JAVA
public class RunTests {
public static void main(String[] args) throws Exception {
sampleTest(); // 마커 애너테이션 테스트 runner
}
private static void sampleTest() throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample");
// 클래스에서 메서드를 차례로 호출
for (Method m : testClass.getDeclaredMethods()) {
// isAnnotationpresent가 실행할 메서드를 찾아주는 메서드
if (m.isAnnotationPresent(CustomTest.class)) {
tests++;
try {
m.invoke(null);
passed++;
// 테스트 메서드 예외 처리
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + " 실패 : " + exc);
// 그 외의 예외 처리 -> @CustomTest 애노테이션을 잘못 사용했다는 뜻
} catch (Exception exc) {
System.out.println("잘못 사용한 @CustomTest : " + m);
}
}
}
System.out.printf("전체 수행 Test : %d%n", tests);
System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
}
}
위 RunTests로 39-2의 Sample을 실행한다면
public static void item39.Sample.m3() 실패 : java.lang.RuntimeException: 실패
잘못 사용한 @CustomTest: public void item39.Sample.m5()
public static void item39.Sample.m7() 실패 : java.lang.RuntimeException: 실패
전체 수행 Test : 4
성공: 1, 실패: 3
@CustomTest 애너테이션은 Sample클래스의 의미(메소드 코드에서 어떤 내용을 실행하는지)에 직접적으로 영향을 주지는 않음
일반적으로 애너테이션은 애너테이션이 달린 코드의 의미(실행 로직)를 변경할수 없음
그러나 애너테이션에 관심있는 도구에게 특별한 처리를 해줄 수는 있음
2.매개변수가 있는 애너테이션
39-4 매개변수 하나를 받는 애너테이션 타입
특정 예외를 던져야만 성공하는 테스트를 지원하는 새로운 애너테이션 생성
import java.lang.annotation.*;
/**
명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
- 매개변수 타입은 Class<? extends Throwable>
- "Throwable을 확장한 클래스의 Class 객체" -> 모든 예외(와 요류)타입 수용
39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램
public class Sample2 {
@ExceptionTest(ArithmeticException.class)
public static void m1() { // 0으로 나누면 발생하는 ArithmeticException 예외 -> 성공
int i = 0;
i = i / i;
}
@ExceptionTest(ArithmeticException.class)
public static void m2() { // IndexOutOfBoundsException 발생 -> ArithmeticException가 아니므로 실패
int[] a = new int[0];
int i = a[1];
}
@ExceptionTest(ArithmeticException.class)
public static void m3() {} // 아무 Exception도 발생하지 않음 -> 실패
}
매개변수 하나짜리 애너테이션 processor (39-3 main 메서드 수정)
public class RunTests {
public static void main(String[] args) throws Exception {
sample2Test(); // 매개변수가 하나인 애너테이션 테스트 runner
}
private static void sample2Test() throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample2");
// 클래스에서 `@ExceptionTest` 애너테이션이 달린 메서드를 차례로 호출
for (Method m : testClass.getDeclaredMethods()) {
// isAnnotationpresent가 실행할 메서드를 찾아주는 메서드
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);
}
// 그 외의 예외 처리 -> @ExceptionTest 애노테이션을 잘못 사용했다는 뜻
} catch (Exception exc) {
System.out.println("잘못 사용한 @ExceptionTest: " + m);
}
}
}
System.out.printf("전체 수행 Test : %d%n", tests);
System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
}
}
테스트 public static void item39.Sample2.m2() 실패: 기대한 예외 java.lang.ArithmeticException, 발생한 예외 java.lang.ArrayIndexOutOfBoundsException: 1
테스트 public static void item39.Sample2.m3() 실패: 예외를 던지지 않음
전체 수행 Test : 3
성공 : 1, 실패 : 2
위의 @CustomTest와의 한 가지 차이는 애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던졌는지 확인하는데 사용
m.getAnnotation(ExceptionTest.class).value()를 실행할 때 ArithmeticException.class가 리턴
3. 배열 매개변수를 받는 애너테이션
Exception이 발생하는 종류를 묶어서 처리하고 싶을 때도 있다.
39-6 배열 매개변수를 받는 애너테이션 타입
/**
명시한 예외를 던져야만, 성공하는 테스트케이스 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionArrayTest{
//한정적 와일드카드를 통해 Throwable을 상속한 모든 타입을 지정
Class<? extends Throwable>[] values();
}
@ExceptionTest에서 배열형태로 Exception클래스를 받을 수 있도록 배열로 선언하였다.
39-7 배열 매개변수를 받는 애너테이션을 사용하는 코드
public class Sample3 {
@ExceptionArrayTest({IndexOutOfBoundsException.class, NullPointerException.class})
public static void m1() {
List<String> list = new ArrayList<>();
//자바 명세에 따르면, 다음 메서드는 IndexOutOfBoundsException이나,
//NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
}
- 원소가 여럿인 배열을 지정할 때는 원소들을 중괄호로 감싸고 쉼표로 구분
```java
public class RunTests {
public static void main(String[] args) throws Exception {
sample3Test(); // 매개변수가 여러개(배열)인 애너테이션 테스트 runner
}
private static void sample3Test() throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample3");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionArrayTest.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionArrayTest.class).value();
int oldPassed = passed;
for (Class<? extends Throwable> excType : excTypes) {
if (excType.isInstance(exc)) {
passed++;
System.out.printf("테스트 %s 성공 : 기대한 예외 %s, 발생한 예외 %s%n", m, excType.getName(), exc);
break;
}
}
if (passed == oldPassed) {
System.out.printf("테스트 %s 실패: %s %n", m, exc);
}
} catch (Exception e) {
System.out.println("잘못 사용한 @ExceptionTest: " + m);
}
}
}
System.out.printf("전체 수행 Test : %d%n", tests);
System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
}
}
테스트 public static void item39.Sample3.m1() 성공 : 기대한 예외 java.lang.IndexOutOfBoundsException, 발생한 예외 java.lang.IndexOutOfBoundsException: Index: 5, Size: 0
전체 수행 Test : 1
성공 : 1, 실패 : 0
위의 코드 중 변경되는 부분은 Class<? extends Throwable>[] excTypes을 배열형태로 받아,
이미 지정한 Exception 클래스 중에 맞는 클래스가 있는지 확인하는 코드가 변경되었다.
4. 반복 가능 애너테이션 (@Repeatable의 사용 / java 8에서부터 지원)
여러 개의 값을 받는 애너테이션을 다른 방식으로도 만들 수 있음
39-8 반복 가능한 애너테이션 타입
/* ExceptionRepeatableTest.java */
// 반복 가능한 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionRepeatableContainer.class)
public @interface ExceptionRepeatableTest {
Class<? extends Throwable> value();
}
- 배열 매개변수를 사용하는 대신 애너테이션에`@Repeatable` 메타애너테이션을 다는 방식
- `@Repeatable` 애너테이션은 하나의 메서드에 여러개의 애너테이션을 지정할 수 있음
1) `@Repeatable`을 단 애너테이션을 반환하는 컨테이너 애너테이션을 하나 더 정의한다.
2) `@Repeatable`에 이 컨터이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
3) 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
4) 컨터이너 애너테이션에는 보존 정책(@Retention)과 적용 대상(@Target)을 적절히 명시한다.
- 그렇지 않으면 컴파일되지 않음
> 39-9 반복 가능 애너테이션을 두 번 단 코드
```java
public class Sample4 {
@ExceptionRepeatableTest(IndexOutOfBoundsException.class)
@ExceptionRepeatableTest(NullPointerException.class)
public static void m1() {
List<String> list = new ArrayList<String>();
list.addAll(5, null);
}
}
반복 가능 애너테이션은 처리할 때 주의
반복 가능 애너테이션을 여러개 달면,하나만 달았을 때와 구분하기 위해 해당 '컨테이너' 애너테이션 타입이 적용
getAnnotationByType 메서드는 이 둘을 구분하지 않아 @ExceptionRepeatableTest와 @ExceptionRepeatableContainer를 모두 가져옴
isAnnotationPresent는 둘을 구분한다.
만약 @ExceptionRepeatableTest를 여러번 단 다음, isAnnotationPresent로 ExceptionRepeatableTest를 검사하면 false가 나옴
@ExceptionRepeatableContainer로 인식하기 때문
반대로 @ExceptionRepeatableTest를 한번 만 단 다음, isAnnotationPresent로 ExceptionRepeatableContainer를 검사하면 false가 나옴
@ExceptionRepeatableTest가 적용되었기 때문
39-10 반복 가능 애너테이션 다루기
public class RunTests {
public static void main(String[] args) throws Exception {
sample4Test(); // 반복 가능 애너테이션 테스트 runner
}
private static void sample4Test() throws Exception {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample4");
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(ExceptionRepeatableTest.class)
|| m.isAnnotationPresent(ExceptionRepeatableContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionRepeatableTest[] excTypes = m.getAnnotationsByType(ExceptionRepeatableTest.class);
for (ExceptionRepeatableTest excType : excTypes) {
if (excType.value().isInstance(exc)) {
System.out.printf("테스트 %s 성공 : 기대한 예외 %s, 발생한 예외 %s%n", m, excType.value().getName(), exc);
passed++;
break;
}
}
if (passed == oldPassed) {
System.out.printf("테스트 %s 실패: %s %n", m, exc);
}
}
}
}
System.out.printf("전체 수행 Test : %d%n", tests);
System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
}
}
```java
테스트 public static void item39.Sample4.m1() 성공 : 기대한 예외 java.lang.IndexOutOfBoundsException, 발생한 예외 java.lang.IndexOutOfBoundsException: Index: 5, Size: 0
전체 수행 Test : 1
성공 : 1, 실패 : 0
반복 가능 애너테이션을 사용해 하나의 프로그램 요소에 같은 애너테이션을 여러 번 달 때의 코드 가독성을 높임
하지만 애너테이션을 선언하고 이를 처리하는 부분에서는 코드 양이 늘어나며, 특히 처리 코드가 복잡해져 오류가 날 가능성이 커짐을 명심하자
요약
애너테이션이 명명 패턴보다 낫다
지금까지 본 예시는 애너테이션으로 할 수 있는 일중 극히 일부를 예로 든거다
다른 프로그래머가 만든 소스코드에 추가 정보를 제공할 수 있는 도구를 만든다면 적당한 애너테이션 타입도 함께 정의해 제공하자
애너테이션은 명명패턴의 문제를 모두 해결해주는 개념
올바른 프로그램 요소에서만 사용되리란 보증이 없다.
프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
1. 마커 애너테이션 (매개변수가 없는)
@Target(ElementType[]): 애너테이션이 적용될 위치를 선언
}
@CustomTest
애너테이션은 Sample클래스의 의미(메소드 코드에서 어떤 내용을 실행하는지)에 직접적으로 영향을 주지는 않음그러나 애너테이션에 관심있는 도구에게 특별한 처리를 해줄 수는 있음
2.매개변수가 있는 애너테이션
/**
매개변수 하나짜리 애너테이션 processor (39-3 main 메서드 수정)
@CustomTest
와의 한 가지 차이는 애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던졌는지 확인하는데 사용m.getAnnotation(ExceptionTest.class).value()를 실행할 때 ArithmeticException.class가 리턴
3. 배열 매개변수를 받는 애너테이션
}
위의 코드 중 변경되는 부분은 Class<? extends Throwable>[] excTypes을 배열형태로 받아, 이미 지정한 Exception 클래스 중에 맞는 클래스가 있는지 확인하는 코드가 변경되었다.
4. 반복 가능 애너테이션 (@Repeatable의 사용 / java 8에서부터 지원)
/ ExceptionRepeatableContainer.java / // 컨테이너 애너테이션 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionRepeatableContainer { ExceptionRepeatableTest[] value(); }
@ExceptionRepeatableTest
와@ExceptionRepeatableContainer
를 모두 가져옴@ExceptionRepeatableTest
를 여러번 단 다음, isAnnotationPresent로 ExceptionRepeatableTest를 검사하면 false가 나옴@ExceptionRepeatableContainer
로 인식하기 때문@ExceptionRepeatableTest
를 한번 만 단 다음, isAnnotationPresent로 ExceptionRepeatableContainer를 검사하면 false가 나옴@ExceptionRepeatableTest
가 적용되었기 때문}
요약