3 minute read

명명 패턴

전통적으로 도구나 프레임워크가 특별히 다루어야 하는 프로그램 요소에는 명명 패턴을 사용해 왔다. JUnit 3까지는 테스트 메서드 이름을 test로 시작하게 하는 식이다.

이 방법은 효과적으로 보일 수 있으나…

  1. 오타가 나면 안된다.
  2. 올바른 프로그램 요소에서만 사용되리라 보증할 수 없다.
    • 메서드가 아니라, 클래스 이름을 Test로 시작하게 하면 테스트가 수행되지 않을 것이다.
  3. 프로그램 요소를 매개변수로 전달할 방법이 없다.
    • 특정 예외를 던져야 성공하는 테스트가 존재할 때, 기대하는 예외 타입을 테스트에 매개변수로 전달할 방법이 없다.

애너테이션

애너테이션은 앞서 언급되었던 명명 패턴의 단점을 해결할 수 있다.

마커(marker) 애너테이션

다음은 아무런 메서드를 선언하지 않은 인터페이스로, 타입체크를 하기위해 사용하는 마커 애너테이션을 선언하는 코드이다.

/** 
* 테스트 메서드임을 선언하는 애너테이션이다. 
* 매개변수 없는 정적 메서드 전용이다. */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {}

@Test 애너테이션 타입 선언에도 두 가지 다른 애너테이션이 달려 있는데, 이와 같이 애너테이션 선언에 다는 애너테이션을 메타 애너테이션이라 한다.

  • @Retention(RetentionPolicy.RUNTIME) : @Test가 런타임에도 유지되어야 한다.

RetentionPolicy class 내부는 다음과 같이 되어 있고, 다양한 설정을 제공한다.

public enum RetentionPolicy {  
    /**  
     * Annotations are to be discarded by the compiler.     */    SOURCE,  
  
    /**  
     * Annotations are to be recorded in the class file by the compiler     * but need not be retained by the VM at run time.  This is the default     * behavior.     */    CLASS,  
  
    /**  
     * Annotations are to be recorded in the class file by the compiler and     * retained by the VM at run time, so they may be read reflectively.     *     * @see java.lang.reflect.AnnotatedElement  
     */    RUNTIME  
}
  • @Target(ElementType.METHOD) : @Test가 메서드 선언에만 사용할 것을 명시한다.
public enum ElementType {  
    /** Class, interface (including annotation interface), enum, or record  
     * declaration */    TYPE,  
  
    /** Field declaration (includes enum constants) */  
    FIELD,  
  
    /** Method declaration */  
    METHOD,  
  
    /** Formal parameter declaration */  
    PARAMETER,  
  
    /** Constructor declaration */  
    CONSTRUCTOR,  
  
    /** Local variable declaration */  
    LOCAL_VARIABLE,  
  
    /** Annotation interface declaration (Formerly known as an annotation type.) */  
    ANNOTATION_TYPE,  
  
    /** Package declaration */  
    PACKAGE,  
  
    /**  
     * Type parameter declaration     *     * @since 1.8  
     */    TYPE_PARAMETER,  
  
    /**  
     * Use of a type     *     * @since 1.8  
     */    TYPE_USE,  
  
    /**  
     * Module declaration.     *     * @since 9  
     */    MODULE,  
  
    /**  
     * Record component     *     * @jls 8.10.3 Record Members  
     * @jls 9.7.4 Where Annotations May Appear  
     *     * @since 16  
     */    RECORD_COMPONENT;  
}

다음은 선언한 마커 애너테이션을 사용한 프로그램 예시이다. @Test 애너테이션이 Sample에 직접 영향을 끼치지는 않고, 이 애너테이션에 관심 있는 프로그램에게 추가 정보를 제공하고 처리가 가능하게 한다.

public class Sample {  
    @Test  
    public static void m1() {}  // 성공
    public static void m2() {}  
    @Test  
    public static void m3() {  
        throw new RuntimeException("Fail");  // 실패
    }  
    public static void m4() {}  
    @Test  
    public void m5() {}  // 정적 메서드가 아니므로 잘못 사용한 예시이다.
    public static void m6() {}  
      
}

매개변수 하나를 받는 애너테이션

/**  
* 명시한 예외를 던져야만, 성공하는 테스트케이스 애너테이션  
*/  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
public @interface ExceptionTest{  
    // 한정적 와일드카드를 통해 Throwable을 상속한 모든 타입을 지정한다.
    Class<? extends Throwable> values();   
}

위와 같이 선언한 애너테이션은 다음과 같이 사용할 수 있다.

public class Sample2 {  
    @ExceptionTest(ArithmeticException.class)  
    public static void m1() {  
        int i = 0;  
        i = i / i; // divide by zero. ArithmeticException 예외를 발생시킴 -> 성공  
    }  
    @ExceptionTest(ArithmeticException.class)  
    public static void m2() {  
        int[] a = new int[0];  
        int i = a[1]; // IndexOutOfBoundsException 발생 -> ArithmeticException가 아니므로 실패한다.
    }  
    @ExceptionTest(ArithmeticException.class)  
    public static void m3() {} // 아무 Exception도 발생하지 않음 -> 실패  
}

여러 매개변수를 받는 애너테이션

배열을 이용한 구현

여러 매개변수를 받아 처리하는 것도 가능하며, 다음은 배열을 통해 구현한 방식이다.

/**  
* 명시한 예외를 던져야만, 성공하는 테스트케이스 애너테이션  
*/  
@Retention(RetentionPolicy.RUNTIME)  
@Target(ElementType.METHOD)  
public @interface ExceptionTest{  
    //한정적 와일드카드를 통해 Throwable을 상속한 모든 타입을 지정  
    Class<? extends Throwable>[] values();   
}

public class Sample3 {  
    @ExceptionTest({IndexOutOfBoundsException.class,  
                    NullPointerException.class})  
    public static void m1() {  
        List<String> list = new ArrayList<>();  
        //자바 명세에 따르면, 다음 메서드는 IndexOutOfBoundsException이나,  
        //NullPointerException을 던질 수 있다.  
        //예외 발생 시 성공  
        list.addAll(5, null);  
    }  
}

@Repeatable 애너테이션

Java 8에서는 @Repeatable 애너테이션을 사용할 수도 있다. 이를 사용할 경우, 주의사항이 몇가지 있다.

  1. @Repeatable을 단 애너테이션을 반환하는 컨테이너 애너테이션을 하나 더 정의한다.
  2. @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
  3. 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의한다.
  4. 컨테이너 애너테이션에는 @Retention@Target을 적절히 명시한다. (그렇지 않으면 컴파일되지 않을 것이다.)

@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() {...}

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

  • getAnnotationByType 메서드는 이 둘을 구분하지 않아 @ExceptionTest@ExceptionTestContainer를 모두 가져온다.
  • isAnnotationPresent는 둘을 구분한다.
    • 만약 @ExceptionTest를 여러번 단 다음, isAnnotationPresentExceptionTest를 검사하면 false가 나온다.
      (@ExceptionTestContainer로 인식하기 때문)
    • 반대로 @ExceptionTest를 한번 만 단 다음, isAnnotationPresentExceptionTestContainer를 검사하면 false가 나온다.
      (@ExceptionTest가 적용되었기 때문)

때문에 모두 검사하려면 두 가지 메서드 모두 확인해야 한다.

Leave a comment