[Effective Java] 아이템 39. 명명 패턴보다 애너테이션을 사용하라

2022. 10. 27. 22:25· BE/Java
목차
  1. 아이템 39. 명명 패턴보다 애너테이션을 사용하라
  2.  
  3.  
  4. 정리
반응형

EFFECTIVE JAVA(이펙티브 자바)

 

이 포스팅에서 작성하는 내용은 EFFECTIVE JAVA(이펙티브자바) 에서 발췌하였습니다.


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

 

명명 패턴

  • 전통적으로 도구나 프레임워크가 특별히 다뤄야할 프로그램 요소에는 딱 구분되는 명명 패턴을 적용해왔다.
  • 예로 테스트 프레임워크인 JUnit은 버전 3까지 테스트 메서드의 이름을 test로 시작하게끔 했다.
  • 효과적이지만 단점도 있다.
    • 오타가 발생하면 안된다.
    • 올바른 프로그램 요소에서만 사용되리라 보증할 방법이 없다는 것 ex) 메서드가 아닌 클래스 이름을 TestSaftyMechanisms로 지어 Junit에 던져줬다고 하면, 개발자는 이 클래스에 정의된 테스트 메서드들을 수행해주길 기대하겠지만 JUnit은 클래스 이름에는 신경쓰지 않는다.
    • 프로그램 요소를 매개변수로 전달할 마땅한 방법이 없다는 것이다.
  • 이러한 문제를 애너테이션이 해결해줄 수 있다.

 

애너테이션

ex 1) marker 애너테이션 타입 선언

import java.lang.annotation.*;

/**
* 테스트 메서드임을 선언하는 애너테이션
* 매개변수 없는 정적 메서드 전용
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test{
}
  • 보다시피 @Test 애너테이션 타입 선언 자체에도 두 가지의 다른 애너테이션이 있다.
  • 이처럼 애너테이션 선언에 다른 애너테이션을 ‘메타애너테이션’이라고 한다.
  • @Retention(RetentionPolicy.RUNTIME) : @Test가 런타임에도 유지되어야 한다는 표시 (이 메타에너테이션을 생략하면 테스트 도구는 @Test를 인식할 수 없다.)
  • @Target(ElementType.METHOD) : @Test가 반드시 메서드 선언애서만 사용한다는 표시 (따라서 클래스 선언, 필드 선언 등 다른 프로그램 요소에서는 애너테이션을 달 수 없다.)

 

 

ex 2) 마커 애너테이션을 사용한 예

public class Sample {
   @Test public static void m1() { }     // 성공해야 한다.
   public static void m2() { }
   @Test public static void m3() {       // 실패해야 한다.
      throw new RuntimeException("실패");
   }
   
   public static void m4() { }
   @Test public void m5() { }      // 잘못 사용한 예 : 정적 메서드가 아니다.
   public static void m6() { }
   @Test public static void m7() {       // 실패해야 한다.
      throw new RuntimeException("실패");
   }
   public static void m8() { }
}
  • 이와 같은 애너테이션을 ‘아무 매개변수 없이 단순히 대상에 마킹한다’ 라는 뜻에서 ‘마커 애너테이션’ 이라고 한다.
  • 이 애너테이션을 사용하는 프로그래머 Test 이름에 오타를 내거나 메서드 선언 외의 프로그램 요소에 달면 컴파일 오류를 내준다.
  • @Test 애너테이션을 달은 4개의 메서드 중 1개는 성공, 2개는 실패, 1개는 잘못 사용한 예이다.
  • 즉, @Test 애너테이션이 Sample 클래스의 의미에 직접적인 영향을 주지 않고 이 애너테이션에 관심 있는 프로그램에게 추가 정보를 제공할 뿐이다.

 

 

ex 3) 마커 애너테이션을 처리하는 프로그램

import java.lang.reflect.*;

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 + " 실패: " + exc);
            } catch (Exception exc) {
               System.out.println("잘못 사용한 @Test: " + m);
            }
         }
      }
      System.out.printf("성공: %d, 실패\\: %d%n", passed, tests - passed);
   }
}
  • 이 테스트 러너는 명령줄로부터 완전 정규화된 클래스 이름을 받아, 그 클래스에서 @Test 애너테이션이 달린 메서드를 차례로 호출한다.
  • isAnnotationPresnet가 실행할 메서드를 찾아주는 메서드이다.
  • 테스트 메서드가 예외를 던지면 리플랙션 매커니즘이 InvocationTargetException으로 감싸서 다시 던진다.
  • 그래서 이 프로그램은 InvocationTargetException을 잡아 원래 예외에 담긴 실패 정보를 추출해 출력한다.
  • InvocationTargetException 외의 예외가 발생한다면 @Test 애너테이션을 잘못 사용했다는 뜻이다.

 

 

ex 4) 매개변수 하나를 받는 애너테이션 타입

import java.lang.annotation.*;

/**
 * 명시한 예외를 던져야만 성공하는 테스트 메서드용 애너테이션
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
   Class<? extends Throwable> value();
}
  • 이 애너테이션의 매개변수 타입은 Class<? extends Throwable>이다.
  • 여기서의 와일드카드 타입은 ‘Throwable을 확장한 클래스의 class 객체’라는 뜻이며, 따라서 모든 예외와 오류 타입을 다 수용한다.

 

 

ex 5) ex 4의 애너테이션을 사용한 프로그램

public class Sample2 {
   @ExceptionTest(ArithmeticException.class)
   public static void m1() { // 성공해야 한다.
      int i = 0;
      i = i / i;
   }
   
   @ExceptionTest(ArithmeticException.class)
   public static void m2() { // 실패해야 한다. (다른 예외 발생)
      int[] a = new int[0];
      int i = a[1];
   }
   
   @ExceptionTest(ArithmeticException.class)
   public static void m3() { } // 실패해야 한다. (예외가 발생하지 않음)
}
  • class 리터럴은 애너테이션 매개변수의 값으로 사용됐다.

 

 

ex 6) ex 5를 기반으로 ex3 코드의 main 메서드를 수정한 코드

if (m.isAnnotationPresent(ExceptionTest.class)) {
   tests++;
   try {
      m.invoke(null);
      System.out.printf("테스트 %s 실패: 예외를 던지지 않음%n", m);
   } catch (InvocationTargetException wrappedEx) {
      Throwable exc = wrappedEx.getCause();
      Class<? extends Throwable> excType =
         m.getAnnotation(ExceptionTest.class).value();
      if (excType.isInstance(exc)) {
         passed++;
      } else {
         System.out.printf(
               "테스트 %s 실패: 기대한 예외 %s, 발생한 예외 %s%n",
                  m, excType.getName(), exc);
      }
   } catch (Exception exc) {
      System.out.println("잘못 사용한 @ExceptionTest: " + m);
   }
}
  • @Test 애너테이션용 코드와 비슷해보이지만, 이 코드는 애너테이션 매개변수의 값을 추출하여 테스트 메서드가 올바른 예외를 던지는 지 확인하는 데 사용한다.
  • 형변환 코드가 없으니 ClassCastException 걱정은 없다.
  • 따라서, 테스트 프로그램이 문제없이 컴파일되면 애너테이션 매개변수가 가리키는 예외가 올바른 타입이라는 뜻이다.
  • 단, 해당 예외의 클래스 파일이 컴파일 타임에는 존재했으나 런타임에는 존재하지 않을 수는 있다. 그런 경우에는 테스트 러너가 TypeNotPresentException을 던질 것이다.

 

 

ex 7) 배열 매개변수를 받는 애너테이션 타입

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface ExceptionTest {
   Class<? extends Throwable>[] value();
}
  • 배열 매개변수를 받는 애너테이션용 문법은 아주 유연하다.
  • 단일 원소 배열에 최적화했지만, 앞서의 @ExceptionTest들도 모두 수정없이 수용한다.

 

 

ex 8) 배열 매개변수를 받는 애너테이션을 사용하는 코드

@ExceptionTest({ IndexOutOfBoundsException.class, NullPointerException.class })
public static void doublyBad() {
   List<String> list = new ArrayList<>();
   
   // 자바 API 명세에 따르면 다음 메서드는 위의 2개를 던질 수 있다.
   list.addAll(5, null);
}
  • 원소가 여럿인 배열을 지정할 때는 다음과 같이 원소들을 중괄호로 감싸고 쉼표로 구분해주기만 하면 된다.

 

 

자바 8에서 여러 값을 받는 애너테이션을 만드는 다른 방법

  • 배열 매개변수를 사용하는 대신 애너테이션에 @Repeatable 메타 에너테이션을 넣으면 된다.
  • @Repeatable을 단 애너테이션은 하나의 프로그램 요소에 여러 번 달 수 있다.
  • @Repeatable 주의점
    • @Repeatable을 단 애너테이션을 반환하는 ‘컨테이너 에너테이션’을 하나 더 정의하고, @Repeatable에 이 컨테이너 에너테이션의 class 객체를 매개변수로 전달해야한다.
    • 컨테이너 에너테이션은 내부 에너테이션 타입의 배열을 반환하는 value 메서드를 정의해야 한다.
    • 컨테이너 에너테이션 타입에는 적절한 보존 정책(@Retention)과 적용 대상(@Target)을 명시해야 한다.

 

ex 9) 반복 가능한 애너테이션 타입

// 반복 가능한 애너테이션
@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() { ... }
  • 반복 가능 애너테이션은 처리할 때도 주의해야 한다.
  • 반복 가능 애너테이션을 여러 개 달면 하나만 달았을 때와 구분하기 위해 해당 ‘컨테이너’ 애너테이션 타입이 적용된다.
  • getAnnotationsByType 메서드는 이 둘을 구분하지 않아 상관없지만, isAnnotationPresent 메서드는 둘을 명확히 구분한다.
  • 따라서 반복 가능 애너테이션을 여러 번 단 다음 isAnnotationPresent로 반복 가능 애너테이션이 달렸는 지 검사한다면 ‘그렇지 않다’로 알려준다. 그 결과, 애너테이션을 여러 번 단 메서드들을 모두 무시하고 지나친다.
  • 같은 이유로, isAnnotationPresent로 컨테이너 애너테이션이 달렸는지 검사한다면 반복 가능 애너테이션을 한 번만 단 메서드를 무시하고 지나친다. 그래서 달려 있는 수와 상관없이 모두 검사하려면 둘을 따로따로 확인해야 한다.

 

 

정리

  • 애너테이션이 명명 패턴보다 낫다.
  • 다른 프로그래머가 소스코드에 추가 정보를 제공할 수 있는 도구를 만드는 일을 한다면 적당한 애너테이션 타입도 함께 정의해보자
  • 애너테이션으로 할 수 있는 일을 명명 패턴으로 처리할 이유는 없다.
  • 도구 제작자를 제외하고는, 일반 프로그래머가 애너테이션 타입을 직접 정의할 일은 없지만, 자바 프로그래머라면 예외 없이 자바가 제공하는 애너테이션 타입들은 사용해야 한다.
반응형

'BE > Java' 카테고리의 다른 글

[Effective Java] 아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라  (0) 2022.10.31
[Effective Java] 아이템 40. @Override 애너테이션을 일관되게 사용하라  (0) 2022.10.28
[Effective Java] 아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라  (0) 2022.10.26
[Effective Java] 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라  (1) 2022.10.25
[Effective Java] 아이템 36. 비트 필드 대신 EnumSet을 사용하라  (0) 2022.10.24
  1. 아이템 39. 명명 패턴보다 애너테이션을 사용하라
  2.  
  3.  
  4. 정리
'BE/Java' 카테고리의 다른 글
  • [Effective Java] 아이템 41. 정의하려는 것이 타입이라면 마커 인터페이스를 사용하라
  • [Effective Java] 아이템 40. @Override 애너테이션을 일관되게 사용하라
  • [Effective Java] 아이템 38. 확장할 수 있는 열거 타입이 필요하면 인터페이스를 사용하라
  • [Effective Java] 아이템 37. ordinal 인덱싱 대신 EnumMap을 사용하라
멍목
멍목
개발 관련 새롭게 알게 된 지식이나 좋은 정보들을 메모하는 공간입니다.
반응형
멍목
김멍목의 개발블로그
멍목
전체
오늘
어제
  • 분류 전체보기 (514)
    • BE (190)
      • Spring (21)
      • Java (141)
      • Kotlin (6)
      • JPA (22)
    • FE (33)
      • Javascript (16)
      • Typescript (0)
      • React (5)
      • Vue.js (9)
      • JSP & JSTL (3)
    • DB (32)
      • Oracle (22)
      • MongoDB (10)
    • Algorithm (195)
    • Linux (8)
    • Git (6)
    • etc (42)
    • ---------------------------.. (0)
    • 회계 (4)
      • 전산회계 2급 (4)
    • 잡동사니 (2)

블로그 메뉴

  • 홈
  • 관리

공지사항

인기 글

태그

  • 더 자바 애플리케이션을 테스트하는 다양한 방법
  • 자바 테스팅 프레임워크
  • 자기 공부
  • java 8
  • Effective Java
  • 코테 공부
  • 자기 개발
  • 더 자바 Java 8
  • MongoDB 공부
  • vue3 공부
  • 이펙티브자바
  • 자바 공부
  • 자바 개발자를 위한 코틀린 입문
  • Java to Kotlin
  • MongoDB with Node.js
  • 알고리즘 공부
  • 전산회계 2급 준비
  • MongoDB 기초부터 실무까지
  • 자기개발
  • junit5
  • JPA
  • 자바공부
  • 코틀린
  • 자기공부
  • Oracle
  • 코테공부
  • JPA 공부
  • 프로젝트로 배우는 Vue.js 3
  • 알고리즘공부
  • 이펙티브 자바

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.0
멍목
[Effective Java] 아이템 39. 명명 패턴보다 애너테이션을 사용하라
상단으로

티스토리툴바

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.