이펙티브 자바, 쉽게 정리하기 - item 39. 명명 패턴보다 애너테이션을 사용하라
명명패턴이란?
- 메서드의 이름 앞을
test...
로 짓는 등 이름에 패턴을 주어Reflection
등으로 해당 패턴 검출 시 특정 작업을 수행하는 식의 코딩 형식이다.
명명패턴의 단점
- 오탈자의 위험
- 메서드, 파라미터, 클래스명 등 영역에 대한 설정이 불가능하다.
- 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
- ex) 특정 예외가 던져져야 올바르게 실행되는 메서드가 있다면?
예제 실행 샘플 1: @MethodTest
, 일반 메서드 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MethodTest {
}
@Retention
과@Target
은 메타 애너테이션이라 불린다.@Retention
은 생존기간을 나타낸다.RetentionPolicy.RUNTIME
: 런타임에도 유지되어야 한다는 표시이다.
@Target(ElementType.METHOD)
: 해당 애너테이션이 반드시 메서드에 적용되어야 한다는 것을 알려준다.@MethodTest
: 애너테이션과 같이 아무 매개변수 없이 단순히 대상에 마킹하는 애너테이션을 마크 애너테이션이라고 한다.
public class Sample {
@MethodTest
public static void m1() { }
public static void m2() { }
@MethodTest
public static void m3() {
throw new RuntimeException("실패");
}
public static void m4() { }
@MethodTest
public void m5() { } // 잘못 사용한 예: 정적 메서드가 아니다.
public static void m6() { }
@MethodTest
public static void m7() {
throw new RuntimeException("실패");
}
public static void m8() { }
}
@Test
public void sampleClassAnnotationTest() throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample");
Method[] declaredMethods = testClass.getDeclaredMethods();
for (Method m : declaredMethods) {
if(m.isAnnotationPresent(MethodTest.class)) {
tests++;
try {
m.invoke(null);
System.out.println(m + ", 성공");
passed++;
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
System.out.println(m + ", 실패: " + exc);
} catch (Exception exc) {
System.out.println("잘못 사용한 @Test: " + m + ", " + exc);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
@Test
애너테이션은Sample
클래스의 의미에 직접적인 영향을 주진 않고, 추가 정보를 주어 이 애너테이션에 관심이 있다면 특별한 처리를 할 수 있는 기회를 준다.- 클래스를 가져와서 메서드를 가져오고(
getDeclaredMethods()
) 리플렉션 API(Method
)를 통해 해당 클래스의 메서드를 불러온다. isAnnotationPresent()
는 특정 애노테이션이 붙어있는지 확인할 수 있는 메서드이다.invoke(null)
을 통한 메서드 호출에 성공한다면, 아직 인스턴스화 전에 호출할 수 있는 메서드였으므로 정적 메서드였을 것이다.- 그러므로, 정적 메서드가 아닌 메서드는 호출에 실패하고
null
을 넘긴 덕에NullPointerException
을 던지게 될 것이다.
- 그러므로, 정적 메서드가 아닌 메서드는 호출에 실패하고
실행 결과
public static void item39.Sample.m3(), 실패: java.lang.RuntimeException: 실패
public static void item39.Sample.m1(), 성공
잘못 사용한 @Test: public void item39.Sample.m5(), java.lang.NullPointerException: Cannot invoke "Object.getClass()" because "obj" is null
public static void item39.Sample.m7(), 실패: java.lang.RuntimeException: 실패
성공: 1, 실패: 3
RuntimeException
을 던지는 두개의 메서드는 예상대로 실패한다.- 정적 메서드가 아니었던
m5()
도 실패한다. - 정적 메서드이며, 예외를 던지지 않던
m1()
만 멀쩡히 성공한다.
예제 실행 샘플 2: @ExceptionSingleTest
기본매개변수로 Exception
을 받는 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionSingleTest {
Class<? extends Throwable> value();
}
- 애너테이션 내부
value()
필드는 기본 파라미터 값을 의미한다.@Target
애너테이션의 경우 기본 파라미터 값으로ElementType
을 받는다고 보면 된다.- 여기서는
Throwable
을 상속하는 모든 클래스를 받기 때문에 모든 예외를 포용할 수 있다.
public class Sample2 {
@ExceptionSingleTest(ArithmeticException.class)
public static void m1() {
int i = 0; // 성공
i = i / i;
}
@ExceptionSingleTest(ArithmeticException.class)
public static void m2() {
int[] a = new int[0]; // 실패, 다른 예외 발생
int i = a[1];
}
@ExceptionSingleTest(ArithmeticException.class)
public static void m3() { } // 실패, 예외가 발생하지 않음
}
ArithmeticException.class
를 인자로 주었다.
@Test
public void sample2ClassAnnotationTest() throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample2");
Method[] declaredMethods = testClass.getDeclaredMethods();
for (Method m : declaredMethods) {
if(m.isAnnotationPresent(ExceptionSingleTest.class)) {
tests++;
try {
m.invoke(null);
System.out.println(m + ", 테스트 실패 (예외를 던지지 않음)");
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
Class<? extends Throwable> excType = m.getAnnotation(ExceptionSingleTest.class).value(); // 애너테이션 매개변수의 값을 추출한다.
if(excType.isInstance(exc)) {
System.out.printf("테스트 %s 성공: 기대한 예외 %s, 발생한 예외 %s%n", m, excType.getName(), 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);
}
Sample2
클래스에 선언된 메서드 중@ExceptionSingleTest
애너테이션이 달린 것을 찾는다.InvocationTargetException
의 하위 예외가 일어나고, 그 예외가@ExceptionSingleTest
애너테이션의 인수로서 들어온 예외와 일치해야만passed
의 카운트가 올라간다.InvocationTargetException
은 리플렉션 API의 통합 예외로.getCause()
메서드를 통해서 실제 예외가 무엇인지 알 수 있다.
실행 결과
public static void item39.Sample2.m3(), 테스트 실패 (예외를 던지지 않음)
테스트 public static void item39.Sample2.m2() 실패: 기대한 예외 java.lang.ArithmeticException, 발생한 예외 java.lang.ArrayIndexOutOfBoundsException: Index 1 out of bounds for length 0
테스트 public static void item39.Sample2.m1() 성공: 기대한 예외 java.lang.ArithmeticException, 발생한 예외 java.lang.ArithmeticException: / by zero
성공: 1, 실패: 2
예제 실행 샘플 3: @ExceptionArrayTest
기본매개변수로 Exception
배열을 받는 애너테이션
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionArrayTest {
Class<? extends Throwable>[] value();
}
- 이전과 형태는 동일한데 배열로 받는 점만 다르다.
- 사실 위의 형태로 단 하나의 애너테이션만 받더라도 아무런 에러가 나지 않는다.
- ex)
@ExceptionArrayTest(IndexOutOfBoundsException)
처럼 작성해도 문제없다.
- ex)
public class Sample3 {
@ExceptionArrayTest({
IndexOutOfBoundsException.class
, NullPointerException.class
})
public static void doublyBad() {
List<String> list = new ArrayList<>();
list.add(5, null);
}
}
- 두가지 예외를 인자로 주었다.
@Test
public void sample3ClassAnnotationTest() throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample3");
Method[] declaredMethods = testClass.getDeclaredMethods();
for (Method m : declaredMethods) {
if(m.isAnnotationPresent(ExceptionArrayTest.class)) {
tests++;
try {
m.invoke(null);
System.out.println(m + ", 테스트 실패 (예외를 던지지 않음)");
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionArrayTest.class).value(); // 애너테이션 매개변수의 값을 추출한다.
for (Class<? extends Throwable> excType : excTypes) {
if(excType.isInstance(exc)) {
System.out.printf("테스트 %s 성공: 기대한 예외 %s, 발생한 예외 %s%n", m, excType.getName(), exc);
passed++;
break;
}
}
if(oldPassed == passed) {
System.out.printf("테스트 %s 실패: 발생한 예외 %s%n", m, exc);
}
} catch (Exception exc) {
System.out.println("잘못 사용한 @ExceptionTest: " + m);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
InvocationTargetException
을 통해 잡힌 예외 중 배열로 준 예외에서 하나라도 일치하는 인스턴스가 있다면, 통과가 되는 것으로 만들었다.
테스트 public static void item39.Sample3.doublyBad() 성공: 기대한 예외 java.lang.IndexOutOfBoundsException, 발생한 예외 java.lang.IndexOutOfBoundsException: Index: 5, Size: 0
성공: 1, 실패: 0
- 5번째 인덱스가 초기화되지 않은 상태에서 값을 추가하여
IndexOutOfBoundsException
이 발생했다.
예제 실행 샘플 4: @Repeatable
을 이용하여 여러 개의 값을 받을 수 있는 애너테이션 만들기
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionRepeatableTest {
Class<? extends Throwable> value();
}
- 자바8부터
@Repeatable
애너테이션을 통해 반복되는 값을 받는다고 선언할 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
ExceptionRepeatableTest[] value();
}
- 이렇게 어떤 배열에 받아야 하는지에 대한 컨테이너가 필요하다.
public class Sample4 {
@ExceptionRepeatableTest(IndexOutOfBoundsException.class)
@ExceptionRepeatableTest(NullPointerException.class)
public static void doublyBad() {
List<String> list = new ArrayList<>();
list.add(5, null);
}
}
- 아까 배열의 예제와 동일한데,
@Repeatable
을 이용하기만 했다.
@Test
public void sample4ClassAnnotationTest() throws ClassNotFoundException {
int tests = 0;
int passed = 0;
Class<?> testClass = Class.forName("item39.Sample4");
Method[] declaredMethods = testClass.getDeclaredMethods();
for (Method m : declaredMethods) {
if(m.isAnnotationPresent(ExceptionRepeatableTest.class)
|| m.isAnnotationPresent(ExceptionTestContainer.class)) {
tests++;
try {
m.invoke(null);
System.out.println(m + ", 테스트 실패 (예외를 던지지 않음)");
} catch (InvocationTargetException wrappedExc) {
Throwable exc = wrappedExc.getCause();
int oldPassed = passed;
ExceptionRepeatableTest[] excTests = m.getAnnotationsByType(ExceptionRepeatableTest.class);// 애너테이션 매개변수의 값을 추출한다.
for (ExceptionRepeatableTest excTest : excTests) {
if(excTest.value().isInstance(exc)) {
System.out.printf("테스트 %s 성공: 기대한 예외 %s, 발생한 예외 %s%n", m, excTest.value().getName(), exc);
passed++;
break;
}
}
if(oldPassed == passed) {
System.out.printf("테스트 %s 실패: 발생한 예외 %s%n", m, exc);
}
} catch (Exception exc) {
System.out.println("잘못 사용한 @ExceptionTest: " + m);
}
}
}
System.out.printf("성공: %d, 실패: %d%n", passed, tests - passed);
}
- 반복 가능 애너테이션의 경우 모든 애너테이션을 확인하려면 조건문을 위와 같이 작성해야 한다.
m.isAnnotationPresent(ExceptionRepeatableTest.class) || m.isAnnotationPresent(ExceptionTestContainer.class)
- 가독성이 조금 더 좋아진다.
핵심 정리
- 애너테이션으로 할 수 있는 일을 굳이 명명패턴으로 처리하지 말자
- 자바 프로그래머라면 애너테이션 타입들을 잘 사용하도록 노력해보자
반응형
'Java > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바, 쉽게 정리하기 - item 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라 (0) | 2022.05.24 |
---|---|
이펙티브 자바, 쉽게 정리하기 - item 40. @Override 애너테이션을 일관되게 사용하라 (0) | 2022.05.24 |
이펙티브 자바, 쉽게 정리하기 - item 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라 (0) | 2022.02.24 |
이펙티브 자바, 쉽게 정리하기 - item 37. ordinal 인덱싱 대신 EnumMap을 사용하라 (0) | 2022.02.24 |
이펙티브 자바, 쉽게 정리하기 - item 36. 비트 필드 대신 EnumSet을 사용하라 (0) | 2022.02.24 |