*알림 : *
Effective Java 3판은 Java 9까지 도입된 언어적 기능을 중심으로 서술되어 있습니다. 10버젼 이후의 Java 개발을 하시는 분들은 우회적인 접근법 대신 Java 언어 내 새로 도입된 기능이 더 간결하고 좋을 수 있습니다.해당 포스팅은 SSAFY 내 책읽기 스터디의 활동을 통해 작성된 포스팅입니다.
https://github.com/kjsu0209/JavaBook
https://medium.com/javabook
유틸이나 프레임워크에서 특별히 다뤄야 할 프로그램 요소에 특정한 이름짓는 규칙을 적용하는 명명 패턴이 있다.
테스트 프레임워크인 JUnit은 test로 시작하는 메서드를 ㅌ베스트 메서드로 인식하게끔 구현되어있다. 그러나 문제점이 있다.
- 오타가 나면 안된다
- tsetBlahBlah와 같이 오타가 난다면 JUnit 3은 이 메서드를 테스트로 취급하지 않는다. 테스트를 무시하지만 별다른 오류를 띄우지 않기 때문에 개발자는 테스트가 통과했다는 착각을 가질 수 있다.
- 개발자가 올바른 프로그램 요소에 사용하리라는 보장이 없다.
- 클래스 이름을 TestSafetyMechanisms로 지어서 개발자는 안에 있는 메서드를 테스트 취급하여 수행해주길 바라지만 JUnit 3은 여기에 관심이 없다.
- 프로그램 요소를 매개변수로 전달할 방법이 없다.
- 특정 예외를 던져야 성공하는 테스트를 수행하고자 하지만, 프로그래머가 함수를 호출하는게 아닌 JUnit이 테스트 메서드를 호출하기 때문에 매개변수를 전달할 방도가 없다.
annotation은 이 문제를 해결하기 위해 도입되었다.
예제 1 : Test annotation
import java.lang.annotation.*;
/**
* 테스트 메서드임을 선언하는 애너테이션이다.
* 매개변수 없는 정적 메서드 전용이다.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
annotation 선언에 annotation이 달려있는 것을 meta-annotation이라 한다.
@Retension
이 annotation이 선언된 annotation은 @Test가 런타임에도 유지되어야 한다는 표시다.
@Target (ElementType.METHOD)
이 annotation이 선언된 annotation은 메서드 선언에 사용되어야 함을 표기한다.
이 예제에서는 개발자가 주석으로 매개변수 없는 정적 메서드에만 부착하도록 구현했다는 의도를 달아놓았지만, 인스턴스 메서드나 매개변수가 있는 메서드에 달 수도 있다. 컴파일러는 이러한 제약을 알 길이 없기 때문이다. 만약 컴파일러가 검사하는 이러한 제약을 구현하고자 한다면 javax.annotation.processing API 문서를 참고하면 된다. 그리고 해당 Test annotation은 아무런 역할을 하지 않아 marker annotation이라고 부른다.
예제 2 : Marker Annotation 적용 예시
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("Crash");
}
public static void m8() { }
}
위에서 언급한 것 처럼 프로그래머가 정적 메서드에만 부착하도록 annotation을 구현하였더라도 별도의 annotation 처리기를 구현하지 않았기에 m5()와 같은 경우가 발생할 수 있다. 컴파일 타임에 에러가 발생하지 않지만, 테스트 과정에 오류가 발생할 수 있다. https://stackoverflow.com/questions/2467544/invoking-a-static-method-using-reflection/2467562
@Test annotation은 Sample 클래스에 아무런 영향을 끼치지 않고, 그냥 테스트를 수행하도록 표시해놓는 역할만을 수행한다. 테스트를 수행하는 프로그램 요소가 이 마커를 보고 테스트를 수행할 것이다.
예제 3 : Marker Annotation을 처리하는 프로그램
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);
}
}
m.isAnnotationPresent(Test.class)를 통해 해당 메서드에 @Test annotation이 달려있는 메서드를 m.invoke()로 호출한다.
만약 테스트가 예외를 던지면 Java Reflection이 InvocationTargetException을 catch하여 실패 정보를 getCause()로 추출하여 출력한다. 만약 InvocationTargetException 이외의 예외가 발생한다면 @Test annotation을 잘못 사용하였다는 의미다.
참고 : 자바의 리플렉션(강관우님) https://brunch.co.kr/@kd4/8
위 예제는 허용되는 아무 예외나 던져도 받아들인다. 만약 특정 예외를 던지면 성공하는 테스트는 어떻게 작성할까?
예제 4 : 매개변수를 받는 annotation 타입
import java.lang.annotation.*;
/**
* 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Throwable> value();//annotation의 매개변수
}
annotation의 매개변수는 Throable을 확장한 클래스의 객체를 받는다.
예제 5 : 매개변수 하나짜리 annotation을 사용한 프로그램
import java.util.*;
// 코드 39-5 매개변수 하나짜리 애너테이션을 사용한 프로그램 (241쪽)
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 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(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);
}
}
}
System.out.printf("성공: %d, 실패: %d%n",
passed, tests - passed);
}
}
m.getAnnotation(ExceptionTest.class).value();를 통해 annotation의 매개변수 값을 추출하고, 발생한 예외와 비교하여 올바른 예외를 던졌는지 확인한다.
하나의 예외가 아니라 여러개의 예외 중 하나가 발생했는지 알고 싶다면?
예제 6 : 배열 매개변수를 받는 annotation
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
Class<? extends Exception>[] value();
}
예제 7 : 배열 매개변수를 받는 annotation을 사용하는 코드
public class Sample3 {
// 이 변형은 원소 하나짜리 매개변수를 받는 애너테이션도 처리할 수 있다. (241쪽 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() { } // 실패해야 한다. (예외가 발생하지 않음)
// 코드 39-7 배열 매개변수를 받는 애너테이션을 사용하는 코드 (242-243쪽)
**@ExceptionTest({ IndexOutOfBoundsException.class,
NullPointerException.class })**
public static void doublyBad() { // 성공해야 한다.
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
}
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()) {
// 배열 매개변수를 받는 애너테이션을 처리하는 코드 (243쪽)
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);
}
}
}
System.out.printf("성공: %d, 실패: %d%n",
passed, tests - passed);
}
}
@ExceptionTest({ IndexOutOfBoundsException.class,NullPointerException.class })와 같이 중괄호로 감싸고 쉼표로 구분해주기만 하면 된다.
여러 값을 받는 annotation을 다른 방식으로도 만들 수 있다.
@Repeatable meta-annotation을 다는게 그 방법이다.
주의할 점도 있다
- @Repeatable을 단 annotation을 반환하는 '컨테이너 annotation'을 하나 더 정의하고, @Repeatable에 컨테이너 annotation의 class 객체를 매개변수로 전달해야 한다.
- 컨테이너 annotation은 내부 annotation의 타입의 배-열을 반환하는 .value() 메서드를 정의해야 한다.
- 컨테이너 annotation 타입에는 @Retention과 @Target을 명시해야 한다. 그렇지 않다면 컴파일에 문제가 생긴다.
예제 8 : @Repeatable을 사용한 annotation 타입
import java.lang.annotation.*;
// 반복 가능한 애너테이션의 컨테이너 애너테이션 (244쪽)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionTest[] value();
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
Class<? extends Throwable> value();
}
예제 9 : 반복 가능한 annotation을 두 번 단 코드
public class Sample4 {
@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() { } // 실패해야 한다. (예외가 발생하지 않음)
// 코드 39-9 반복 가능 애너테이션을 두 번 단 코드 (244쪽)
**@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)**
public static void doublyBad() {
List<String> list = new ArrayList<>();
// 자바 API 명세에 따르면 다음 메서드는 IndexOutOfBoundsException이나
// NullPointerException을 던질 수 있다.
list.addAll(5, null);
}
}
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()) {
// 코드 39-10 반복 가능 애너테이션 다루기 (244-245쪽)
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);
}
}
}
System.out.printf("성공: %d, 실패: %d%n",
passed, tests - passed);
}
}
배열로 선언하는 대신 두 번 달아서 같은 효과를 낼 수도 있지만 주의가 필요하다. 여러 개 달 때와 하나만 달았을 때를 구분하기 위해 '컨테이너' 애너테이션 타입이 적용된다.
getAnnotationsByType() 메서드는 @Repeatable을 사용한 Annotation과 컨테이너 Annotation를 모두 가져오지만, isAnnotationPresent() 메서드는 둘을 명확히 구분하기 때문에 @Repeatable을 사용한 Annotation이 달렸는지 isAnnotationPresent()로 검사할 때는 컨테이너 Annotation과 @Repeatable을 사용한 Annotation 모두를 검사해주어야 한다.
if (m.isAnnotationPresent(ExceptionTest.class)
|| m.isAnnotationPresent(ExceptionTestContainer.class))
정리하기
Annotation으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다. 처리코드가 방대해져 오류가 날 가능성이 높지만, 가독성이 높아진다.
'IT > Effective Java' 카테고리의 다른 글
[Effective Java 3/E] ITEM 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2021.03.28 |
---|---|
[Effective Java 3/E] ITEM 40. @Override Annotation을 일관적으로 사용하기 (0) | 2021.03.28 |
[Effective Java 3/E] ITEM 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라. (0) | 2021.03.28 |
[Effective Java 3/E] ITEM 37. ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2021.03.28 |
[Effective Java 3/E] ITEM 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2021.03.28 |
댓글