어노테이션이란? 주석처럼 프로그래밍 언어에 영향을 미치지 않으며, 유용한 정보를 제공
다음은 모든 어노테이션의 조상인 Annotation인터페이스의 소스코드의 일부이다.
/**
* The common interface extended by all annotation types. Note that an
* interface that manually extends this one does <i>not</i> define
* an annotation type. Aloso note that this interface does not itself
* define an annotation type.
...
* The {@link java.lang.reflect.AnnotatedElement} interface discusses
* compatibility concerns when evolving an annotation type from being
* non-repeatable to being repeatable.
*
* @author Josh bloch
* @since 1.5
*/
public interface Annotation {
...
자바를 개발한 사람들은 소스코드에 대한 문서를 따로 만들기보다 소스코드와 문서를 하나의 파일로 관리하는 것이 낫다고 생각해 소스코드의 주석'/* ~ /'에 소스코드에 대한 문서를 저장하고, 해당 소스코드로부터 HTML문서를 생성해내는 javadoc.exe를 만들어서 사용했다.
'/**'로 시작하는 주석 안에 소스코드에 대한 설명들이 있고, 그 안에 '@'이 붙은 태그 들이 눈에 띌 것이다. 미리 정의된 태그들을 이용해 주석 안에 정보를 저장하고 javadoc.exe가 이 정보를 읽어서 문서를 작성한다.
이 기능을 응용하여 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 어노테이션이다. 어노테이션은 주석 처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다.
어노테이션은 JDK에서 기본 제공하는 것과, 다른 프로그램에서 제공하는 것들이 있는데, 어느 것이든 그저 약속된 형식으로 정보를 제공하기만 하면 될 뿐이다.
JDK에서 제공하는 표준 어노테이션은 주로 컴파일러를 위한 것으로 컴파일러에게 유용한 정보를 제공한다. 그리고 새로운 어노테이션을 정의할 떄 사용하는 메타 어노테이션도 제공한다.
JDK에서 제공하는 어노테이션은'java.lang.annotation'패키지에 포함돼 있다.
@Test
public void method() {
...
}
해당 @Test의 의미는 JUnit이라는 단위테스트 프로그램에 이 메서드가 테스트 대상이라는 것을 알리는 기능을 한다.
자바에서 기본적으로 제공하는 어노테이션은 몇 개 없다. 그나마 일부는 '메타 어노테이션'으로 어노테이션을 정의하는데 사용되는 어노테이션이다.
*이 붙은 어노테이션은 메타 어노테이션이다.
@Override 어노테이션은 오버라이딩을 올바르게 했는지 컴파일러가 체크하게 한다.
오버라이딩할 때 메서드이름을 잘못적는 실수를 하는 경우가 많다.
class Parent {
void parentMethod() {}
}
class Child extends Parent {
void parentmethod() {} // M을 소문자로 적어 오버라이딩x
}
// 컴파일 에러 발생x 다만 실제 코드 실행시 의도대로 오버라이딩 한 메서드가 아닌
// Parent클래스의 parentMethod()가 실행될 가능성이 매우 큼.
// 이럴때 아래와 같이 @Override를 붙여주면 컴파일러가 Parent클래스에는
parentmethod()가 없다는 것을 컴파일 타임에 알려줌
class Child2 extends Parent {
@Override
void parentmethod() {} // 컴파일 에러 발생
}
에러 내용 ↓
AnnotationExample.java:6: error: method does not override or implement a method
from a supertype
@Override
^
error: 메서드가 조상타입의 메서드로부터 override 혹은 implement하지 않았습니다.
새로운 버전의 JDK가 소개될 때, 기존에 있던 기능을 개선하여 새로운 기능들을 선보였다고하자, 이런 경우 기존에 있던 기능들을 삭제하면 호환성에 심각한 문제를 야기하기 때문에 호환성을 중요시하는 자바의 개발자들은 구 기능들을 @Deprecated라는 어노테이션을 사용해 사용을 지양할것을 알린다.
대체 가능한 더 좋은 필드나 메서드가 존재한다는 뜻으로 보면 된다.
@Deprecated
public int getDate() {
return nomalize().getDayOfMonth();
}
위 코드는 java.util.Date클래스의 getDate()메서드이다. Date클래스의 대부분 메서드는 @Deprecated가 붙어있다.
참고로 Date클래스는 Calendar클래스로 대체됐다.
만일 '@Deprecated'가 붙은 대상을 사용하는 코드를 작성한다면, 컴파일할 때 아래와 같은 메시지가 나타난다.
Note: AnnotationExample.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
해당 'AnnotationExample.java'가 deprecated된 대상을 사용하고 있고 '-Xlint:deprecation'옵션을 붙여서 다시 컴파일 하면 자세한 내용을 알 수 있다는 뜻이다.
C:\jdk1.8\work>ch12>javac -Xlint:deprecation AnnotationExample.java
AnnotationExample.java:21: warning: [deprecation] onField in NewClass
has been deprecated
nc.oldField = 10;
^
'함수형 인터페이스(functional interface)'를 선언할 때, 이 어노테이션을 붙이면 컴파일러가 '함수형 인터페이스'를 올바르게 선언했는지 확인하고, 잘못된 경우 에러를 발생시킨다.
함수형 인터페이스는 추상 메서드가 하나뿐이어야 한다는 제약이 있다.
@FunctionalInterface
public interface Runnalbe {
public abstract void run(); // 추상 메서드
}
컴파일러가 보여주는 경고메세지가 나타나지 않게 만든다. 컴파일러의 경고 메세지는 무시하고 넘어갈 수도 있지만, 모두 확인하고 해결해서 컴파일 후에 어떠한 메세지도 나타나지 않게 해야 한다.
가끔 경고는 묵인해야 할 때가 있는데 이 경고를 그대로 놔두면 컴파일시마다 경고가 나타날 것이다. 따라서 묵인해야 할 경고들은 @SuppressWarnings를 붙여서 경고 메시지를 없앨 수 있다.
주로 @SuppressWarnings로 억제하는 경고는 "deprecation", "unchecked", "rawtypes", "varargs"정도이다.
@SuppressWarnings("unchecked") // 제네릭 관련된 경고 억제
ArrayList list = new ArrayList(); // 제네릭 타입을 지정하지 않음.
list.add(obj); // 여기서 경고가 발생하지만 억제됨.
만약 둘 이상의 경고를 억제하려면 배열처럼 괄호{}를 추가로 사용해야 한다.
@SuppressWarnnings({"deprecation", "unchecked", "varargs"})
앞서 설명한 바와 같이 어노테이션을 위한 어노테이션이다.
어노테이션을 정의할 때 어노테이션의 적용대상이나 유지기간등을 지정하는데 사용된다.
어노테이션이 적용가능한 대상을 지정한다. 아래는 @SuppressWarnings의 실제 소스이다. 이 어노테이션을 적용할 수 있는 대상을 @Target으로 지정한 것이다.
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
@Target으로 지정할 수 있는 어노테이션 적용대상의 종류는 아래 표를 참고하자.
위의 표의 값들은 java.lang.annotation.ElementType이라는 enum에 정의 되어 있으며 아래와 같이 static import문을 쓰면 ElementType.TYPE을 TYPE과 같이 줄여서 사용할 수 있다.
import static java.lang.annotation.ElementType.*;
@Target({FIELD, TYPE, TYPE_USE}) // 적용 대상이 FIELD, TYPE, TYPE_USE
public @interface MyAnnotation { } // MyAnnotation을 정의
@MyAnnotation // 적용 대상이 TYPE인 경우
class MyClass {
@MyAnnotation // 적용 대상이 FIELD인 경우
int i;
@MyAnnotation // 적용 대상이 TYPE_USE인 경우
MyClass mc;
}
어노테이션이 유지(retention)되는 기간을 지정하는데 사용하고 유지 정책은 아래 표와 같다.
@Override나 @SuppressWarnings처럼 컴파일러가 사용하는 어노테이션은 유지 정책이 SOURCE이다. 컴파일러를 직접 작성할 것이 아니면, 이 유지정책은 필요없을 것이다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}
유지 정책을 'RUNTIME'으로 하면, 실행 시에 '리플렉션(reflection)'을 통해 클래스 파일에 저장된 어노테이션의 정보를 읽어서 처리할 수 있다. '@FunctionalInterface'는 '@Override'처럼 컴파일러가 체크해 주지만 실행 시에도 사용되므로 유지정책이 'RUNTIME'이다.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
'CLASS'유지 정책은 컴파일러가 어노테이션의 정보를 클래스 파일에 저장할 수 있게는 하지만, 클래스 파일이 JVM에 로딩될 때에는 어노테이션의 정보가 무시되어 실행 시에 어노테이션에 대한 정보를 얻을 수 없게된다. 따라서 'CLASS'유지 정책은 기본값임에도 불구하고 잘 사용되지 않는다.
어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다. 자바 기본 어노테이션 중 '@Override'와 '@SuppressWarnings'를 제외하고는 이 메타 어노테이션이 붙어있다.
※ 사용할 일이 별로 없다.
어노테이션이 자손 클래스에 상속돼게 한다. '@Inherited'가 붙은 어노테이션을 조상 클래스에 붙이면, 자손 클래스도 이 어노테이션이 붙은 것과 같이 인식된다.
@Inherited
@interface SuperAnno {}
@SuperAnno
class Parent {}
class Child extends Parnet {} // Child에 어노테이션이 붙은 것으로 인식
'@Repeatable'이 붙은 어노테이션은 여러 번 사용할 수 있게 된다.
@Repeatable(ToDos.class) // Todo어노테이션을 여러 번 사용 가능하게 함.
@interface ToDo {
String value();
}
위와 같이 @ToDo를 정의하고 아래와 같이 여러번 사용할 수 있다.
@ToDo("delete test codes.")
@ToDo("override inherited methods")
class MyClass { ... }
일반적인 어노테이션과는 달리 같은 이름의 어노테이션이 하나의 대상에 여러 번 적용될 수 있기 때문에, 이 어노테이션들을 하나로 묶어 다룰 수 있는 어노테이션도 추가로 정의해야 한다.
@interface ToDos { // 여러 개의 ToDo어노테이션을 담을 컨테이너 어노테이션 ToDos
ToDo[] value(); // ToDo어노테이션 배열타입의 요소를 선언. 이름이 반드시 value이어야만 한다.
}
@Repeatable(ToDos.class) // 괄호 안에 컨테이너 어노테이션을 지정해 줘야한다.
@interface ToDo {
String value();
}
새로운 어노테이션을 정의하는 방법은 아래와같이 interface앞에 @만 붙이면 된다.
@interface 어노테이션이름 {
타입 요소이름(); // 어노테이션의 요소를 선언한다.
...
}
어노테이션 내에 선언된 메서드를 '어노테이션 요소(element)'라고 한다. 아래 TestInfo어노테이션은 5개의 요소를 갖는다.
@interface TestInfo {
int count();
String testedBy();
String[] testTools();
TestType testType(); // enum TestType { FIRST, FINAL }
DateTime testDate(); // 자신이 아닌 다른 어노테이션(@DateTime)을 포함할 수 있다.
}
@interface DateTime {
String yymmdd();
String hhmmss();
}
어노테이션의 요소는 반환값이 있고 매개변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현할 필요가 없다. 다만 어노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다. 요소의 이름도 같이 적어주므로 순서는 상관없다.
@TestInfo (
count = 3, testedBy="Kim",
testTools = {"JUnit", "AutoTester"},
testType = TestType.FIRST,
testDate = @DateTime(yymmdd="160101", hhmmss="235959")
)
public class NewClass { ... }
어노테이션의 각 요소는 기본값을 가질 수 있으며, 기본값이 있는 요소는 어노테이션을 적용할 때 값을 지정하지 않으면 기본값이 사용된다.
기본값으로 null을 제외한 모든 리터럴이 가능하다.
@interface TestInfo {
int count() default 1; // 기본값을 1로 지정
}
@TestInfo // @TestInfo(count = 1) 과 동일
public class NewClass { ... }
기본 값이 여러개인 경우 아래와 같이 중괄호를 사용한다.
@interface TestInfo {
int count() default {1, 2};
}
어노테이션의 요소가 오직 하나이고 이름이 value라면 어노테이션 적용시 요소의 이름을 생략할 수 있다.
interface TestInfo {
String value();
}
@TestInfo("passed") // @TestInfo(value="passed")와 동일
class NewClass { ... }
요소의 타입이 배열이라면 괄호{}를 사용하여 여러 개의 값을 지정할 수 있다.
@interface TestInfo {
String[] testTools();
}
@Test(testTools={"JUnit", "AutoTester"}) // 값이 여러개인 경우
@Test(testTools="JUnit") // 값이 하나일 때는 중괄호 생략 가능
@Test(testTools={}) // 값이 없을 때는 필히 중괄호 있어야 함.
요소의 타입이 배열일 때에도 이름이 value라면 요소의 이름을 생략할 수 있다
ex) @SuppressWarnings
@interface SuppressWarnings {
String[] value();
}
그래서 @SuppressWarnings를 사용할 때 아래와 같이 사용했던 것이다.
// @SuppressWarnings(value = {"deprecation", "unchecked"})
@SuppressWarnings({"deprecaton", "unchecked"})
class NewClass { ... }
모든 어노테이션의 조상은 Annotation이다. 그러나 어노테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotaton을 조상으로 지정할 수는 없다.
단순히 문법이다.
@interface TestInfo extends Annotation { // 에러. 허용되지 않는 표현
int count();
String testedBy();
...
}
게다가 Annotation은 어노테이션(@interface)으로 정의된게 아니라 일반적인 인터페이스(interface)로 정의돼 있다.
package java.lang.annotation;
public interface Annotation {
boolean equals(Object obj);
int hashCode();
String toString();
Class<? extends Annotation> annotationType();
}
Annotation인터페이스가 위와 같이 정의돼 있기 때문에, 모든 어노테이션 객체에 대해 equals(), hashCode(), toString()과 같은 메서드를 호출할 수 있다.
Annotation인터페이스의 메서드들은 컴파일러가 자동으로 구현해주기 때문에 사용하는데 문제가 없는것이다.
class<AnnotationTest> cls = AnnotationTest.class;
Annotation[] annoArr = cls.getAnnotations();
for(Annotation a : annoArr) {
System.out.println("toString() : " + a.toString());
System.out.println("hashCode() : " + a.hashCode());
System.out.println("equals() : " + a.eqauls(a));
System.out.println("annotationType() : " + a.annotationType());
}
위의 코드는 AnnotationTest 클래스에 적용된 모든 어노테이션에 대해 toString(), hashCode(), equals()를 호출한다.
Serializable이나 Cloneable인터페이스처럼 요소가 하나도 정의되지 않은 어노테이션을 마커 어노테이션이라고 한다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {} // 마커 어노테이션. 정의된 요소가 하나도 없다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Test {} // 마커 어노테이션. 정의된 요소가 하나도 없다.
@Test // Test 메서드임
public void method() {
...
}
@Deprecated
public int getDate() {
...
}
위 두 메서드의 어노테이션도 마커 어노테이션으로 요소가 없어 ()안에 적을 필요가 없다.
어노테이션 요소를 선언할때는 아래의 규칙을 지켜야한다.
- 요소의 타입은 기본형, String, enum, 어노테이션, Class만 허용된다.
- ()안에 매개변수를 선언할 수 없다.
- 예외를 선언할 수 없다.
- 요소를 타입 매개변수로 정의할 수 없다.
@interface AnnoTest {
int id = 100; // 상수 선언 OK. static final int id = 100;
String major(int i, int j); // 에러. 매개변수를 선언할 수 없음.
String minor() throws Exception; // 에러. 예외를 선언할 수 없음.
ArrayList<T> list(); // 에러. 요소의 타입에 타입 매개변수 사용 불가.
}
import java.lang.annotation.*;
@Deprecated
@SuppressWarnings("1111") // 유효하지 않은 어노테이션은 무시된다.
@TestInfo(testedBy="jsd", testDate = @DateTime(yymmdd="160101", hhmmss="235959"))
public class AnnotationExample {
public static void main(String[] args) {
// AnnotationExample의 Class객체를 얻는다.
Class<AnnotationExample> cls = AnnotationExample.class;
TestInfo anno = cls.getAnnotation(TestInfo.class);
System.out.println("anno.testedBy() = " + anno.testedBy());
System.out.println("anno.testDate().yymmdd() = " + anno.testDate().yymmdd());
System.out.println("anno.testDate().hhmmss() = " + anno.testDate().hhmmss());
for(String str : anno.testTools())
System.out.println("testTools = " + str);
System.out.println();
// AnnotationExample에 저용된 모든 어노테이션을 가져온다.
Annotation[] annoArr = cls.getAnnotations(); // 무시된 어노테이션은 제외(@SuppressWarnings())
for(Annotation a : annoArr)
System.out.println(a);
// @SuppressWarnings() 어노테이션을 제외한 Deprecated, TestInfo어노테이션만 출력된다.
}
}
@Retention(RetentionPolicy.RUNTIME)
@interface TestInfo {
int count() default 1;
String testedBy();
String[] testTools() default "JUnit";
TestType testType() default TestType.FIRST;
DateTime testDate();
}
@Retention(RetentionPolicy.RUNTIME)
@interface DateTime {
String yymmdd();
String hhmmss();
}
enum TestType {
FIRST, FINAL
}
전체적으로 사용한 어노테이션 예제이다. '@SuppressWarnings("1111")'은 잘못된 값이기에 /"1111"/로 바뀌면서 무시된다.