JAVA/Effective Java

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

Garonguri 2022. 6. 3. 17:13
728x90

명명 패턴이 뭘까요?

메서드나 타입의 이름을 특정 규칙으로 짓고, 이 규칙을 지켜 만든 메소드나 타입 등에 추가적인 처리를 제공하는 것

ex) JUnit은 test method의 이름을 test로 시작하게끔 하는 것....

 

명명 패턴은 효과적이지만, 단점도 많다.

 

명명 패턴의 단점

  1. 오타
    • 메서드나 타입을 이름으로 구분하기 때문에, 오타가 나면 자칫 이 메서드는 무시된 채로 넘어갈 수 있다.
    • JUnit의 test method의 경우 이름 실수가 났을 경우 테스트의 성공/실패 자체를 알 수 없기 때문에, 성공된줄 알고 지나칠 수도 있다.
  2. 올바른 프로그램 요소에서만 사용될 것이라는 보증이 없음
    1. Junit에서는 테스트 메서드 이름을 test로 시작해야 할뿐, 클래스 이름은 이를 따를 필요가 없지만, 사용자가 클래스에 위 규칙을 적용해 테스트 메서드가 수행되길 기대할 수 있다.
    2. JUnit은 위 클래스를 무시할 것이고, 사용자는 어떠한 경고 메시지도 받지 못할 뿐더러 당연히 의도한 테스트도 수행할 수 없다.
  3. 프로그램 요소를 매개변수로 전달할 방법이 마땅치 않다.
    • 특정 예외를 던져야만 성공하는 테스트가 있고, 기대하는 예외 타입은 테스트에 매개변수로 전달해야 한다고 가정해보자.
      • 예외의 이름을 테스트 메서드 이름에 덧붙이는 방법은 가독성도 나쁘고 깨지기도 쉽다.
      • 컴파일러는 메서드 이름에 덧붙인 문자열이 예외인지 알 수 없다.
      • 테스트를 실행하기 전까지 존재조차 알 수 없다.

그럼 명명 패턴 말고 무엇을 사용해야 할까?

 

애너테이션을 사용하면 된다.

 

이해를 돕기 위해 테스트 프레임워크를 만들어봅시다.

// test method annotation
// 매개변수 없는 정적 메서드 전용
@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
public @interface Test {
}

@Rentation(RetentionPolicy.RUNTIME)

  • 위 메타 애너테이션은 @Test가 런타임에도 유지되어야 한다는 의미이다.
  • 이를 생략하면 테스트 도구는 @Test를 인식할 수 없다.

@Target(ElementType.METHOD)

  • 위 메타 애너테이션은 @Test가 반드시 메서드 선언에서만 사용되어야 한다는 의미이다.
  • 클래스 선언, 필드 선언 등에는 달 수 없는 애너테이션이다.

적절한 애너테이션 처리기 없이 인스턴스 메서드나 매개변수가 있는 메서드에 애너테이션을 달면, 컴파일은 잘 되겠지만 테스트 도구를 실행할 때 문제가 생기게 된다.


marker annotation이란?

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


실제로 적용해 봅시다.

매개변수가 없는 애너테이션을 처리하는 예제

@item39.Test public static void m1(){} //success

public static void m2(){} //fail

@item39.Test public static void m3(){ 
    throw new RuntimeException("fail"); //ignore
}

public static void m4(){} //ignore

@item39.Test public void m5(){} //fail (wrong fail)

public static void m6(){} //ignore

@item39.Test public static void m7(){
    throw new RuntimeException("fail"); //fail
}

public static void m8(){} //ignore
  • 위 test가 매개변수 없는 정적 메서드 전용이기 때문에, 정적 메서드가 아닌 곳에 @Test를 붙이면 안된다.
  • @Test를 붙이지 않으면 테스트 도구는 이를 무시한다.
  • 예외를 던지면 결과는 실패를 반환한다.
  • 이 외의 경우에서는 성공을 반환한다.

@Test 애너테이션은 위 class의 의미에 직접적인 영향을 주지 않는다. 마커 애너테이션은 표시일 뿐이다

대상 코드의 의미는 그대로 둔 채 해당 애너테이션에 관심 있는 도구에서 특별한 처리를 할 기회(추가정보)를 준다.


테스트 러너를 만들어 테스트를 해보자.

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("suc :: %d, fail : %d%n", passed, tests - passed);
}

@Test 애너테이션이 선언된 메서드만을 선별하여 호출한다. 이때 예외가 나지 않으면 테스트는 성공한다.

//예외를 던지면 실패하는 테스트 케이스의 출력
public static void Sample.m3() failed: RuntimeException: Boom
Invalid @Test: public void Sample.m5()
public static void Sample.m7() failed: RuntimeException: Crash
suc : 1, fail : 3

 


매개변수가 있는 애너테이션을 처리하는 예제

@item39.ExceptionTest(ArithmeticException.class)
public static void m1() {
    int i = 1 / 0; //성공
}

@item39.ExceptionTest(ArithmeticException.class)
public static void m2() {
    int[] arr = new int[0];
    arr[1] = 1;     // outOfIndex 예외 발생
}

@item39.ExceptionTest(ArithmeticException.class)
public static void m3() {} //실패(예외 없이)
  • m1 : @ExceptionTest 애너테이션에 전달된 ArithmeticException 예외를 던지므로 테스트에 성공한다.
  • m2 :  ArithmeticException이 아닌 다른 예외를 던지므로 테스트에 실패한다.
  • m3 : 예외를 던지지 않으므로 테스트에 실패한다.

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

즉, 배열 매개변수를 받는 애너테이션도 존재한다.

배열 매개변수를 받는 애너테이션을 처리하는 예제

@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
public @interface ExceptionTest {
    Class<? extends Throwable>[] value();
}
@item39.ExceptionTest( {ArithmeticException.class, NullPointerException.class})
public static void m4() {
    List<String> list = new ArrayList<>();
}

(테스트 러너는 생략하겠음)


아무튼 이러한 방식으로 여러 개의 값을 받는 애너테이션을 만들 수가 있는데,

다른 방식으로도 만들 수 있다.

 

배열 매개변수가 아니라 애너테이션에 @Repeatable 메타 에너테이션 하나만 달면 된다.

주의할 점은 몇개가 있다.

  1. @Repeatable을 단 애너테이션을 반환하는 컨에티너 애너테이션을 하나 더 정의하고, @Repeatable에 이 컨테이너 애너테이션의 class 객체를 매개변수로 전달해야 한다.
  2. 컨테이너 애너테이션은 내부 애너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
  3. 컨테이너 애너테이션 타입에는 적절한 보존 정책과 적용 대상(@Retention, @Target)을 명시해야 한다. 이를 지키지 않으면 컴파일되지 않는다.
//이런 식으로..

@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
@Repeatable(ExceptionTestContainer.class)
public @interface ExceptionTest2 {
    Class<? extends Throwable>[] value();
}

//Container 애너테이션
@Retention(RetentionPolicy.RUNTIME) // -> meta-annotaion. (애너테이션 선언에 다는 애너테이션)
@Target(ElementType.METHOD) // -> meta-annotaion
public @interface ExceptionTestContainer {
    ExceptionTest2[] value();
}
//이런 느낌으로 변경된다.
@item39.ExceptionTest2(IndexOutOfBoundsException.class)
@item39.ExceptionTest2(NullPointerException.class)
public static void m5(){
    List<String> list = new ArrayList<>();
}

핵심 정리

여러 예시들을 많이 봐서 알겠지만 (?)
다음 예시들과 같이 명명 패턴을 사용하기 보다는 애너테이션을 사용해 여러 가지를 처리하는것이 낫다.
애너테이션을 새로 정의할 필요는 없고, 자바가 제공하는 애너테이션들을 골라골라 사용하면 된다.

 

 

 

728x90