Effective Java : 아이템 39. 명명 패턴보다 어노테이션을 사용하라

    Effective Java : 아이템 39. 명명 패턴보다 어노테이션을 사용하라

    • 명명 패턴은 test_a, test_b 처럼 특정한 이름을 규칙을 가진 녀석들이 특정한 작업을 하도록 정의하는 패턴임.
    • 명명 패턴은 다음 단점이 있음.
      • 이름을 잘못 쳐도 컴파일 에러가 나지 않음. test_a → tste_a로 친 경우
      • 명명 패턴에 필요한 매개변수를 전달할 수 없음. test_ThrowableException이라고 쳤을 때, 컴파일러는 ThrowableExeption이라는 글자가 무엇을 의미하는지 알 수 없음. 
    • 어노테이션을 사용하면 명명패턴의 단점을 극복할 수 있음. 
      • 명명패턴은 컴파일 에러가 발생하지 않으나, 어노테이션은 잘못 사용되었을 경우 컴파일 에러가 발생되므로 단단한 코드를 작성할 수 있음. 
    • 어노테이션은 @Retention (보존 기간) / @Target (적용 범위)을 이용해서 정의할 수 있음.
      • Retention.SOURCE : 컴파일 시점에만 보존
      • Retention.CLASS : 컴파일 이후 클래스 파일에는 남으나 런타임에 JVM이 읽을 수 없음.
      • Retention.RUNTIME: 런타임까지 보존되고 JVM이 리플렉션으로 읽을 수 있음.

    명명 패턴이란?

    명명 패턴은 특정 이름을 가진 것들을 모아서 비슷한 동작을 하는 것들이다. 한 가지 예로는 이런 테스트 코드가 있을 것이다. 

    public void test_doSomething();
    public void test_doExecute();
    public void test_doWrapping();

    위 코드는 test를 Prefix로 가지는 명명 패턴이다. test를 Prefix로 가지는 모든 메서드는 '테스트' 되어야 한다는 것을 의미한다. 이런 테스트의 처리는 'test' Prefix를 인식하고 테스트 하는 또 다른 테스트 프레임워크를 통해서 진행될 것이다. 그렇지만 이런 명명 패턴에는 문제가 있다.

    • test → tesftw라고 쳐도 어떠한 일도 일어나지 않음. 
    • 필요한 매개변수도 테스트 프레임워크에 넘길 수 없음. 
    • 메서드 이름으로 매개변수를 넘긴다 하더라도, 컴파일 시점에 그것이 잘못된 코드인지 알 수 없음. 

    결론적으로 명명 패턴을 사용하는 것은 굉장히 깨지기 쉬운 타입이라는 것을 의미한다. 따라서 명명 패턴을 사용하는 대신에 다른 방법을 이용해서 코드를 작성해야 한다.


    어노테이션 선언

    자바에서는 어노테이션을 지원하고, 어노테이션은 명명 패턴보다 좀 더 유연하게 사용할 수 있는 좋은 방법이다. 

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface test {
    
    }

    어노테이션은 일반적으로 위와 같이 선언한다. 

    • Retention(보존 기간) : 어노테이션이 유지되는 기간을 의미
    • Target : 어노테이션을 붙일 수 있는 곳을 의미

    위의 어노테이션은 런타임까지 유지되면, 메서드 타입에만 붙일 수 있는 어노테이션을 의미한다. Retention 타입은 총 3가지가 존재하는데 각각 다음 의미를 가진다.

    • SOURCE는 소스코드 단계에서만 유지됩니다.
    • CLASS는 컴파일 후의 바이트코드에도 유지되지만 런타임에는 접근할 수 없습니다.
    • RUNTIME은 컴파일 후에도 유지되며, 런타임에 접근하여 정보를 읽을 수 있습니다.

    우선 이렇게 어노테이션을 만들어서 붙여도, 어노테이션 그 자체는 어떠한 동작도 하지 않는다. 어노테이션이 런타임에서 유의미한 동작을 하게 하려면 리플렉션 API를 이용해야 한다. 


    명명 패턴을 어노테이션 기반 테스트 프레임워크로 바꾸기

    위와 같이 test를 prefix로 가지는 메서드를 테스트 하는 테스트 프레임워크가 있다고 가정해보자. 이런 것들은 명명패턴으로 사용하는 대신에 어노테이션을 사용하는 것이 더 좋다. 

    아래 예시로 공부해보자. 아래 예시에서는 @test 어노테이션이 붙은 3개의 메서드가 있다. 각 메서드는 @test 어노테이션 내부에 발생해야 하는 예외를 명시했다. 이 예외가 발생해야 테스트가 성공하는 것이다. 그런데 앞서 이야기 한것처럼 어노테이션을 붙인 것은 단순히 마킹만 하는 것이지 실제로 어떤 동작이 일어나지는 않는다.

    public class SampleTest {
        @test(ArithmeticException.class)
        public static void m1(){
            int i = 0;
            i = i / i; // 성공
        }
        @test(ArithmeticException.class)
        public static void m2(){
            int[] a= new int[0];
            int i = a[1]; // 다른 예외 발생
        }
        // 실패
        @test(ArithmeticException.class)
        public static void m4(){}
    }

    이제 @test 어노테이션을 인식하고 동작하는 코드를 작성해야한다. 런타임까지 Retention 되는 어노테이션은 리플렉션 API를 이용해서 읽고 사용할 수 있다. 먼저 아래와 같이 어노테이션에서 매개변수를 받아서 사용할 수 있도록 어노테이션을 수정한다.

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

    아래에는 어노테이션이 있는 메서드를 구분해서 처리하는 코드를 작성한다.

    1. 리플렉션을 이용해 클래스 내에 선언된 메서드를 모두 조회한다.
    2. 메서드 중에서 isAnnotationPresent() 메서드를 이용해 특정 어노테이션이 있는 메서드들만 추린다.
    3. 해당 메서드들을 invoke() 메서드를 이용해 실행한다. 
    4. 메서드 실행 시 발생한 invoke Exception을 잡아서 어노테이션에 명시한 '기대한 에러'와 같은지 확인한 후 성공 / 실패 갯수를 카운트한다. 
    public class RunTests {
    
        public static void execute(String className) throws ClassNotFoundException {
            int tests = 0;
            int passed = 0;
            Class<?> testClass = Class.forName(className);
    
            Method[] declaredMethods = testClass.getDeclaredMethods();
    
            List<Method> filteredMethods = Arrays.stream(declaredMethods)
                    .filter(method -> method.isAnnotationPresent(test.class))
                    .toList();
    
            for (Method declaredMethod : filteredMethods) {
                    try {
                        tests ++;
                        declaredMethod.invoke(null);
                    } catch (InvocationTargetException e) {
    
                        Throwable targetException = e.getTargetException();
                        Class<? extends Throwable> expectedType = declaredMethod
                                .getAnnotation(test.class)
                                .value();
    
                        if (expectedType.isInstance(targetException)) {
                            passed++;
                        } else {
                            System.out.printf("테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n",
                                    declaredMethod, expectedType, targetException);
                        }
                    } catch (Exception e) {
                        System.out.println("잘못 사용한 @test:" + declaredMethod);
                    }
                }
            System.out.printf("성공: %d, 실패: %d", passed, tests - passed);
        }
    }

    어노테이션을 인식해서 테스트를 해주는 간단한 테스트 프레임워크는 위와 같은 방식으로 작성할 수 있다. 그리고 테스트 프레임워크는 아래와 같이 실행하면 된다.

    public static void main(String[] args) throws ClassNotFoundException {
        String name = SampleTest.class.getName();
        RunTests.execute(name);
    }

    아주 간단히 구현해 볼 수 있다. 

     


    명명 패턴 대신 어노테이션을 쓰자! 

    위에서 볼 수 있듯이 명명 패턴으로 할 수 있는 것은 어노테이션을 만들어서 처리할 수도 있다. 어노테이션으로 처리하면 앞서 이야기 했던 명명패턴의 단점을 모두 해결할 수 있게 된다.

    • test → tesftw라고 쳐도 어떠한 일도 일어나지 않음. 
      • @test → @testftw로 잘못 치면 컴파일 에러 발생.
    • 필요한 매개변수도 테스트 프레임워크에 넘길 수 없음. 
      • 어노테이션에 매개변수 전달 가능.
    • 메서드 이름으로 매개변수를 넘긴다 하더라도, 컴파일 시점에 그것이 잘못된 코드인지 알 수 없음. 
      • 전달된 매개변수는 어노테이션 / 메서드에 전달 가능하고, 이 매개변수는 컴파일 시점에 잘못되었는지 확인할 수 있음.

    댓글

    Designed by JB FACTORY