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

신명철·2022년 3월 7일
0

Effective Java

목록 보기
37/80

명명 패턴

도구나 프레임워크가 특별히 다뤄야 하는 프로그램 요소에는 딱 구분되는 명명 패턴을 적용해 왔다. 예를 들어 테스트 프레임워크인 JUnit3 에서는 테스트 메서드 이름을 test로 시작하게끔 지어야 했었다.

하지만 이 경우 다음과 같은 문제가 있다.

  • 오타가 나면 안된다.
    • 실수로 이름을 tset~ 라고 지으면 그 테스트 메서드는 무시하고 지나가기 때문에 테스트 메서드가 제대로 실행됐는지 모른다.
  • 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다.
    • TestSafetyMechanisms를 JUnit에 던져줬다고 해보자. 개발자는 이 클래스에 정의된 테스트 메서드들을 수행하리라 기대하겠지만 JUnit은 클래스 이름에는 전혀 관심이 없다.
  • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
    • 특정 예외를 던져야 성공하는 테스트가 있을 대 기대하는 예외 타입을 매개 변수로 전달해야 하는 상황이다. 예외의 이름을 테스트 메서드 이름에 덧붙이는 방법도 있지만 보기에도 나쁘고 깨지기도 쉽다.

어노테이션

JUnit4 부터 어노테이션을 이용한 테스트 프레임워크 지원을 시작했다. 이는 위와 같은 단점들을 제거해주며 효과적으로 동작한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
  • @Retention(RetentionPolicy.RUNTIME)은 Runtime 에도 어노테이션이 유지되어야 한다는 의미이다.
  • @Target(ElementType.METHOD)@Test 어노테이션이 메서드에서만 선언되어야 한다는 의미이다.
public class Sample {
    @Test
    public static void m2() {}
    @Test
    public static void m3() {
        throw new RuntimeException("Failure");
    }
    public static void m4() {}
    @Test
    public void m5() {}
    public static void m6() {}
    @Test
    public static void my() {
        throw new RuntimeException("Failure");
    }
    public static void m8() {}
}
  • @Test 어노테이션이 Sample 클래스에 직접적인 영향을 주는 것은 아니다. 그저 이 어노테이션에 관심이 있는 프로그램들에게 추가 정보를 줄 뿐이다.
  • 즉, @Test 어노테이션에 관심이 있는 프로그램에게 특별한 처리를 할 기회를 준다.
public class RunTests {
    public static void main(String[] args) throws Exception {
        int tests = 0;
        int passed = 0;
       
        Class<?> testClass = Sample.class;

        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 + " Failure : " + exc);
                } catch (Exception exc) {
                    System.out.println("Wrong Useage @Test : " + m);
                }
            }
        }
        System.out.printf("Success: %d, Fauilure: %d%n", passed, tests - passed);

    }
}
  • 이 코드는 테스트가 예외를 던지면 리플렉션 매커니즘이 에외를 InvocationTargetException 으로 감싸서 다시 던진다.
  • InvocationTargetException을 잡아 원래 예외에 담긴 실패 정보를 추출해(getCause) 출력한다.

특정 예외를 던져야만 성공하는 테스트를 위한 어노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}
public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {
        int i = 0;
        i = i / 1;
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m2() {
        int[] a = new int[0];
        int i = a[1];
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m3() {

    }
}
Class<?> testClass = Sample2.class;

for (Method m : testClass.getDeclaredMethods()) {
    if (m.isAnnotationPresent(ExceptionTest.class)) {
        tests++;
        try {
                m.invoke(null);
                System.out.printf("Test Failed %s : Did not Throw Exception");
            } 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("Test %s Failed : Expected %s but %s%n",m, excType.getName(), exc);
            } catch (Exception exc) {
            System.out.println("Wrong Useage @ExceptionTest : " + m);
        }
    }
}
  • 이 어노테이션의 타입은 Throwable을 확장한 클래스의 Class 객체를 모두 받을 수 있다. 따라서 모든 예외 타입을 다 수용할 수 있다.

여러개의 예외를 받는 어노테이션

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
    Class<? extends Throwable>[] value();
}
public class Sample3 {
    @ExceptionTest({ArithmeticException.class, SomeOtherException.class})
    public static void m1() {
        int i = 0;
        i = i / 1;
    }
}
if(m.isAnnotaionPresent(ExceptionTest.class)) {
    test++;
    try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    } catch (InvocationTargetException wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();
        
        int oldPassed = passed;
        for(Class<? extends Throwable> excType : excTypes) {
            if(excType.isInstance(exc)) {
            	passed++;
                break;
            }
        }
        
        if(passed == oldPassed) {
            System.out.println("테스트 %s 실패: %s %n", m, exc);
        }
    } catch (Exception e) {
        System.out.println("잘못 사용한 @ExceptionTest: " + m);
    }
}
  • 배열로 예외 타입을 받아서 지정한 예외 클래스 중에 맞는 클래스가 있는지 확인한다.

반복 가능 어노테이션 @Repeatable

자바 8 에서는 다른 방식으로도 만들 수 있다. @Repeatable 메타 어노테이션을 달면 된다. 단 주의할 점이 있다.

첫째는 @Repeatable을 단 어노테이션을 반환하는 컨테이너 어노테이션을 하나 더 정의하고 @Repeatable에 이 컨테이너 어노테이션의 Class 객체를 매개변수로 전달해야 한다.

둘째는 컨테이너 어노테이션은 내부 어노테이션 타입의 배열을 반환하는 value 메소드를 정의해야 한다.

마지막으로, 컨테이너 어노테이션에는 @Retention@Targer을 명시해야 한다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest {
    Class<? extends Throwable> value();
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTestContainer {
    ExceptionTest[] value();
}
@ExceptionTest(IndexOutOfBoundsException.class)
@ExceptionTest(NullPointerException.class)
public static void m1() {...}

반복 가능 어노테이션은 처리할 때 주의가 필요하다. 반복 가능 어노테이션을 여러개 달면 하나만 달았을 때와 구분하기 위해 해당 컨테이너 어노테이션 타입이 적용된다.

try {
        m.invoke(null);
        System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
    } catch (InvocationTargetException wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
    
        ExceptionTest[] excTests = m.getAnnotationByType(ExceptionTest.class);
        for(ExceptionTest excType : excTypes) {
            if(excType.isInstance(exc)) {
            	passed++;
                break;
            }
        }
        
        if(passed == oldPassed) {
            System.out.println("테스트 %s 실패: %s %n", m, exc);
        }
}
  • getAnnotationByType 메서드는 이 둘을 구분하지 않아서 @ExceptionTest@ExceptionTestContainer를 모두 가져온다.
  • isAnnotationPresent는 둘을 구분한다.
    • 만약 @ExceptionTest를 여러번 단 다음, isAnnotationPresentExceptionTest를 검사하면 @ExceptionTestContainer로 인식하기 때문에 false가 나온다.
    • 반대로 @ExceptionTest를 한번만 단 다음에 isAnnotationPresentExceptionTestContainer를 검사하면 @ExceptionTest가 적용되었기 때문에 false가 나온다.

반복 가능 어노테이션을 사용한 경우에는 getAnnotationByType을 사용해 어노테이션 정보를 가져오는 것이 좋다.

profile
내 머릿속 지우개

0개의 댓글