Search
Duplicate

아이템 39 - 명명 패턴보다 애너테이션을 사용하라

작성자
챕터
6장 - 열거 타입과 애너테이션
최종 편집
2023/07/29 05:36
생성 시각
2023/07/28 11:19

개요

전통적으로 Junit과 같은 프레임워크가 다루는 프로그램 요소에는 정확히 구분되는 명명 패턴을 적용해왔다. 하지만, 명명 패턴이 가지고 있는 대부분의 문제들을 애너테이션으로 해결할 수 있다.

명명 패턴의 문제

오타가 나면 안된다.
올바른 (기존에 프레임워크가 의도한) 프로그램 요소에서만 사용되리라 보증할 수 없다.
프로그램 요소를 매개변수로 전달할 방법이 없다.

개념 및 용어

meta annotation
애너테이션 선언에 다는 애너테이션
marker annotation
매개변수 없이 단순히 target이 되는 프로그램 요소에 marking하는 애너테이션

예제코드

@Retention(RetentionPolicy.RUNTIME) // meta annotation (보존 정책) @Target(ElementType.METHOD) // meta annotation (적용 대상) public @interface Test { // marking annotation }
JavaScript
복사
Test 애너테이션을 정의하였다.
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) { // @Test 애너테이션을 잘못된 곳에 사용해서 발생한 예외(기대했던 예외와 다른) System.out.println("잘못 사용한 @Test: " + m); } } } System.out.printf("성공: %d, 실패 : %d%n", passed, tests - passed); } }
JavaScript
복사
위의 Runtests는 명령줄 인수로 클래스 이름을 받아서 @Test 애너테이션이 달린 메서드를 차례대로 호출한다.
// 명시한 예외가 발생해야 성공하는 테스트 메서드용 애너테이션 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable> value(); }
JavaScript
복사
ExceptionTest 애너테이션의 매개변수 타입은 Class<? extends Throwable>로, Throwable을 확장한 클래스의 Class 객체라는 의미이다. 따라서, 모든 예외와 오류 타입을 수용한다. (모든 예외와 오류 객체들은 Throwable 클래스를 상속하기 때문)
public class Sample2 { @ExceptionTest(ArithmethicException.class) public static void m1() { // 성공 int i = 0; i = i / i; } @ExceptionTest(ArithmethicException.class) public static void m2() { // 실패 (다른 예외 발생) int[] a = new int[0]; int i = a[1]; } @ExceptionTest(ArithmethicException.class) public static void m3() { } // 실패 (예외 발생 안함) }
JavaScript
복사
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.println("테스트 실패: 예외 발생 안함"); } catch (InvocationTargetException wrappedExc) { Throwable exc = wrappedExc.getCause(); Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value(); if (excType.isInstance(exc)) { passed++; } else { System.out.println("테스트 실패: 기대한 예외와 다른 예외 발생"); } } catch (Exception exc) { System.out.println("잘못 사용한 경우"); } } } System.out.printf("성공: %d, 실패 : %d%n", passed, tests - passed); } }
JavaScript
복사
@Test 애너테이션과 비슷하게 동작하지만, 애너테이션 매개변수의 값을 추출하여 테스트 메서드가 기대한 예외를 던지는지 확인한다.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionTest { Class<? extends Throwable>[] value(); }
JavaScript
복사
위와 같이, 여러 개의 예외를 매개변수를 배열로 받는 애너테이션을 만들수도 있다.
위의 방식을 Repeatable 메타 애너테이션으로도 구현할 수 있다.
Repeatable 메타 애너테이션이 붙은 애너테이션은 하나의 프로그램 요소에 여러 번 달 수 있다.

내용

애너테이션은 애너테이션을 다는 프로그램 요소(클래스, 메서드 등)의 의미는 그대로 두고, 그 애너테이션에 관심있는 프로그램(ex. Junit과 같은 프레임워크)에게 추가 정보를 제공한다.
정의하고자 하는 애너테이션이 특정 프로그램 요소에만 쓰이도록 하고 싶다면 (ex. 정적 메서드 전용) 이러한 기능을 하는 애너테이션 처리기를 구현해야 한다.
javax.annotation.processing API (Reference)

결론

애너테이션으로 할 수 있는 일을 명명패턴으로 처리할 이유는 단 한가지도 없다.

참고자료