An annotation, in the Java computer programming language, is a form of syntactic metadata that can be added to Java source code.[1] Classes, methods, variables, parameters and packages may be annotated. Unlike Javadoc tags, Java annotations can be reflective in that they can be embedded in class files generated by the compiler and may be retained by the Java VM to be made retrievable atrun-time.[2] It is possible to create meta-annotations out of the existing ones in Java.[3]
Contents
[hide]History[edit]
The Java platform has various ad-hoc annotation mechanisms—for example, the transient modifier, or the @deprecated javadoc tag. JSR-175 introduced the general-purpose annotation (also known asmetadata) facility to the Java Community Process in 2002; it gained approval in September 2004.[4] Annotations became available in the language itself beginning with version 1.5 of the JDK. The apttool provided a provisional interface for compile-time annotation processing in JDK version 1.5; JSR-269 formalized this, and it became integrated into the javac compiler in version 1.6.
Built-in annotations[edit]
Java defines a set of annotations that are built into the language.[5]
Annotations applied to Java code:
- @Override - Checks that the method is an override. Causes a compile error if the method is not found in one of the parent classes or implemented interfaces.
- @Deprecated - Marks the method as obsolete. Causes a compile warning if the method is used.
- @SuppressWarnings - Instructs the compiler to suppress the compile time warnings specified in the annotation parameters.
- @SafeVarargs - Suppress warnings for all callers of a method or constructor with a generics varargs parameter, since Java 7.
- @FunctionalInterface - Specifies that the type declaration is intended to be a functional interface, since Java 8.
Annotations applied to other annotations: "or can be called Meta Annotations"
- @Retention - Specifies how the marked annotation is stored—Whether in code only, compiled into the class, or available at runtime through reflection.
- @Documented - Marks another annotation for inclusion in the documentation.
- @Target - Marks another annotation to restrict what kind of Java elements the annotation may be applied to.
- @Inherited - Marks another annotation to be inherited to subclasses of annotated class (by default annotations are not inherited to subclasses).
- @Repeatable - Specifies that the annotation can be applied more than once to the same declaration, since Java 8.
Example[edit]
Built-in annotations[edit]
This example shows the use of the @Override annotation. It instructs the compiler to check parent classes for matching methods. In this case, an error is generated as the gettype() method of class Cat does not in fact override getType() of class Animal as desired. If the @Override annotation was absent, a new method of name gettype() would be created in class Cat.
public class Animal {
public void speak() {
}
public String getType() {
return "Generic animal";
}
}
public class Cat extends Animal {
@Override
public void speak() { // This is a good override.
System.out.println("Meow.");
}
@Override
public String gettype() { // Compile-time error due to mistyped name.
return "Cat";
}
}
Custom annotations[edit]
Annotation type declarations are similar to normal interface declarations. An at-sign (@) precedes the interface keyword. Each method declaration defines an element of the annotation type. Method declarations must not have any parameters or a throws clause. Return types are restricted to primitives, String, Class, enums, annotations, and arrays of the preceding types. Methods can have default values.
// @Twizzle is an annotation to method toggle().
@Twizzle
public void toggle() {
}
// Declares the annotation Twizzle.
public @interface Twizzle {
}
Annotations may include an optional list of key-value pairs:
// Same as: @Edible(value = true)
@Edible(true)
Item item = new Carrot();
public @interface Edible {
boolean value() default false;
}
@Author(first = "Oompah", last = "Loompah")
Book book = new Book();
public @interface Author {
String first();
String last();
}
Annotations themselves may be annotated to indicate where and when they can be used:
@Retention(RetentionPolicy.RUNTIME) // Make this annotation accessible at runtime via reflection.
@Target({ElementType.METHOD}) // This annotation can only be applied to class methods.
public @interface Tweezable {
}
The compiler reserves a set of special annotations (including @Deprecated, @Override and @SuppressWarnings) for syntactic purposes.
Annotations are often used by frameworks as a way of conveniently applying behaviours to user-defined classes and methods that must otherwise be declared in an external source (such as an XML configuration file) or programmatically (with API calls). The following, for example, is an annotated JPA data class:
@Entity // Declares this an entity bean
@Table(name = "people") // Maps the bean to SQL table "people"
public class Person implements Serializable {
@Id // Map this to the primary key column.
@GeneratedValue(strategy = GenerationType.AUTO) // Database will generate new primary keys, not us.
private Integer id;
@Column(length = 32) // Truncate column values to 32 characters.
private String name;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
The annotations are not method calls and will not, by themselves, do anything. Rather, the class object is passed to the JPA implementation at run-time, which then extracts the annotations to generate an object-relational mapping.
A complete example is given below:
package com.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD,
ElementType.CONSTRUCTOR,ElementType.ANNOTATION_TYPE,
ElementType.PACKAGE,ElementType.FIELD,ElementType.LOCAL_VARIABLE})
@Inherited
public @interface Unfinished {
public enum Priority { LOW, MEDIUM, HIGH }
String value();
String[] changedBy() default "";
String[] lastChangedBy() default "";
Priority priority() default Priority.MEDIUM;
String createdBy() default "James Gosling";
String lastChanged() default "2011-07-08";
}
package com.annotation;
public @interface UnderConstruction {
String owner() default "Patrick Naughton";
String value() default "Object is Under Construction.";
String createdBy() default "Mike Sheridan";
String lastChanged() default "2011-07-08";
}
package com.validators;
import javax.faces.application.FacesMessage;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.validator.Validator;
import javax.faces.validator.ValidatorException;
import com.annotation.UnderConstruction;
import com.annotation.Unfinished;
import com.annotation.Unfinished.Priority;
import com.util.Util;
@UnderConstruction(owner="Navin Gujarish")
public class –DateValidator implements Validator {
public void validate(FacesContext context, UIComponent component, Object value)
throws ValidatorException
{
String date = (String) value;
String errorLabel = "Please enter a valid date.";
if (!component.getAttributes().isEmpty())
{
errorLabel = (String) component.getAttributes().get("errordisplayval");
}
if (!Util.validateAGivenDate(date))
{
@Unfinished(changedBy = "Steve",
value = "whether to add message to context or not, confirm",
priority = Priority.HIGH
)
FacesMessage message = new FacesMessage();
message.setSeverity(FacesMessage.SEVERITY_ERROR);
message.setSummary(errorLabel);
message.setDetail(errorLabel);
throw new ValidatorException(message);
}
}
}
Processing[edit]
When Java source code is compiled, annotations can be processed by compiler plug-ins called annotation processors. Processors can produce informational messages or create additional Java source files or resources, which in turn may be compiled and processed, and also modify the annotated code itself. The Java compiler conditionally stores annotation metadata in the class files, if the annotation has a RetentionPolicy of CLASS or RUNTIME. Later, the JVM or other programs can look for the metadata to determine how to interact with the program elements or change their behavior.
In addition to processing an annotation using an annotation processor, a Java programmer can write their own code that uses reflections to process the annotation. Java SE 5 supports a new interface that is defined in the java.lang.reflect package. This package contains the interface called AnnotatedElement that is implemented by the Java reflection classes including Class, Constructor, Field,Method, and Package. The implementations of this interface are used to represent an annotated element of the program currently running in the Java Virtual Machine. This interface allows annotations to be read reflectively.
The AnnotatedElement interface provides access to annotations having RUNTIME retention. This access is provided by the getAnnotation, getAnnotations, and isAnnotationPresent methods. Because annotation types are compiled and stored in byte code files just like classes, the annotations returned by these methods can be queried just like any regular Java object. A complete example of processing an annotation is provided below:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
// This is the annotation to be processed
// Default for Target is all Java Elements
// Change retention policy to RUNTIME (default is CLASS)
@Retention(RetentionPolicy.RUNTIME)
public @interface TypeHeader {
// Default value specified for developer attribute
String developer() default "Unknown";
String lastModified();
String [] teamMembers();
int meaningOfLife();
}
// This is the annotation being applied to a class
@TypeHeader(developer = "Bob Bee",
lastModified = "2013-02-12",
teamMembers = { "Ann", "Dan", "Fran" },
meaningOfLife = 42)
public class SetCustomAnnotation {
// Class contents go here
}
// This is the example code that processes the annotation
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
public class UseCustomAnnotation {
public static void main(String [] args) {
Class<SetCustomAnnotation> classObject = SetCustomAnnotation.class;
readAnnotation(classObject);
}
static void readAnnotation(AnnotatedElement element) {
try {
System.out.println("Annotation element values: \n");
if (element.isAnnotationPresent(TypeHeader.class)) {
// getAnnotation returns Annotation type
Annotation singleAnnotation =
element.getAnnotation(TypeHeader.class);
TypeHeader header = (TypeHeader) singleAnnotation;
System.out.println("Developer: " + header.developer());
System.out.println("Last Modified: " + header.lastModified());
// teamMembers returned as String []
System.out.print("Team members: ");
for (String member : header.teamMembers())
System.out.print(member + ", ");
System.out.print("\n");
System.out.println("Meaning of Life: "+ header.meaningOfLife());
}
} catch (Exception exception) {
exception.printStackTrace();
}
}
}
See also[edit]
- JSR 250: Common Annotations for the Java Platform
- CLI Attributes
- Java programming
- Java virtual machine
- Model-driven architecture
- Python decorators, inspired by Java annotations, which have a similar syntax.
References[edit]
- ^ "Annotations". Sun Microsystems. Retrieved 2011-09-30..
- ^ Sun Microsystems (2005). Java(TM) Language Specification (3rd ed.). Prentice Hall. ISBN 0-321-24678-0..
- ^ Dare Obasanjo (2007). "A COMPARISON OF MICROSOFT'S C# PROGRAMMING LANGUAGE TO SUN MICROSYSTEMS' JAVA PROGRAMMING LANGUAGE: Metadata Annotations". Dare Obasanjo. Archived from the original on 2007. Retrieved 2012-09-20.
- ^ Coward, Danny (2006-11-02). "JSR 175: A Metadata Facility for the JavaTM Programming Language". Java Community Process. Retrieved 2008-03-05.
- ^ http://www.java2s.com/Tutorial/Java/0020__Language/TheBuiltInAnnotations.htm
External links[edit]
Annotations in Tiger, Part 1: 메타데이터를 자바 코드에 추가하기
프로그래밍, 특히 자바 프로그래밍의 최신 경향 중 하나는 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의 기본 어노테이션 유형이 사용하기 쉽다는 것을 알게될 것이다.
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
표준 어노테이션 유형을 살펴보자. 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
이다. 이것이 어떤 일을 수행하는지 알아내는 것은 쉽다. 하지만 왜 이 어노테이션이 중요한지는 분명하지 않다. 이는 실제로 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에서는 커스텀 어노테이션 유형을 작성하는 방법을 자세히 다루겠다. 자바 클래스를 만들고 이를 어노테이션 유형으로서 정의하는 방법, 컴파일러가 어노테이션 유형을 인식하게 하는 방법, 코드에 주석을 달 때 이를 사용하는 방법 등을 설명하겠다.
Annotations in Tiger, Part 2: 커스텀 어노테이션
이전글에서 메타데이터가 무엇이고, 이것이 가치 있는 이유와 J2SE 5.0 (Tiger)에 도입된 기본적인 빌트인 어노테이션을 사용하는 방법을 설명했다. 이제 이러한 개념들에 익숙해졌다면 Java 5가 제공하는 세 개의 표준 어노테이션이 특별히 강력한 것은 아니라는 것도 깨달았을 것이다. Deprecated
, SuppressWarnings
, Override
를 사용할 수 있을 뿐이다. 다행히도, Tiger를 사용하여 자신만의 어노테이션 유형을 정의할 수 있다. 이 글에서 몇 가지 예제를 들어 비교적 간단한 프로세스를 설명하겠다. 어노테이션에 주석을 다는 방법과 이를 통한 효용을 설명한다. O'Reilly Media, Inc.에 특별히 감사한다. 이들의 도움으로 내 저서의 어노테이션 챕터에서 코드 샘플을 인용할 수 있었다. (참고자료)
약간의 문법의 추가로(Tiger는 많은 문법상의 구조체를 추가해왔다.) 자바는 새로운 유형인 어노테이션 유형을 지원한다. 어노테이션 유형은 일반 클래스와 비슷해보이지만 독특한 속성이 있다. 가장 주목할만한 것은 클래스에서 @
(at)기호와 함께 사용하여 다른 자바 코드에 주석을 달 수 있다는 점이다.
새로운 어노테이션 유형을 정의하는 것은 하나의 인터페이스를 만드는 것과 많은 부분 같다. 단 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
이다. 이 메타-어노테이션은 자바 컴파일러가 주석이 달린 어노테이션 유형을 다루는 방법과 관련되어 있다. 컴파일러는 여러 옵션들이 있다:
- 주석이 달린 클래스의 컴파일 된 클래스 파일에 있는 어노테이션을 유지하다가 클래스가 첫 번째로 로딩될 때 이를 읽는다.
- 컴파일 된 클래스 파일에서 어노테이션을 유지하지만 런타임 시 이를 무시한다.
- 지시된 대로 어노테이션을 사용하지만 컴파일 된 클래스 파일에서 이를 버린다.
이 세 가지 옵션은 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 컴파일러가 이를 지원하고 작동도 이해하기 쉽다. 하지만 커스텀 어노테이션과 메타-어노테이션의 경우 힘들게 작업한 유형들이 개발 정황의 밖에서 어떤 의미를 갖고 있는지를 파악하기는 더 힘들다. 합리적으로 어노테이션을 사용하되 비웃음거리가 되지 않도록 하라. 어노테이션 장치는 가치가 있고 개발 프로세스에 도움이 된다.
어노테이션은 자바 SE5가 제공하는 핵심적인 변화 중 하나이며,
자바언어로 표현할 수 없지만 프로그램에서 전체적으로 표현해야 할 데이터를 기술하는 방법을 제공한다.
다시말해 어노테이션을 사용하면, 프로그램에서 컴파일러가 테스트하고, 검증해야 하는 부가정보를
정해진 형식으로 설명하는 것이 가능하다.
어노테이션의 문법은 매우 단순하게 구성되어 있으며, 대체로 언어에 @ 심볼을 추가하는 것으로 시작한다.
자바 SE5의 java.lang 은 크게 세 가지 목적의 어노테이션을 제공한다.
- @Override : 기반클래스의 메소드를 오버라이드 한 것을 표시힌다. 메소드 이름을 잘못 표기하거나 시그니처를 잘못 지정할 경우 컴파일 에러가 발생한다.
- @Deprecated : 해당 요소가 사용될 경우 컴파일러가 경고를 발생시킨다.
- @SuppressWarning : 부적절한 컴파일러의 경고를 제거하기 위해 사용한다. 자바 SE5의 초기버전에서는 이 어노테이션을 사용할 수는 있었찌만 실제로는 지원되지 않고 무시되었다.
* 기본문법
아래 예제에서 testExecute() 메소드에 @Test를 사용했다.
이 자체로는 아무런 작동을 하지 않지만, 컴파일러는 사용자가 빌드 경로 어딘가에 @Test 어노테이션에 대한 정의를 해 놓았을 것으로 판단한다.
- package annotation.begin;
- public class Testable {
- public void execute(){
- System.out.println("Executing.....");
- }
- @Test void testExecute(){ execute(); }
- }
* 어노테이션의 정의
위에서 언급한 어노테이션의 정의이다. 일반적인 인터페이스의 정의와 매우 유사하다.
컴파일러는 다른 자바 인터페이스와 마찬가지로 어노테이션을 클래스로 컴파일 한다.
- package annotation.begin;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface Test { }
어노테이션 정의시 @Target, @Retention 이라는 메타-어노테이션이 필요함.
@Target 은 어노테이션을 적용할 대상(예를들면 메소드 또는 필드)
@Retention 은 해당 어노테이션이 소스코드(SOURCE), 클래스파일(CLASS), 또는 런타임(RUNTIME) 중 어디에서 적용되는지 정의.
위의 @Test와 같이 요소를 가지지 않은 어노테이션은 표식 어노테이션(marker annotation)이라고 불린다.
다음 글에서는 위 내용에 이어서,
프로젝트의 유스케이스를 추적하는 어노테이션 작성과정을 정리해 보겠습니다.
1번 글에 이어서 annotation의 적용 사례로
프로젝트의 유스케이스를 추적하는 어노테이션을 작성하는 것을 정리해 보도록 하겠습니다.
프로젝트 관리자는 구현된 유스케이스의 수를 이용하여 프로젝트의 진척도를 알 수 있으며 개발자는 시스템 내의
비즈니스 규칙을 변경하거나 디버깅할 때 유스케이스를 쉽게 찾을 수 있으므로 프로젝트의 유지보수를 쉽게 할 수 있을것입니다.
- @UseCase 선언
UseCase.java
- //: annotation.usecase/UseCase.java
- package annotation.usecase;
- import java.lang.annotation.ElementType;
- import java.lang.annotation.Retention;
- import java.lang.annotation.RetentionPolicy;
- import java.lang.annotation.Target;
- @Target(ElementType.METHOD)
- @Retention(RetentionPolicy.RUNTIME)
- public @interface UseCase {
- public int id();
- public String description() default "no description";
- }
- 다음은 클래스의 메소드에 유스케이스 정보를 기록한 예제입니다.
PasswordUtils.java
- //: annotation.usecase/PasswordUtils.java
- package annotation.usecase;
- import java.util.List;
- public class PasswordUtils {
- @UseCase(id = 47, description = "password must contain at least one numeric")
- public boolean validatePassword(String password){
- return (password.matches("\\w*\\d\\w*"));
- }
- @UseCase(id = 48)
- public String encryptPassword(String password){
- return new StringBuilder(password).reverse().toString();
- }
- @UseCase(id = 49, description = "New password can't equal previously used ones")
- public boolean checkForNewPassword(List<String> prevPasswords, String password){
- return !prevPasswords.contains(password);
- }
- }
*어노테이션 프로세스 작성하기!
어노테이션을 읽고 처리할 수 있는 프로세서가 없다면 어노테이션은 단지 주석에 불과할 뿐입니다.
그러므로 어노테이션을 사용하는 과정에서 해당 프로세서를 정의하고 적용하는 것이 중요합니다.
자바 SE5는 이러한 툴을 만들기 위한 리플렉션 API의 확정성을 제공합니다.
- 다음은 어노테이션을 적용한 PasswordUtils 클래스를 읽고 리플렉션을 사용하여 @UseCase를 검색하는 어노테이션 프로세스 입니다.
UseCaseTracker.java
- package annotation.usecase;
- import java.lang.reflect.Method;
- import java.util.ArrayList;
- import java.util.Collections;
- import java.util.List;
- public class UseCaseTracker {
- public static void trackUseCases(List<Integer> useCases, Class<?> cl){
- for(Method m : cl.getDeclaredMethods()){
- UseCase uc = m.getAnnotation(UseCase.class);
- if(uc != null){
- System.out.println("Found Use Case :" + uc.id() + " " + uc.description());
- useCases.remove(new Integer(uc.id()));
- }
- }
- for(int i : useCases){
- System.out.println("Warning : Missing use case-" + i);
- }
- }
- /**
- * @param args
- */
- public static void main(String[] args) {
- List<Integer> useCases = new ArrayList<Integer>();
- Collections.addAll(useCases, 47, 48, 49, 50);
- trackUseCases(useCases, PasswordUtils.class);
- }
- }
-실행결과
*어노테이션 요소
UseCase.java에 정의된 @UseCase 태그는 int 형의 id요소와 String 형의 description 요소를 가지고 있습니다.
다음은 어노테이션 요소에 허용되는 타입의 목록입니다.
- 모든 기본형 타입(int, float, boolean 등)
- String
- enum
- Annotation
- 상기 타입의 배열
->포장클래스 (Wrapper Class) 는 사용할수 없다.
자 이정도면 어느정도 annotation 에 대한 감이 잡히시는지요?
시간이 된다면 이어서 어노테이션을 이용하여 데이터베이스 생성 스크립트를 만들어주는 예제에 대해서
정리해 보도록 하겠습니다.
출처 -
http://choija.com/204
[Java] 어노테이션(Annotation)
깊이 있게 생각해보지는 못했었다. 단순히 컴파일러에게 알려주기 위한 표식정도로 생각했었다. 그런데 Spring Roo 와
Spring3.0 과 같은 최신 프레임웍들의 변화 경향을 보면, 어노테이션을 적극활용하는 쪽으로 변화되고 있다. 어노테이션을
사용하여 코드의 작성량도 한결 줄어들었다고 한다. 어노테이션들의 어떤 특성을 활용한 것일까? 어노테이션이란 뭘까?
최신 프레임웍들에 변화경향을 보기에 앞서, 어노테이션에 대해서 먼저 알아보았다.
유연성이란 장점을 얻었지만, 단점 또한 존재한다. 프로그램 작성을 위해 매번 많은 설정파일을 작성해야 한다는 것이다.
그 규모가 커질수록 설정의 양도 많아지게 되며 이를 잘 구조화 할 수 있는 방법도 필요하게 된다. 또 하나의 단점은 이것보다 좀 더 크다. 도메인 데이터 처리정보가 Model 클래스, 서비스 클래스, XML 설정파일에 분산되어 있어서, 이를 확인하기 위해서는 Model , Service 클래스와 XML 설정파일을 모두 뒤져야 한다는 것이다.
3. Annotation의 사용했을 때의 장점은?
(엄밀히 말하면, 코드의 양은 줄어들지 않는다. 하지만 코드가 깔끔해지고, 어노테이션의 재사용도 가능해진다. )
그럼 이제부터 어노테이션을 사용해보자. 사용을 위해 먼저 선행지식들에 대해 잠깐 알아보자.
5. 일반적인 어노테이션의 용도
@Deprecated
마커 어노테이션으로 차후 버전에 지원되지 않을 수 있기 때문에 더 이상 사용되지 말아야할 메소드를 나타낸다.
특이하게 더이상 사용되지 말아야할 메소드와 같은 라인상에 놓여져야 한다. (이유모름)
(두 플래그 -deprecated 또는 Xlint:deprecated 중 하나와 javac 명령어를 사용하여 컴파일러 경고를 켜야만, 컴파일러 에러를 발생시켜준다)
@SupressWarning
의미데로 경고를 제거하는 어노테이션이다. Object형을 엘리먼트로 하는 컬렉션을 사용하면, 컴파일러 경고가 발생하는데 이 어노테이션을 사용하여 프로그래머의 의도적인 Object 형 사용임을 알려 경고를 제거할 수 있다.
7. 커스텀(Custom) 어노테이션
1) 커스텀 어노테이션 정의하기
public @interface InProgress { }
어노테이션 정의파일을 컴파일하고, 이 파일을 클래스패스에서 참조할 수 있으면 다른 소스코드상에서 어노테이션을 사용할 수 있다.
어노테이션 유형은 멤버변수를 가질 수 있으며, 이 변수들 컴파일시 또는 런타임에 메타-데이터로서 사용될 수 있다.
이렇게 정의를 하면 자동으로 accessor와 mutator를 제공해준다.
// 해당 태스크가 완료되야함 나타내는 어노테이션
public @interface TODO {
String value();
}
정의한 어노테이션을 다음과 같이 사용한다.
@TODO("Figure out the amount of interest per month")
public void calculateInterest(float amount, float rate) {
// ...
}
단일 멤버를 갖을 경우에만 위와 같이 사용할 수 있다.
3) 디폴트 값 설정하기
public @interface GroupTODO {
public enum Severity { CRITICAL, IMPORTANT, TRIVIAL, DOCUMENTATION };
Severity severity() default Severity.IMPORTANT;
String item();
String assignedTo();
String dateAssigned();
}
디폴트값을 사용한 예는 다음과 같다.
@GroupTODO(
item="Figure out the amount of interest per month",
assignedTo="Brett McLaughlin",
dateAssigned="08/04/2004"
)
public void calculateInterest(float amount, float rate) {
// ...
}
4) 메타-어노테이션
어노테이션에 사용되는 어노테이션으로 해당 어노테이션의 동작대상 및 보여타임을 결정한다.
① @Target 메타-어노테이션
어노테이션이 적용되는 동작 프로그래밍
엘리멘트를 지정하며, 다음과 같은 값들이 있다.
package java.lang.annotation;
public enum ElementType {
TYPE, // Class, Interface, enum
FIELD, // 프로퍼티 (enum 나열값들은 제외)
METHOD, // 메서드
PARAMETER, // 메서드 파라미터
CONSTRUCTOR, // 생성자
LOCAL_VARIABLE, // 로컬변수 또는 Catch문
ANNOTATION_TYPE, // 어노테이션(메타 어노테이션)_
PACKAGE // 자바 패키지
}
② @Retention 메타-어노테이션
자바 컴파일러가 어노테이션을 다루는 방법과 관련이 있다. 소스파일, 클래스파일, 런타임 중 어느시점까지 어노테이션을 보유하고 있을 것인지를 결정한다.
package java.lang.annotation;
public enum RetentionPolicy {
SOURCE, // 어노테이션이 컴파일러에 의해 버려짐
CLASS, // 어노테이션이 클래스파일에 저장되지만, JVM에게 무시됨
RUNTIME // 어노테이션이 클래스파일에 저장되며, JVM에 의해 읽혀짐
}
위 세값 중 하나를 인자로 취해 다음과 같이 사용된다.
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
...
}
Annotation을 실제로 사용하는 예제를 알아보자.
나중에 모든 유스케이스를 구현하는 모든 메소드들이 잘 구현되었는지 확인하기 위해 UseCaseTracker 를 사용하여 어노테이션 정보를 출력한다. (코드는 Thinking in Java 4E 에 있는 예제코드를 사용하였다. )
메서드에 사용할 어노테이션이므로 @Target을 ElementType.METHOD를 설정하였고,
런타임 시에 사용되기 때문에 @Rention을 RetentionPolicy.RUNTIME 로 설정하여 class 파일에 어노테이션 정보가 남도록 지정하였다.
package net.atgame.annotation.usecase; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface UseCase { public int id(); public String description() default "no description"; }
UseCase 어노테이션은 코드 작성간에는 주석의 역할을 하고, 코드 실행시에는 테스트로서의 역할도 한다.
import java.util.List; public class PasswordUtils { @UseCase(id = 47, description = "passwords must contain at least one numeric") public boolean validatePassword(String password) { return (password.matches("\\w*\\d\\w*")); } @UseCase(id = 48) public String encryptPassword(String password) { return new StringBuilder(password).reverse().toString(); } @UseCase(id = 49, description = "New passwords can't equal perviously used ones") public boolean checkForNewPassword(ListprevPasswords, String password) { return !prevPasswords.contains(password); } }
3. UseCaseTracker를 사용하여 모든 유스케이스를 구현하는 메소드들이 작성되었는지 확인한다.
아래 코드에서는 리플렉션을 사용하여 유스케이스 아이디 47, 48, 49, 50 을 사용하는 메소드들을 검색하여 그 메서드의 어노테이션 멤버를 출력하고 있다. 이 프로그램 실행을 통해 어떤 유스케이스가 아직 구현되지 않았는지 알 수 있다.
package net.atgame.annotation.usecase; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.List; public class UseCaseTracker { public static void trackUseCases(ListuseCases, Class cl) { for (Method m : cl.getDeclaredMethods()) { UseCase uc = m.getAnnotation(UseCase.class); if (uc != null) { System.out.println("Found Use Case:" + uc.id() + " " + uc.description()); useCases.remove(new Integer(uc.id())); } } for (int i : useCases) { System.out.println("Warning: Missing use case~" + i); } } public static void main(String[] args) { List useCases = new ArrayList (); Collections.addAll(useCases, 47, 48, 49, 50); trackUseCases(useCases, PasswordUtils.class); } }
이제 좀더 실용적인 두번째 예제를 소개하겠다. 데이터베이스 접속 처리를 하기위해 데이터베이스 종류에 맞는 SQL을 작성하여 DBMS에 전송해야한다. 데이터베이스에 맞는 SQL 작성! 이 상당히 코드를 지저분하게 만들수 있기 때문에 모듈화가 잘 고려되어야 한다. 데이터베이스별 SQL 작성을 안할 수는 없지만 어노테이션을 사용하면, DB 접속에 관한 부분을 클라이언트 프로그래머에게 투명하게 처리할 수 있다. 그 의미에 대해서 알아보자
1. 테이블명을 나타내기 위한 DBTable 어노테이션을 정의한다.
package net.atgame.annotation.database; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DBTable { public String name() default ""; }
2. 테이블 컬럼의 타입을 나타내기 위한 SQLInteger 와 SQLString 어노테이션을 정의한다.
package net.atgame.annotation.database; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface SQLInteger { String name() default ""; Constraints constraints() default @Constraints; }
3. 제약조건을 나타내기 위한 Constraint 어노테이션을 정의한다.
package net.atgame.annotation.database; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface SQLString { int value() default 0; String name() default ""; Constraints constraints() default @Constraints; }
4. Memeber 모델 클래스에 정의한 어노테이션들을 사용한다.
package net.atgame.annotation.database; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Constraints { boolean primaryKey() default false; boolean allowNull() default true; boolean unique() default false; }
5. TableCreator 클래스에서 어노테이션 정보를 사용하여, 데이베이스 생성 SQL를 만들어낸다.
package net.atgame.annotation.database; import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; public class TableCreator { public static void main(String[] args) throws Exception { if (args.length < 1) { System.out.println("arguments: annotated classes"); System.exit(0); } // 모든 엔티티 클래스를 읽음 for (String className : args) { Class cl = Class.forName(className); // 클래스 어노테이션 가져오기 DBTable dbTable = cl.getAnnotation(DBTable.class); if (dbTable == null) { System.out.println("No DBTable annotations in class " + className); continue; } String tableName = dbTable.name(); if (tableName.length() < 1) { tableName = cl.getName().toUpperCase(); } ListcolumnDefs = new ArrayList (); /// 필드목록 조회 for (Field field : cl.getDeclaredFields()) { String columnName = null; Annotation[] anns = field.getDeclaredAnnotations(); if (anns.length < 1) { continue; } if (anns[0] instanceof SQLInteger) { SQLInteger sInt = (SQLInteger)anns[0]; if (sInt.name().length() < 1) { columnName = field.getName().toUpperCase(); } else { columnName = sInt.name(); } columnDefs.add(columnName + " INT" + getConstraints(sInt.constraints())); } if (anns[0] instanceof SQLString) { SQLString sString = (SQLString)anns[0]; if (sString.name().length() < 1) { columnName = field.getName().toUpperCase(); } else { columnName = sString.name(); } columnDefs.add(columnName + " VARCHAR(" + sString.value() + ")" + getConstraints(sString.constraints())); } } StringBuilder createCommand = new StringBuilder("CREATE TABLE " + tableName + " ("); for (String columnDef : columnDefs) { createCommand.append("\n " + columnDef + ","); } // 마지막 쉼표제거 String tableCreate = createCommand.substring(0, createCommand.length()-1) + ");"; System.out.println("Table Creation SQL for " + className + " is :\n" + tableCreate); } } private static String getConstraints(Constraints con) { String constraints = ""; if (!con.allowNull()) { constraints += " NOT NULL"; } if (con.primaryKey()) { constraints += " PRIMARY KEY"; } if (con.unique()) { constraints += " UNIQUE"; } return constraints; } }
Model 클래스 어노테이션 정보를 읽어서 DB에 해당하는 SQL를 생성하고, 테이블을 생성하게 할 수 있다.
더 나아가 getter 와 setter 메소드를 오버라이드하여 인스턴스에 대한 조작만으로 데이터베이스 CRUD(등록,조회,수정,삭제) 연산을 처리하게 할 수도 있을 것이다. 이렇게 하면 데이터베이스 종류에 따른 번거로운 처리를 클라이언트 프로그래머에게
좀더 투명하게 사용할 수 있다.
이상 어노테이션을 사용하는 예제를 간략히 알아보았다. 어노테이션을 잘 활용하면 간결하고, 투명한 코드를 작성할 수 있다.
하지만 언제나 그렇듯 "잘 활용하면" 이라는 조건이 붙는다. 어떻게 잘 활용할지에 대한 생각은 이제 개인의 몫이다.
출처 - http://hiddenviewer.tistory.com/88
pointcut : 실제 로직에 적용되는 joinpoint
출처 - http://marobiana.egloos.com/1108974
'Development > Java' 카테고리의 다른 글
DAO, DTO, VO (2) | 2012.03.16 |
---|---|
Thread pool (0) | 2012.03.16 |
JVM 인코딩 설정(Dfile.encoding 옵션) (0) | 2012.03.13 |
자바빈즈(javabeans) (0) | 2012.03.11 |
java - StringTokenizer 클래스 및 split 메서드 (0) | 2012.03.09 |