NMP-Study / EffectiveJava2022

Effective Java Study 2022
5 stars 0 forks source link

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

Closed okhee closed 2 years ago

mbyul commented 2 years ago

애너테이션은 명명패턴의 문제를 모두 해결해주는 개념

단점

  1. 오타가 나면 안 된다.
    • 실수로 이름을 tset~라고 지으면, JUnit 3은 이 메서드를 무시하고 지나침
    • image
  1. 올바른 프로그램 요소에서만 사용되리란 보증이 없다.

    • (메서드가 아닌) 클래스명을 TestSafetyMechanisms로 지어 JUnit에 던져줘도, JUnit은 무시
  2. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.

    • (특정 예외를 던져야만 성공하는 테스트가 있다고 가정) 기대하는 예외 타입을 테스트에 매개변수로 전달해도, 컴파일러는 예외를 가리키는지 알 리가 없음

JUnit은 버전 4에서 애너테이션 전면 도입


1. 마커 애너테이션 (매개변수가 없는)

39-1 마커(marker) 애너테이션 타입 선언

/**
* 테스트 메서드임을 선언하는 애너테이션
* 매개변수 없는 정적메서드 전용
*/
@Retention(RetentionPolicy.RUNTIME) // 메타애너테이션, @CustomTest가 런타임에도 유지되어야 한다는 표시
@Target(ElementType.METHOD) // 메타애너테이션, @CustomTest가 메서드 선언에서만 사용돼야 한다는 표시
public @interface CustomTest{
}
  • @CustomTest애너테이션 실제 적용 모습 (적절한 애너테이션 처리기 없음) = 애너테이션을 아무 매개변수 없이 단순히 대상에 마킹(Marking)한다는 뜻에서 마커 애너테이션 (Marker Annotation)이라 부름
  • 애너테이션 타입에 다는 애너테이션을 메타 애너테이션 (Meta Annotation)이라 부른다
  • 메타 애너테이션의 종류
    
    @Documented: 문서에도 애너테이션 정보가 표현되게 함
    @Inherited: 자식클래스가 애너테이션을 상속받을 수 있게 함
    @Repeatable: 애너테이션을 반복적으로 사용할 수 있게 함
    @Retention(RetentionPolicy): 애너테이션의 범위를 지정
  • RetentionPolicy.RUNTIME: 컴파일 이후에도 JVM에 의해 참조가 가능
  • RetentionPolicy.CLASS: 컴파일러가 클래스를 참조 할때 까지 유효
  • RetentionPolicy.SOURCE: 애너테이션 정보가 컴파일 이후 사라짐

@Target(ElementType[]): 애너테이션이 적용될 위치를 선언

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);
    }
}

/**

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도 발생하지 않음 -> 실패
}

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

/ ExceptionRepeatableContainer.java / // 컨테이너 애너테이션 @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface ExceptionRepeatableContainer { ExceptionRepeatableTest[] 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);
    }
}

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

요약