어노테이션이란? 주석처럼 프로그래밍 언어에 영향을 미치지 않으며, 유용한 정보를 제공

다음은 모든 어노테이션의 조상인 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

@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하지 않았습니다.

@Deprecated

새로운 버전의 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;
        			  ^

@FunctionalInterface

'함수형 인터페이스(functional interface)'를 선언할 때, 이 어노테이션을 붙이면 컴파일러가 '함수형 인터페이스'를 올바르게 선언했는지 확인하고, 잘못된 경우 에러를 발생시킨다.

함수형 인터페이스는 추상 메서드가 하나뿐이어야 한다는 제약이 있다.

	@FunctionalInterface
    public interface Runnalbe {
    	public abstract void run();	// 추상 메서드
    }

@SuppressWarnings

컴파일러가 보여주는 경고메세지가 나타나지 않게 만든다. 컴파일러의 경고 메세지는 무시하고 넘어갈 수도 있지만, 모두 확인하고 해결해서 컴파일 후에 어떠한 메세지도 나타나지 않게 해야 한다.

가끔 경고는 묵인해야 할 때가 있는데 이 경고를 그대로 놔두면 컴파일시마다 경고가 나타날 것이다. 따라서 묵인해야 할 경고들은 @SuppressWarnings를 붙여서 경고 메시지를 없앨 수 있다.

주로 @SuppressWarnings로 억제하는 경고는 "deprecation", "unchecked", "rawtypes", "varargs"정도이다.

	@SuppressWarnings("unchecked")	// 제네릭 관련된 경고 억제
    ArrayList list = new ArrayList(); // 제네릭 타입을 지정하지 않음.
    list.add(obj);	// 여기서 경고가 발생하지만 억제됨.

만약 둘 이상의 경고를 억제하려면 배열처럼 괄호{}를 추가로 사용해야 한다.

	@SuppressWarnnings({"deprecation", "unchecked", "varargs"})

메타 어노테이션

앞서 설명한 바와 같이 어노테이션을 위한 어노테이션이다.
어노테이션을 정의할 때 어노테이션의 적용대상이나 유지기간등을 지정하는데 사용된다.

@Target

어노테이션이 적용가능한 대상을 지정한다. 아래는 @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

어노테이션이 유지(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'유지 정책은 기본값임에도 불구하고 잘 사용되지 않는다.

@Documented, @Inherited

@Documented

어노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다. 자바 기본 어노테이션 중 '@Override'와 '@SuppressWarnings'를 제외하고는 이 메타 어노테이션이 붙어있다.

※ 사용할 일이 별로 없다.

@Inherited

어노테이션이 자손 클래스에 상속돼게 한다. '@Inherited'가 붙은 어노테이션을 조상 클래스에 붙이면, 자손 클래스도 이 어노테이션이 붙은 것과 같이 인식된다.

	@Inherited
    @interface SuperAnno {}
    
    @SuperAnno
    class Parent {}
    
    class Child extends Parnet {} // Child에 어노테이션이 붙은 것으로 인식

@Repeatable

'@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()를 호출한다.

마커 어노테이션(Marker Annotation)

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"/로 바뀌면서 무시된다.

0개의 댓글