월요일, 12월 23
Shadow

#034 Annotations in Tiger, Part 2

Part 1에서 J2SE 5.0의 새로운 메타데이터 장치인 어노테이션을 소개했고 Tiger의 기본적인 빌트인 어노테이션에 초점을 맞추었다. 커스텀 어노테이션을 작성을 지원한다는 점이 특징적이였다. 이 글에서 커스텀 어노테이션을 만드는 방법과 어노테이션에 주석을 달아 코드의 문서화와 커스터마이징을 강화하는 방법을 설명하겠다.

이전글에서 메타데이터가 무엇이고, 이것이 가치 있는 이유와 J2SE 5.0 (Tiger)에 도입된 기본적인 빌트인 어노테이션을 사용하는 방법을 설명했다. 이제 이러한 개념들에 익숙해졌다면 Java 5가 제공하는 세 개의 표준 어노테이션이 특별히 강력한 것은 아니라는 것도 깨달았을 것이다. DeprecatedSuppressWarningsOverride를 사용할 수 있을 뿐이다. 다행히도, Tiger를 사용하여 자신만의 어노테이션 유형을 정의할 수 있다. 이 글에서 몇 가지 예제를 들어 비교적 간단한 프로세스를 설명하겠다. 어노테이션에 주석을 다는 방법과 이를 통한 효용을 설명한다. O’Reilly Media, Inc.에 특별히 감사한다. 이들의 도움으로 내 저서의 어노테이션 챕터에서 코드 샘플을 인용할 수 있었다.

자신의 어노테이션 유형 정의하기

약간의 문법의 추가로(Tiger는 많은 문법상의 구조체를 추가해왔다.) 자바는 새로운 유형인 어노테이션 유형을 지원한다. 어노테이션 유형은 일반 클래스와 비슷해보이지만 독특한 속성이 있다. 가장 주목할만한 것은 클래스에서@(at)기호와 함께 사용하여 다른 자바 코드에 주석을 달 수 있다는 점이다.

@interface 선언

새로운 어노테이션 유형을 정의하는 것은 하나의 인터페이스를 만드는 것과 많은 부분 같다. 단 interface 키워드 앞에 @(at)기호를 붙인다는 점만 다르다. Listing 1은 가장 간단한 어노테이션 유형 예제이다:
Listing 1. 어노테이션 유형


package com.oreilly.tiger.ch06;

/**
 * Marker annotation to indicate that a method or class
 *   is still in progress.
 */
public @interface InProgress { }

 

 

 

Listing 1은 설명적이다. 이 어노테이션 유형을 컴파일하고 이것이 자신의 클래스경로에 있다는 것을 확인하면 자신의 소스 코드 메소드에서 이를 사용하여 메소드 또는 클래스가 여전히 실행 중이라는 것을 나타낼 수 있다. (Listing 2):
Listing 2. 커스텀 어노테이션 유형 사용하기


@com.oreilly.tiger.ch06.InProgress
public void calculateInterest(float amount, float rate) {
  // Need to finish this method later
}

 

 

Listing 1의 어노테이션 유형을 빌트인 어노테이션 유형을 사용했던 것과 정확히 같은 방식으로 Listing 1에서 어노테이션 유형을 사용한다. 이름과 패키지로 커스텀 어노테이션이라는 것을 표시하면 된다. 물론 일반적인 자바 규칙을 적용하여 어노테이션 유형을 반입하고 이를 @InProgress 언급하면 된다.

 

멤버 추가하기

내가 설명한 기본적인 사용법은 강력함과는 거리가 멀다. 어노테이션 유형은 멤버 변수를 가질 수 있다. (참고자료) 단순한 미가공(raw) 문서가 아닌 복잡한 메타데이터로서 어노테이션을 사용하기 시작할 때 특히 유용하다. 코드 분석 툴은 많은 정보를 가질 필요가 있고 커스텀 어노테이션은 이 정보를 제공할 수 있다.

어노테이션 유형의 데이터 멤버들은 제한된 정보를 사용하여 작동하도록 설정된다. 멤버 변수를 정의하지 않고 accessor와 mutator 메소드를 제공한다. 대신, 멤버의 이름을 딴 하나의 메소드를 정의한다. 데이터 유형은 이 메소드의 리턴 값이어야 한다. Listing 3의 구체적인 예제를 보면 보다 명확해진다:
Listing 3. 어노테이션 유형에 멤버 추가하기


package com.oreilly.tiger.ch06;

/**
 * Annotation type to indicate a task still needs to be
 *   completed.
 */
public @interface TODO {
  String value();
}

 

 

Listing 3은 이상하게 보이지만 이는 어노테이션 유형에서 필요한 것이다. Listing 3은 어노테이션 유형이 받아들일 수 있는 value 라는 스트링을 정의한다. 이제 Listing 4 처럼 어노테이션 유형을 사용한다:
Listing 4. 멤버 값과 어노테이션 유형 사용하기


@com.oreilly.tiger.ch06.InProgress
@TODO("Figure out the amount of interest per month")
public void calculateInterest(float amount, float rate) {
  // Need to finish this method later
}

 

 

여기에서 트릭은 많이 사용하지 않았다. Listing 4는 com.oreilly.tiger.ch06.TODO 가 반입되었고 따라서 소스에서는 패키지 이름으로 어노테이션에 접두사를 달지 않는다. Listing 4는 속기법을 사용한다: 멤버 변수 이름을 지정하지 않고 값 ("Figure out the amount of interest per month") 을 어노테이션에 준다. Listing 4는 Listing 5와 같다. 속기법을 사용하지 않는다:
Listing 5. Listing 4의 “보통표기(Longhand)” 버전


@com.oreilly.tiger.ch06.InProgress
@TODO(value="Figure out the amount of interest per month")
public void calculateInterest(float amount, float rate) {
  // Need to finish this method later
}

 

 

물론 우리는 모두 코더(coder)들이기 때문에 보통표기법으로 혼란을 가중시키고 싶지 않다. 속기는 어노테이션 유형이 value라는 싱글멤버 변수를 갖고 있을 경우에만 통한다.

디폴트 값 설정하기

지금까지 시작은 좋았다. 하지만 이것을 요리할 많은 방법이 있다. 이제 다음 순서는 이 어노테이션에 디폴트 값을 설정하는 것이라고 생각할지도 모르겠다. 이 같은 경우는 사용자들이 값을 지정하도록 할 때는 좋지만 디폴트와 다를 경우에만 다른 값을 지정해야 한다. Listing 6은 이 개념과 구현을 또 다른 커스텀 어노테이션을 사용하여 설명한다. Listing 4의 TODO 어노테이션 유형의 더욱 완숙한 버전이라고 할 수 있다:
Listing 6. 디폴트 값을 가진 어노테이션 유형


package com.oreilly.tiger.ch06;

public @interface GroupTODO {

  public enum Severity { CRITICAL, IMPORTANT, TRIVIAL, DOCUMENTATION };

  Severity severity() default Severity.IMPORTANT;
  String item();
  String assignedTo();
  String dateAssigned();
}

 

 

Listing 6의 GroupTODO 어노테이션 유형은 새로운 여러 변수들을 추가한다. 이 어노테이션 유형은 싱글-멤버 변수가 없기 때문에 이 변수 중 하나에 value라는 이름을 붙여도 어떤 것도 얻지 못한다는 것을 기억하라. 한 개 이상의 멤버 변수를 갖게 되면 가능한 정확하게 이름을 정해야 한다. Listing 5의 속기 문법으로는 효과를 얻지 못한다. 조금 더 장황해지더라도 어노테이션 유형에 대한 보다 나은 문서를 만들도록 한다.

Listing 6의 또 다른 새로운 기능은 어노테이션 유형이 고유의 열거방식을 정의한다는 것이다. (열거(Enumeration): 일반적으로 enums라고 불린다. Java 5의 새로운 기능이다. 어노테이션 유형에 있어 주목할 만한 것은 아니다.) Listing 6은 멤버 변수용 유형으로서 새로운 열거를 사용한다.

마지막으로 다시 주제로 돌아가서 디폴트 값을 보겠다. 디폴트 값을 설정하는 것은 매우 간단하다. 멤버 선언의 끝에 라는 키워드를 추가하고 디폴트 값을 주면 된다. 멤버 변수를 위해 선언했던 것과 같은 유형이다. 거듭 말하지만 이것은 로케트 과학은 아니다. 그저 어휘 트릭일 뿐이다. Listing 7은 GroupTODO 어노테이션의 실행 모습이다.severity가 표시되지 않는 경우이다:
Listing 7. 디폴트 값 사용하기


  @com.oreilly.tiger.ch06.InProgress
  @GroupTODO(
    item="Figure out the amount of interest per month",
    assignedTo="Brett McLaughlin",
    dateAssigned="08/04/2004"
  )
  public  void calculateInterest(float amount, float rate) {
    // Need to finish this method later
  }

 

 

Listing 8은 같은 어노테이션을 사용하고 있다. severity에 제공된 값을 함께 사용한다:
Listing 8. 디폴트 값 오버라이드


  @com.oreilly.tiger.ch06.InProgress
  @GroupTODO(
    severity=GroupTODO.Severity.DOCUMENTATION,
    item="Need to explain how this rather unusual method works",
    assignedTo="Jon Stevens",
    dateAssigned="07/30/2004"
  )
  public  void reallyConfusingMethod(int codePoint) {
    // Really weird code implementation
  }


위로

 

어노테이션에 주석달기

어노테이션에 대한 글을 마감하기 전에 어노테이션에 주석을 다는 것을 간단히 설명하고자 한다. Part 1에서 배웠던 사전 정의된 어노테이션 유형은 사전에 정해진 목적을 갖고 있다. 하지만 자신의 어노테이션 유형을 작성하는 만큼 어노테이션 유형의 목적은 언제나 스스로 명백해지는 것은 아니다. 기본 문서에 더하여 특정 멤버 유형에 해당하는 유형을 작성할 수도 있고 또는 멤버 유형의 특정 세트에 해당하는 유형을 작성할 수도 있다. 이럴 때에는 어노테이션 유형에 일종의 메타데이터를 제공해야 한다. 이렇게 되면 컴파일러는 어노테이션이 의도한 기능을 실행할 수 있다.

물론 어노테이션이 해결책이라는 생각을 바로 해야 한다. 메타-어노테이션이라고 하는 네 개의 사전 정의된 어노테이션 유형을 사용하여 어노테이션에 주석을 단다.

목표 지정하기

가장 분명한 메타-어노테이션은 어떤 프로그램 엘리먼트가 정의된 유형의 어노테이션을 가질 것인가를 가르킬 수 있도록 하는 것이다. 이 메타-어노테이션을 Target이라고 한다. Target을 사용하는 방법을 보기 전에 또 다른 새로운 클래스인 ElementType을(실제로 enum) 알아야 한다. 이 enum은 어노테이션 유형이 목표로 하는 다양한 프로그램 엘리먼트를 정의한다. Listing 9는 ElementType을 보여준다:
Listing 9. ElementType enum


package java.lang.annotation;

public enum ElementType {
  TYPE,			// Class, interface, or enum (but not annotation)
  FIELD,		// Field (including enumerated values)
  METHOD,		// Method (does not include constructors)
  PARAMETER,		// Method parameter
  CONSTRUCTOR,		// Constructor
  LOCAL_VARIABLE,	// Local variable or catch clause
  ANNOTATION_TYPE,	// Annotation Types (meta-annotations)
  PACKAGE		// Java package
}

 

 

Listing 9의 열거된 값은 아주 분명하고 각각 어떻게 적용해야 하는지 이해할 수 있다. Target 메타-어노테이션을 사용할 때 열거된 값들 중 최소한 하나를 주고 주석이 달린 어노테이션이 목표로 할 수 있는 프로그램 엘리먼트가 무엇인지 나타낸다. Listing 10은 Target의 실행 모습이다:
Listing 10. Target 메타-어노테이션 사용하기


package com.oreilly.tiger.ch06;

import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

/**
 * Annotation type to indicate a task still needs to be completed
 */
@Target({ElementType.TYPE,
         ElementType.METHOD,
         ElementType.CONSTRUCTOR,
         ElementType.ANNOTATION_TYPE})
public @interface TODO {
  String value();
}

 

 

이제 자바 컴파일러는 유형, 메소드, 컨스트럭터, 기타 어노테이션 유형에 TODO를 적용한다. 이로서 어떤 누구도 어노테이션 유형을 취하거나 이를 그릇되게 적용하지 않는다는 것을 보장한다.

retention 설정하기

이제 설명할 메타-어노테이션은 Retention이다. 이 메타-어노테이션은 자바 컴파일러가 주석이 달린 어노테이션 유형을 다루는 방법과 관련되어 있다. 컴파일러는 여러 옵션들이 있다:

  • 주석이 달린 클래스의 컴파일 된 클래스 파일에 있는 어노테이션을 유지하다가 클래스가 첫 번째로 로딩될 때 이를 읽는다.
  • 컴파일 된 클래스 파일에서 어노테이션을 유지하지만 런타임 시 이를 무시한다.
  • 지시된 대로 어노테이션을 사용하지만 컴파일 된 클래스 파일에서 이를 버린다.

이 세 가지 옵션은 java.lang.annotation.RetentionPolicy에서 나타난다. (Listing 11)::
Listing 11. The RetentionPolicy enum


package java.lang.annotation;

public enum RetentionPolicy {
  SOURCE,		// Annotation is discarded by the compiler
  CLASS,		// Annotation is stored in the class file, but ignored by the VM
  RUNTIME		// Annotation is stored in the class file and read by the VM
}

 

 

Retention 메타-어노테이션 유형은 하나의 인자로서 열거된 값들(Listing 11) 중 하나를 취한다. 이 메타-어노테이션을 자신의 어노테이션으로 향하게 한다. (Listing 12):
Listing 12. Retention 메타-어노테이션 사용하기


@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
  // annotation type body
}

 

 

Retention이 싱글-멤버 변수를 갖고 있기 때문에 Listing 12에서 속기를 사용한다. Retention이RetentionPolicy.CLASS가 되도록 하려면 어떤 일을 수행하지 않아도 된다. 디폴트 작동이 있기 때문이다.

퍼블릭 문서 추가하기

다음 메타-어노테이션은 Documented이다. Documented는 마커(marker) 어노테이션이기 때문에 이해하기 쉽다. Part 1에서도 언급했지만, 마커 어노테이션은 멤버 변수가 없다. Documented는 어노테이션이 클래스용 Javadoc에 나타나야 한다는 것을 나타내고 있다. 기본적으로 어노테이션들은 Javadoc에 포함되지 않는다. 클래스에 주석을 달기위해 많은 시간을 보내야 할 때, 할 일이 나아있음을 상세히 적으로 때, 정확히 무엇을 수행하는지 기록할 때 이를 기억해두면 좋다.

Listing 13은 Documented 메타-어노테이션의 사용법이다:
Listing 13. Documented 메타-어노테이션


package com.oreilly.tiger.ch06;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Marker annotation to indicate that a method or class
 *   is still in progress.
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface InProgress { }

 

 

Documented의 한 가지 문제는 retention 정책이다. Listing 13은 어노테이션의 retention을 RUNTIME으로 지정한다. 이는 Documented 어노테이션 유형을 사용할 때 필요한 부분이다. Javadoc은 가상 머신을 사용하여 클래스 파일(소스 파일 아님)에서 정보를 로딩한다. VM이 클래스 파일들에서 Javadoc을 만들어내기 위한 정보를 얻는다는 것을 확인하는 유일한 방법은 RetentionPolicy.RUNTIME의 retention을 지정하는 것이다. 결과적으로 어노테이션은 컴파일 된 클래스 파일에 저장되고 VM에 의해 로딩된다; Javadoc은 이를 가져다가 클래스의 HTML 문서에 추가한다.

상속 설정하기

마지막 메타-어노테이션인 Inherited는 설명하기도 가장 복잡하고, 자주 사용되지도 않으며, 많은 혼란을 일으킨다.

먼저, 사용 케이스를 보도록 하자. 클래스가 진행중임을 표시한다고 가정한다. 물론 자신의 커스텀 InProgress어노테이션을 사용한다. Documented 메타-어노테이션을 정확히 적용했다면 Javadoc에서 보일 것이다. 새로운 클래스를 작성하여 진행중인 클래스로 확장한다고 가정해본다. 쉬울 것 같다. 하지만 수퍼클래스도 진행 중이라는 것을 기억하라. 서브클래스를 사용하고 문서를 본다면 어떤 것도 불완전하다는 것을 인식하지 못한다. 상속 받은서브클래스를 통해 InProgress 어노테이션이 전달되기를 기대했겠지만 그렇지 않다. 원하는 작동을 지정하기 위해 Inherited 메타 어노테이션을 사용해야 한다. (Listing 14):
Listing 14. Inherited 메타-어노테이션


package com.oreilly.tiger.ch06;

import java.lang.annotation.Documented;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

/**
 * Marker annotation to indicate that a method or class
 *   is still in progress.
 */
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
public @interface InProgress { }

 

 

@Inherited의 추가로 InProgress 어노테이션이 주석이 달린 클래스의 서브클래스상에 나타난다. 물론 여러분의 모든 어노테이션 유형에 이 작동이 필요한 것은 아니다. (디폴트는 상속되지 않기 때문이다); 예를 들어TODO 어노테이션은 광고 되지 않는다.(되어서도 안된다.) 오직 이 경우에만 Inherited가 유용할 뿐이다.

결론

이쯤 되면 모든 것을 문서화하고 주석을 달 준비가 되어있을 것이다. 모든 사람이 Javadoc을 이해할 때 어떤 일이 일어날지를 생각나게 한다. Javadoc이 혼란스러운 클래스나 메소드를 명확히 하는데 최상의 방법이라는 것을 깨닫기 전에 모든 것을 과도하게 문서화했다. 어떤 누구도 이러한 이해하기 쉬운 메소드인 getXXX()setXXX()를 보려고 하지 않을 것이다.

어노테이션의 경우도 마찬가지다. 가끔 표준 어노테이션 유형을 사용하는 것은 좋은 생각이다. 모든 Java 5 컴파일러가 이를 지원하고 작동도 이해하기 쉽다. 하지만 커스텀 어노테이션과 메타-어노테이션의 경우 힘들게 작업한 유형들이 개발 정황의 밖에서 어떤 의미를 갖고 있는지를 파악하기는 더 힘들다. 합리적으로 어노테이션을 사용하되 비웃음거리가 되지 않도록 하라. 어노테이션 장치는 가치가 있고 개발 프로세스에 도움이 된다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.