애너테이션

문지은·2022년 10월 27일
0

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

이 장에서는 실제로 명명 패턴보다 애너테이션을 사용하는 것을 추천한다기 보다는 (최근에는 명명 패턴을 거의 안 쓰기 때문에) 애너테이션이 어떻게 나오게 된 건지를 설명하는 장이라고 생각하고 보면 편할 것 같다.

명명 패턴이 뭘까?

전통적인 프로그래밍에서 특별히 다뤄야할 프로그램 요소에는 구분되는 명명 패턴을 사용해왔다.
ex. JUnit 3 에서는 모든 테스트 메서드 이름이 test로 시작해야 했다.

여기에는 3가지 문제점이 존재한다.

  1. 오타가 나면 안된다.
  2. 올바른 프로그램 요소에만 사용된다는 보장이 없다.
    사용자가 실수로 메서드가 아닌 클래스 이름을 test로 시작하게 했을 때 테스트는 수행되지 않고 어떠한 에러도 뜨지 않는다.
  3. 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다.
    특정 예외를 던져야 성공하는 테스트가 있다고 할 때 예외 타입을 전달할 수가 없다.

애너테이션을 사용하면 이런 문제를 해결할 수 있다!

JUnit4도 적극 도입했다.

// marker annotation

// 메타 애너테이션들
// 런타임에도 유지
@Retention(RetentionPolicy.RUNTIME)
// 메서드 선언에만 사용
@Target(ElementType.METHOD)
public @interface Test {
}

// 실제 사용
public class Sample {
    @Test
    public static void m1() { }        // Test should pass
    public static void m2() { }
    @Test public static void m3() {    // Test should fail
        throw new RuntimeException("Boom");
    }
    public static void m4() { }  // Not a test
    @Test public void m5() { }   // INVALID USE: nonstatic method
    public static void m6() { }
    @Test public static void m7() {    // Test should fail
        throw new RuntimeException("Crash");
    }
    public static void m8() { }
}

@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 + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }
        }
        System.out.printf("Passed: %d, Failed: %d%n",
                passed, tests - passed);
    }
}

이제 명명 패턴의 두번째 단점이었던 매개변수를 가질 수 없다는 점을 애너테이션은 어떻게 해결하고 있는지 확인해보자.

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
		// 이 애너테이션은 Throwable을 확장한 클래스의 객체만 매개변수로 가진다.
    Class<? extends Throwable> value();
}

// 실제 사용

public class Sample2 {
    @ExceptionTest(ArithmeticException.class)
    public static void m1() {  // Test should pass
        int i = 0;
        i = i / i;
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m2() {  // Should fail (wrong exception)
        int[] a = new int[0];
        int i = a[1];
    }
    @ExceptionTest(ArithmeticException.class)
    public static void m3() { }  // Should fail (no exception)
}

// 애너테이션 동작 원리

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 + " failed: " + exc);
                } catch (Exception exc) {
                    System.out.println("Invalid @Test: " + m);
                }
            }

						// 새로운 애너테이션 타입 추가
            if (m.isAnnotationPresent(ExceptionTest.class)) {
                tests++;
                try {
                    m.invoke(null);
                    System.out.printf("Test %s failed: no exception%n", m);
                } catch (InvocationTargetException wrappedEx) {
										// 에러를 받아서 해당 에러가 맞으면 pass
                    Throwable exc = wrappedEx.getCause();
                    Class<? extends Throwable> excType =
                            m.getAnnotation(ExceptionTest.class).value();
                    if (excType.isInstance(exc)) {
                        passed++;
                    } else {
                        System.out.printf(
                                "Test %s failed: expected %s, got %s%n",
                                m, excType.getName(), exc);
                    }
                } catch (Exception exc) {
                    System.out.println("Invalid @ExceptionTest: " + m);
                }
            }
        }

        System.out.printf("Passed: %d, Failed: %d%n",
                passed, tests - passed);
    }
}

여러 매개 변수를 받고 싶으면 배열을 이용하면 된다.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
		// 배열 이용
    Class<? extends Exception>[] value();
}

배열 대신에 여러개의 애너테이션을 달고 싶다면?

		// 그냥 애너테이션		

		@Retention(RetentionPolicy.RUNTIME)
		@Target(ElementType.METHOD)
		public @interface ExceptionTestContainer {
		    ExceptionTest[] value();
		}		

		// 반복 애너테이션

		@Retention(RetentionPolicy.RUNTIME)
		@Target(ElementType.METHOD)
		// 반복 가능하게 해줌
		@Repeatable(ExceptionTestContainer.class)
		public @interface ExceptionTest {
		    Class<? extends Throwable> value();
		}

		// 사용 예시
		@ExceptionTest(IndexOutOfBoundsException.class)
    @ExceptionTest(NullPointerException.class)
    public static void doublyBad() {
        List<String> list = new ArrayList<>();

        // The spec permits this staticfactory to throw either
        // IndexOutOfBoundsException or NullPointerException
        list.addAll(5, null);
    }

이 때는 애너테이션을 한 개만 달았을 때와 달리 해당 컨테이너 애너테이션 타입이 적용된다.

profile
백엔드 개발자입니다.

0개의 댓글