본문 바로가기
JAVA/Effective Java

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

by Garonguri 2022. 6. 3.
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

댓글