월요일, 12월 23
Shadow

#032 Annotation 소개

J2SE 5.0 (Tiger)의 새로운 기능인 Annotation은 필요가 많은 메타데이터 기능을 핵심 자바 언어로 가져왔다. 메타테이터가 유용하게 쓰이는 이유를 설명하고 자바의 Annotation 소개한다.

프로그래밍, 특히 자바 프로그래밍의 최신 경향 중 하나는 metadata를 사용한다는 것이다. 메타데이터는 간단히 말해서 데이터에 대한 데이터이다. 메타데이터는 문서화에 사용될 수 있고 코드 의존성을 트래킹하는데 사용되며 심지어 초기 컴파일 시간 체크를 수행 할 때도 사용될 수 있다. XDoclet 같은 (참고자료)메타데이터용 툴들은 이러한 기능을 핵심 자바 언어로 가져왔고 얼마 동안 자바 프로그래밍 관습의 일부가 되었다.

J2SE 5.0 (일명 Tiger)이 나오기 전 까지, 핵심 자바 언어는 javadoc 방법론과 함께 메타데이터 장치에 근접했다. 특별한 태그 세트를 사용하여 코드를 마크업(mark-up)하고 그런 다음 javadoc 명령어를 실행하여 태그를 포맷된 HTML로 변환하여 태그들이 어태치 될 클래스들을 문서화한다. 하지만 Javadoc은 부적당한 메타데이터 툴이다. 문서들을 모으는 것 이상의 다른 목적의 데이터들을 얻을 수 있는 견고하고 표준화된 방법이 없기 때문이다. HTML 코드가 종종 Javadoc 아웃풋과 섞인다는 사실이 이를 더욱 증명하고 있다.

Tiger는 어노테이션이라는 새로운 기능을 통해 보다 다양한 메타데이터 장치를 핵심 자바 언어에 추가했다. 어노테이션은 코드에 추가할 수 있고, 패키지 선언, 유형 선언, 생성자, 메소드, 필드, 매개변수, 변수에 적용할 수 있는 변경자(modifier)이다. Tiger에는 빌트인 어노테이션이 추가되었고 직접 작성할 수 있는 커스텀 어노테이션도 지원한다. 이 글에서는 메타테이터의 효용을 설명하고 Tiger의 빌트인 어노테이션을 소개하겠다. Part 2에서는 커스텀 어노테이션에 대해 자세히 알아 볼 것이다. O’Reilly Media, Inc.에 특별히 감사한다. 이들의 도움으로 내 저서의 어노테이션 챕터에서 코드 샘플을 인용할 수 있었다.(참고자료)

메타데이터의 가치

일반적으로 메타데이터의 효용은 세 가지로 나눌 수 있다. 문서화, 컴파일러 체크, 코드 분석. 코드 레벨의 문서화는 가장 자주 인용되는 사용법이다. 메타데이터는 메소드가 다른 메소드에 의존하고 있다는 것을 가르키는 유용한 방법을 제공한다. 또한 그들이 불완전한지, 특정 클래스가 또 다른 클래스를 레퍼런싱 하는지 등을 가르킨다. 이는 정말로 유용하지만 문서화는 메타데이터를 자바에 추가하는 것과 가장 관련이 적은 항목이다. 코드의 문서화에 있어서는 Javadoc이 사용이 쉽고 강력한 방식을 제공하고 있기 때문이다. 이미 사용하고 있는 것이 있고 작동도 잘 되고 있는데 문서를 굳이 만들 필요는 없지 않는가?

 

컴파일러 체크

보다 중요한 메타데이터의 장점은 컴파일러가 메타데이터를 사용하여 몇 가지 기본적인 컴파일 시간 체크를 수행할 수 있는 기능이라고 할 수 있다. 예를 들어, 이 글의 The Override 어노테이션섹션에서 Tiger 는 메소드가 또 다른 메소드를 수퍼클래스에서 겹쳐 쓰게끔 지정하는 어노테이션을 도입한다. 자바 컴파일러는 메타데이터에서 가르키는 작동이 실제로 코드 레벨에서 발생한다는 것을 확인할 수 있다. 이러한 유형의 버그를 추적해 본 적이 없다면 어리석은 일 같지만 대부분의 자바 프로그래밍 베테랑들은 코드가 왜 작동하지 않는지를 밝혀내기 위해서 밤을 지새우기도 하는 법이다. 메소드가 잘못된 매개변수를 갖고 있고 사실 수퍼클래스에서 메소드를 겹쳐 쓰지 않는다는 것을 발견했을 때의 씁쓸함이란… 메타데이터를 소비하는 툴을 사용하면 이러한 유형의 에러를 쉽게 발견할 수 있다. 많은 밤을 뜬눈으로 지새우지 않아도 된다.

 

코드 분석

아마도 좋은 어노테이션 또는 메타데이터 툴의 최고의 기능은 여분의 데이터를 사용하여 코드를 분석하는 것이다. 간단한 경우, 코드 목록을 구현하고 필요한 인풋 유형을 제공하고 리턴 유형을 지시한다. 하지만 자바 리플렉션도 같은 기능을 제공한다고 생각할 수도 있다; 결국 이 모든 정보에 대한 코드를 검사할 수 있다. 표면적으로 볼 때 그럴 듯 하지만 실제로 그렇지 않다. 많은 경우 메소드는 인풋으로서 받아들이거나 아웃풋으로 리턴한다. 이는 메소드가 원하는 것이 아니다. 예를 들어, 매개변수 유형이 Object이지만 메소드는 Integer를 사용해서만 작동한다. 이는 메소드가 겹쳐쓰기된 곳에서 쉽게 발생할 수 있다. 그리고 수퍼클래스가 메소드를 일반 매개변수로 선언하던가 많은 직렬화가 진행되는 시스템에서도 쉽게 발생한다. 두 경우 모두 메타데이터는 코드 분석 툴을 지정할 수 있다. 매개변수 유형이 Object이더라도 정말로 원하는 것은 Integer라는 것을 나타낼 수 있다. 이러한 종류의 분석은 상당히 유용하며 그 가치는 상당하다.

보다 복잡한 경우 코드 분석 툴은 모든 종류의 추가 태스크들을 수행할 수 있다. 그 예 중 하나가 Enterprise JavaBean (EJB) 컴포넌트이다. 심지어 간단한 EJB 시스템으로의 의존성과 복잡함은 상당하다. 로컬 인터페이스와 로컬 홈 인터페이스의 가능성과 함께 홈 인터페이스와 원격 인터페이스를 얻는다. 이 모든 클래스들을 연동시키는 것은 진정한 고통이다. 하지만 메타데이터는 이 문제에 대한 솔루션을 제공한다. 좋은 툴은(예를 들어, XDoclet)은 이 모든 의존성을 관리하면서 “코드-레벨” 연결이 없지만 “로컬-레벨” 관계를 가진 클래스들이 연동될 수 있도록 한다. 이것이 바로 메타테이터의 진정한 가치이다.

 

어노테이션의 기초

메타데이터가 어디에 좋은 지를 이해했으니 Tiger 의 어노테이션을 소개하겠다. Tiger는 “at” 표시(@)를 취한다. 그 뒤에는 어노테이션 이름이 붙는다. 그런 다음 데이터가 필요할 때 어노테이션에 데이터를 제공한다. (name=value)종류의 표기법을 사용할 때 마다 어노테이션을 만든다. 코드 한 조각은 10, 50 또는 그 이상의 어노테이션을 가진다. 하지만 많은 어노테이션은 같은 어노테이션 유형을 사용한다. 이 유형은 실제 구조체이고 어노테이션 자체는 이 유형의 특정 사용법이다. (사이드 바 참조 어노테이션 또는 어노테이션 유형?)

 

어노테이션은 세 가지 기본 범주로 나뉜다:

  • Marker 어노테이션은 변수가 없다. 이 어노테이션은 이름으로 구분되며 추가 데이터 없이 나타난다. 예를 들어, @MarkerAnnotation은 marker 어노테이션이다. 데이터가 없으며 단지 어노테이션 이름만 있을 뿐이다.
  • Single-value 어노테이션은 marker와 비슷하지만 데이터를 제공한다. 싱글 비트 데이트를 제공하기 때문에 간단한 신택스를 사용할 수 있다. (단, 어노테이션 유형이 이 문법을 수용해야 함):@SingleValueAnnotation("my data")이는 @표시만 제외하고는 일반적인 자바 메소드 호출과 비슷하다.
  • Full 어노테이션은 다중 데이터 멤버를 갖고 있다. 결과적으로 전체 신택스를 사용해야 한다. (그리고 어노테이션은 일반 자바 메소드와 더 이상 비슷하지 않다): @FullAnnotation(var1="data value 1", var2="data value 2", var3="data value 3")

디폴트 신택스를 통해 어노테이션에 값을 제공하는 것 외에도 한 개 이상의 값을 전달해야 할 때 name-value쌍을 사용할 수 있다. 또한 어노테이션 변수에 값 어레이를 제공할 수 있다. 이때 중괄호({})를 사용한다. Listing 1은 어노테이션에서의 값의 어레이 예제이다.

 

Listing 1. 어노테이션에서 어레이 값 사용하기


@TODOItems({    // Curly braces indicate an array of values is being supplied
  @TODO(
    severity=TODO.CRITICAL,
    item="Add functionality to calculate the mean of the student's grades",
    assignedTo="Brett McLaughlin"
  ),
  @TODO(
    severity=TODO.IMPOTANT,
    item="Print usage message to screen if no command-line flags specified",
    assignedTo="Brett McLaughlin"
  ),
  @TODO(
    severity=TODO.LOW,
    item="Roll a new website page with this class's new features",
    assignedTo="Jason Hunter"
  )
})

 

 

Listing 1의 예제는 보기보다 간단하다. TODOItems 어노테이션 유형은 값을 취하는 하나의 변수를 갖고 있다. 여기에서 제공되는 값은 매우 복잡하다. 하지만 TODOItems를 사용하면 single-value 어노테이션 스타일과 실제로 맞다. Single value가 어레이라는 것을 제외하면 말이다. 이 어레이는 세 개의 TODO 어노테이션을 포함하고 있는데 각각 값이 증폭된다. 콤마는 각 어노테이션에서 값을 분리하고 하나의 어레이에서의 값도 콤마로 분리된다.

TODOItems와 TODO는 커스텀 어노테이션이다. 커스텀 어노테이션은 Part 2의 주제이다. 여러분에게 복잡한 어노테이션을 보여주고 싶었다. Listing 1은 어떤 어노테이션 보다 복잡하지만 그렇게 심하지는 않다. 자바의 표준 어노테이션 유형을 살펴본다면 그렇게 복잡한 것도 드물다. 다음 섹션에서는 Tiger의 기본 어노테이션 유형이 사용하기 쉽다는 것을 알게될 것이다.

 

Override 어노테이션

Tiger의 첫 번째 빌트인 어노테이션 유형은 Override이다. Override는 메소드에 대해서만 사용되어야 한다. (클래스, 패키지 선언, 기타 구조체는 안된다.) 주석이 첨가된 메소드는 수퍼클래스에서 메소드를 오버라이드한다는 것을 나타낸다. Listing 2는 예제이다.
Listing 2. The Override 어노테이션


package com.oreilly.tiger.ch06;

public class OverrideTester {

  public OverrideTester() { }

  @Override
  public String toString() {
    return super.toString() + " [Override Tester Implementation]";
  }

  @Override
  public int hashCode() {
    return toString().hashCode();
  }
}

 

 

Listing 2는 따라가기 쉽다. @Override어노테이션은 두 개의 메소드, toString()과 hashCode()OverrideTester 클래스의 수퍼클래스 (java.lang.Object)에서 메소드의 버전을 오버라이드 한다는 것을 나타내고 있다. 언뜻 보기에는 사소한 것 같지만 매우 좋은 기능이다. 이들 메소드를 오버라이딩 하지 않고는 클래스를 컴파일 할 수 없다. 어노테이션은 toString()과 썩일 때 적어도 hashCode()와 맞는다는 것을 확인해야 하는 것을 나타낸다.

이 어노테이션 유형은 코딩하기엔 너무 늦었거나 무언가를 잘못 타이핑했을 때 빛을 발한다. (Listing 3)
Listing 3. Override 어노테이션의 오타 찾아내기


package com.oreilly.tiger.ch06;

public class OverrideTester {

  public OverrideTester() { }

  @Override
  public String toString() {
    return super.toString() + " [Override Tester Implementation]";
  }

  @Override
public int hasCode() {
    return toString().hashCode();
  }
}

 

 

Listing 3에서, hashCode()가 hasCode()로 잘못 표기되었다. 어노테이션은 hasCode()가 메소드를 오버라이드해야 한다는 것을 지시한다. 하지만 컴파일 시, javac는 수퍼클래스(java.lang.Object)가 오버라이드 할 hasCode()라는 메소드가 없다는 것을 알게 된다. 결과적으로 컴파일러는 에러를 표시한다. (그림 1)
그림 1. Override 어노테이션에서의 컴파일러 경고

이 간편한 기능으로 오타를 매우 빠르게 잡을 수 있다.

 

Deprecated 어노테이션

이제 Deprecated표준 어노테이션 유형을 살펴보자. Override와 마찬가지로 Deprecated는 marker 어노테이션이다. Deprecated를 사용하여 더 이상 사용되지 말아야 하는 메소드에 주석을 단다. Override와 다른 점은, Deprecated는 더 이상 사용되지 말아야 하는(depreciated) 메소드와 같은 라인상에 놓여져야 한다. (이유는 나도 모르겠다.)

 

Listing 4. Deprecated 어노테이션 사용하기


package com.oreilly.tiger.ch06;

public class DeprecatedClass {

  @Deprecated public void doSomething() {
    // some code
  }

  public void doSomethingElse() {
    // This method presumably does what doSomething() does, but better
  }
}

 

 

이 클래스를 컴파일 할 때 비정상적인 그 어떤 것도 기대해서는 안된다. 오버라이드 또는 호출이든 Depreciated 메소드를 사용하면 컴파일러는 어노테이션을 처리하고 메소드가 사용되어서는 안된다는 것을 알게 되고 에러 메시지를 만든다. (그림 2)
그림 2. Deprecated 어노테이션의 컴파일러 경고

컴파일러 경고를 켜고 정상적인 depreciation 경고를 원한다는 것을 자바 컴파일러에게 명령한다. 두 플래그 -deprecated또는 -Xlint:deprecated중 하나와 javac명령어를 사용할 수 있다.

 

SuppressWarnings 어노테이션

마지막 어노테이션 유형은 SuppressWarnings이다. 이것이 어떤 일을 수행하는지 알아내는 것은 쉽다. 하지만 왜 이 어노테이션이 중요한지는 분명하지 않다. 이는 실제로 Tiger의 새로운 기능의 부작용이다. 예를 들어, generics를 생각해보자. generics는 모든 유형의 새로운 type-safe 작동을 만든다. 특히 자바 컬렉션의 경우 더욱 그렇다. 하지만 generics 때문에 컴파일러는 컬렉션이 type-safety 없이 사용될 때 경고를 던진다. Tiger를 겨냥한 코드에는 유용하지만 Java 1.4.x의 경우엔 코드 작성이 고통 그 자체이다. 전혀 신경 쓰지 않은 것에 대해 경고를 받아야 한다. 컴파일러를 어떻게 하면 없앨 수 있을까?

SupressWarnings는 구원자다. Override와 Deprecated와는 다르게 SupressWarnings는 변수를 갖고 있다. 따라서 이를 작동하게 하려면 싱글-어노테이션 유형을 사용한다. 값 어레이로서 변수를 제공할 수 있다. 각각 삭제할(Suppress) 특정 유형의 경고를 나타낸다. Listing 5의 예제를 보자. Tiger에서 에러를 만드는 코드이다.
Listing 5. type-safe가 아닌 Tiger 코드


public void nonGenericsMethod() {
  List wordList = new ArrayList();    // no typing information on the List

  wordList.add("foo");                // causes error on list addition
}

 

 

그림 3은 Listing 5에서 코드 컴파일을 한 결과이다.
그림 3. non-typed 코드에서 컴파일러 경고

Listing 6은 SuppressWarnings 어노테이션을 사용하여 번거로운 경고를 제거한다.
Listing 6. 경고 제거하기


@SuppressWarnings(value={"unchecked"})
public void nonGenericsMethod() {
  List wordList = new ArrayList();    // no typing information on the List

  wordList.add("foo");                // causes error on list addition
}

 

 

간단하지 않은가? 경고 유형을 배치하고(그림 3의 “unchecked”) SuppressWarnings에 전달하면 된다.

SuppressWarnings의 변수 값이 어레이를 취한다는 사실은 같은 어노테이션으로 다중의 경고를 삭제할 수 있음을 의미한다. 예를 들어, @SuppressWarnings(value={"unchecked", "fallthrough"})는 두 개의 값 어레이를 취한다. 이 장치는 매우 유연한 방식을 제공하여 장황하지 않게 에러를 핸들 할 수 있다.

 

결론

이 글에서 본 문법이 다소 생소하더라도 어노테이션은 이해하기도 쉽고 사용도 쉽다는 것을 알아야 한다. Tiger 에서 제공하는 표준 어노테이션 유형이 어설프고 개선의 여지가 많다. 메타데이터는 점점 유용해지고 있고 자신의 애플리케이션에 맞는 어노테이션 유형을 사용할 수 있을 것이다. Part 2에서는 커스텀 어노테이션 유형을 작성하는 방법을 자세히 다루겠다. 자바 클래스를 만들고 이를 어노테이션 유형으로서 정의하는 방법, 컴파일러가 어노테이션 유형을 인식하게 하는 방법, 코드에 주석을 달 때 이를 사용하는 방법 등을 설명하겠다.

답글 남기기

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

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