티스토리 뷰

반응형

명명 패턴

전통적으로 도구나 프레임워크가 특별히 다뤄야할 프로그램 요소에는 딱 구분되는 명명패턴을 적용해왔다.

ex) JUnit3의 테스트 메서드 이름을 test로 시작하게함

//JUnit3 : 메서드 이름을 test로 시작해야함
public class helloTest extends TestCase {
    public void testHello(){
         String hello = "hello"
    }
}

//JUnit4 : @Test 애너테이션을 사용해 테스트 
public class helloTest  {
    @Test
    public void testHello(){
         String hello = "hello"
    }
}

 

 

명명 패턴의 단점

  • 오타가 나면 안됨
  • 올바른 프로그램 요소에서만 사용될 보증 없음
  • 프로그램 요소를 매개변수로 전달할 방법 없음
    ex) 특정 예외를 던져야만 성공하는 테스트가 있다면, 기대하는 예외타입을 테스트에 매개변수로 전달해야한다. 예외의 이름을 테스트 메서드 이름에 덧붙일 수도 있지만 가독성에 좋지 않다. 컴파일러는 메서드 이름의 덧붙인 예외가 예외를 가리키는지도 모른다.

→ 이런 단점들을 모두 해결해주는게 애너테이션이다.

 

 

마커 애너테이션 타입 선언

아무 매개변수 없이 단순히 대상에 마킹한다는 뜻에서 마커 애너테이션이라 한다.

//매개변수 없는 정적 메서드 전용

@Retention(RetentionPolicy.RUNTIME)  //런타임에도 유지된다
@Target(ElementType.METHOD)  //메서드 선언에만 이 에너테이션 사용 가능
public @interface Test {
}
  • @Retention : 애너테이션의 스코프를 결정
  • @Target : 애너테이션이 적용될 대상 결정

위 두 애너테이션을 메타 애너테이션이라 한다.

애너테이션의 선언에 다는 애너테이션이다.

명명패턴과 다르게 애너테이션을 사용하면 오타를 내거나 메서드 선언 외의 프로그램 요소에 달면 컴파일 오류를 내준다.

 

 

마커 애너테이션 사용 예시

public class Sample {
    @Test public static void m1(){}  //성공해야함

    public static void m2(){}

    @Test public static void m3(){  //실패해야함 (약속되지 않은 예외 던짐)
        throw new RuntimeException("실패");
    }

    @Test public void m4() { }  //잘못 사용한 예 : 정적 메서드가 아니다.
}

@Test 를 붙이지 않은 나머지 메서드는 테스트 도구가 무시할 것이다.

@Test 가 Sample 클래스의 의미에 직접적인 영향을 주진 않음

애너테이션은 추가 정보를 제공할뿐

 

 

애너테이션 처리 프로그램 예시

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)) {    //Test 애너테이션이 선언된 메서드만을 호출한다.
                tests++;
                try {
                    m.invoke(null);
                    passed++;
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    System.out.println(m + " 실패 : " + exc);
                } catch (Exception e) {
                    System.out.println("잘못 사용한 테스트 @Test : " + m);
                }
            }
        }
        System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
    }
}

정규화된 클래스 이름을 받아 그 클래스에서 @Test 애너테이션이 달린 메서드를 차례로 호출하는 코드이다.

 

 

매개변수를 받는 애너테이션 타입 선언

//명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션 

@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 = 1 / 0;
    }

    //명시한 예외가 아닌 다른 예외를 던지기 때문에 테스트 실패
    @ExceptionTest(ArithmeticException.class)
    public static void m2() {
        int[] arr = new int[0];
        arr[1] = 1;     // outOfIndex 예외 발생
    }

    //예외를 던지지 않으므로 테스트 실패 
    @ExceptionTest(ArithmeticException.class)
    public static void m3() {}
}

 

 

매개변수가 있는 애너테이션 처리 프로그램 예시

public class RunExceptionTests {
    public static void main(String[] args) throws Exception{
        int tests = 0;
        int passed = 0;
        Class<?> testClass = Class.forName("example.Sample2");
        for (Method m : testClass.getDeclaredMethods()) {
            if (m.isAnnotationPresent(ExceptionTest.class)) {    //ExceptionTest 애너테이션을 사용한 메서드 선별
                tests++;
                try {
                    m.invoke(null);
                } catch (InvocationTargetException wrappedExc) {
                    Throwable exc = wrappedExc.getCause();
                    Class<? extends Throwable> excType = m.getAnnotation(ExceptionTest.class).value();    // 애너테이션의 매개변수 타입 확인
                    if (excType.isInstance(exc)) {    // 애너테이션의 매개변수 타입과 같을 경우 통과
                        passed++;
                    }
                } catch (Exception e) {
                    System.out.println("잘못 사용한 테스트 @Test : " + m);
                }
            }
        }
        System.out.printf("성공 : %d, 실패 : %d%n", passed, tests - passed);
    }
}

 

 

배열 매개변수를 받는 애너테이션 타입 선언

예외를 여러개 명시하고 그중 하나가 발생하면 성공하게 만들 수도 있다.

애너테이션의 매개변수 타입을 Class 객체의 배열로 선언하자.

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

 

 

배열 매개변수를 받는 애너테이션 사용 예시

@ExceptionTest({IndexOutOfBoundsException.class, NullPointerException.class})
public static void doublyBad() {  //성공해야함
    List<String> list = new ArrayList<>();

    //IndexOutOfBoundsException 이나 NullPointerException 발생 가능
    list.addAll(5, null);
}

 

 

배열 매개변수를 받는 애너테이션 처리 프로그램 예시

if (m.isAnnotationPresent(ExceptionTest.class)) {    //ExceptionTest 애너테이션을 사용한 메서드 선별
    tests++;
    try {
        m.invoke(null);
        System.out.println("테스트 %s 실패 : 예외를 던지지 않음\n", m);
    } catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        Class<? extends Throwable>[] excTypes = m.getAnnotation(ExceptionTest.class).value();    // 애너테이션의 매개변수 타입 확인
        for(Class<? extends Thorwable> excType : excTypes) {  //애너테이션의 배열 매개변수를 돈다.
           if (excType.isInstance(exc)) {    // 애너테이션의 매개변수 타입과 같을 경우 통과
              passed++;
              break;
           }
        }
        if(passed == oldPassed)
           System.out.printf("테스트 %s 실패 : %s %n", m, exc);
    } 
}

 

 

반복 가능한 애너테이션 타입 선언

자바8에서는 여러개 값을 받는 애너테이션을 다른 방식으로도 만들 수 있다.

배열 매개변수를 사용하는 대신 애너테이션에 @Repeatable 메타애너테이션을 다는 방식

주의할 점이 있다.

  • @Repeatable을 단 애너테이션을 반환하는 컨테이너 애너테이션을 하나 더 정의하고 @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야함
  • 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야함
  • 컨테이너 애너테이션 타입에는 적절한 보존정책(@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 doublyBad() { }

 

 

반복 가능 애너테이션 처리 프로그램 예시

//반복 가능 애너테이션과 그 컨테이너 애너테이션을 모두 따로 확인해야함 
if (m.isAnnotationPresent(ExceptionTest.class)
            || m.isAnnotationPresent(ExceptionTestContainer.class)) {
    tests++;
    try {
        m.invoke(null);
        System.out.println("테스트 %s 실패 : 예외를 던지지 않음\n", m);
    } catch (Throwable wrappedExc) {
        Throwable exc = wrappedExc.getCause();
        int oldPassed = passed;
        ExceptionTest[] excTests = m.getAnnotationsByType(ExceptionTest.class);
        for(ExceptionTest excTest : excTests) {
           if (excTest.value().isInstance(exc)) {    // 애너테이션의 매개변수 타입과 같을 경우 통과
              passed++;
              break;
           }
        }
        if(passed == oldPassed)
           System.out.printf("테스트 %s 실패 : %s %n", m, exc);
    } 
}
  • 반복 가능 애너테이션은 처리할때 주의를 요함
  • 반복 가능 애너테이션을 여러개 달면 하나만 달았을때와 구분하기 위해 해당 컨테이너 애너테이션 타입이 적용된다.
  • getAnnotationsByType 메서드는 둘을 구분하지 않아서 반복 가능 애너테이션과 그 컨테이너 애너테이션을 모두 가져옴, 그러나 isAnnotationPresent 메서드는 둘을 명확히 구분
  • 그래서 반복 가능 애너테이션을 여러번 단 다음 isAnnotationPresent 메서드로 달렸는지 검사한다면 아니라고 함(컨테이너가 달려서)
  • isAnnotationPresent 메서드로 컨테이너 애너테이션이 달렸는지 검사한다면 반복 가능 애너테이션을 한번만 달은 메서드를 무시하고 지나침
  • 결론적으로 달려있는 수와 관계없이 둘을 따로 모두 검사해야함

 

결론

애너테이션이 명명패턴보다 낫다는 점을 보여주었다.

소스코드에 추가 정보를 제공하는 도구를 만드는 일을 한다면 적당한 애너테이션 타입을 정의하여 같이 제공하자.

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

자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용해야 한다.

반응형
댓글
반응형
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday