
자바의 애노테이션에 대해 학습한다.
애노테이션은 주석이라는 뜻으로, 프로그램의 소스코드안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이다.
주석처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공한다.
예를들어, 자신이 작성한 소스코드 중에서 특정 메소드만 테스트하길 원한다면, 다음과 같이 @Test라는 애노테이션을 메서드 앞에 붙여 테스트 프로그램에게 해당 메소드가 테스트 대상임을 알린다.
@Test
public void method() {}
@Override는 애노테이션이고, Override는 애노테이션의 타입이다.
애노테이션 타입을 정의하는 방법은 다음과 같다.
@interface 애노테이션명 {
타입 요소명();
}
애노테이션 내에 선언된 메소드를 애노테이션의 요소라고 한다.
애노테이션의 요소는 반환값이 있고 매개변수는 없는 추상 메소드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다.
다만, 애노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다.
요소의 타입은 기본형, String, enum, 애노테이션, Class만 허용된다.
요소에 예외를 선언할 수 없다.
요소를 타입 매개변수로 정의할 수 없다.
요소의 이름도 같이 적어주므로 순서는 상관없다.
요소 타입이 배열일 경우, 괄호{}를 사용해서 여러 개의 값을 지정할 수 있다.
{}는 생략이 가능하다.요소가 하나도 정의되지 않은 애노테이션을 마커 애노테이션이라 한다.
@Override, @Test예제
@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="230212", hhmmss="202900")
)
public class MyClass {}
애노테이션의 각 요소는 기본값을 가질 수 있다.
@interface TestInfo {
int count() default 1; // 기본값을 1로 지정
}
모든 애노테이션의 조상이다.
다음은 Annotation의 구조이다.

따라서, 모든 애노테이션 객체에 대해 equals(), hashcode(), toString(), annotationType() 호출이 가능하다.
다음은 Annotation 클래스에 적용된 모든 애노테이션에 대해 해당 메소드들을 호출한다.
Class<AnnotationTest> cls = AnnotationTest.class;
Annotation[] annoArr = cls.getAnnotations();
for(Annotation anno : annoArr) {
System.out.println("toString(): " + anno.toString());
System.out.println("hashcode(): " + anno.hashCode());
System.out.println("equals(): " + anno.equals(anno));
System.out.println("annotationType(): " + anno.annotationType());
}
toString(): @java.lang.Deprecated(forRemoval=false, since="")
hashcode(): 2011250702
equals(): true
annotationType(): interface java.lang.Deprecated
애노테이션은 크게 JDK에서 기본적으로 제공하는 것(= 표준 애노테이션), 메타 애노테이션, 사용자 정의 애노테이션으로 구성되어 있다.
사용자 정의 애노테이션은 위의 애노테이션 정의에서 살펴보았다.
JDK에서 기본적으로 제공하는 애노테이션
주로 컴파일러에게 유용한 정보를 제공하기위해 사용한다.
java.lang.annotation 패키지에 포함되어 있다.다음은 대표적인 표준 애노테이션들이다.
컴파일러에게 해당 메소드가 오버라이딩한 메소드라는 것을 알린다.
해당 메서드가 조상 클래스에 있는지 확인하고, 없으면 컴파일 에러를 던진다.

오버라이딩할 때 메소드명을 잘못 적을 경우, 해당 사실을 알아채기 힘드므로 반드시 @Override를 붙여 실수를 미연에 방지하는 것이 좋다.
class Parent {
void parentMethod() {}
}
class Child extends Parent {
void parentmethod() {} // 오버라이딩하려 했으나 실수로 메서드명을 잘못 적음
}
새로운 버전의 JDK가 소개될 때, 새로운 기능이 추가될 뿐만 아니라 기존의 부족했던 기능들을 개선하기도 한다. 이 과정에서 기존의 기능을 대체할 것들이 추가되어도, 이미 여러 곳에서 사용되고 있는 기존의 것들을 함부로 삭제할 수 없다.
따라서 더 이상 사용되지 않는 필드나 메서드에 @Deprecated를 붙여 해당 필드나 메서드는 다른 것으로 대체되었으니 더 이상 사용하지 않을 것을 권한다.
예를들어 java.util.Date클래스의 getDate()에는 @Deprecated가 붙어있는데,
getDate()의 설명을 보면 해당 메서드 대신에 JDK 1.1버전 부터 추가된 Calendar.get()을 사용하라고 되어있다.

@Deprecated가 붙은 필드나 메소드를 사용할 경우, 컴파일할 때 아래와 같은 메시지가 나타난다.PS C:\...> javac AnnotationExample.java
Note: AnnotationExample.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.해당 소스파일이 deprecated된 대상을 사용하고 있으며, -Xlint:deprecation 옵션을 붙여 재컴파일하면 자세한 내용을 확인할 수 있다는 뜻이다.함수형 인터페이스를 선언할 때, 이 애노테이션을 붙이면 컴파일러가 함수형 인터페이스를 올바르게 선언했는지 확인하고 잘못된 경우 에러를 발생시킨다.
예를들어 함수형 인터페이스를 선언할 때 함수형 인터페이스는 추상 메소드가 하나뿐이어야 한다 제약을 어기면 컴파일 에러가 발생한다.


컴파일러가 보여주는 경고메시지가 나타나지 않게 억제(Suppress)해준다.
컴파일러의 경고메시지는 모두 확인하고 해결해서 컴파일 후에 어떠한 메시지도 나타나지 않도록 해야하지만,
경고가 발생할 것을 알면서도 묵언해야할 때는 해당 애노테이션을 사용한다.
@Deprecated가 붙은 대상을 사용해서 발생하는 경고를 억제ArrayList list = new ArrayList(); // 제네릭스로 타입을 지정하지 않음
list.add(obj); // 여기서 검증되지 않은 연산자(제네릭스를 사용하지 않은 ArrayList의 add)를 사용하여 unchecked 경고 발생PS C:\...> javac -Xlint AnnotationExample.java
AnnotationExample.java:9: warning: [unchecked] unchecked call to add(E) as a member of the rawtype ArrayList
list.add(1);
^
where E is a type-variable:
E extends Object declared in class ArrayList
...@SuppressWarnings("unchecked") // unchecked 경고 억제
class AnnotationExample {
public static void main(String[] args) {
ArrayList list = new ArrayList(); // 제네릭스로 타입을 지정하지 않음
list.add(obj); // 여기서 unchecked 경고 발생
}
}PS C:\...> javac -Xlint AnnotationExample.java
// unchecked 경고가 사라짐
...ArrayList list = new ArrayList(); // 제네릭스를 사용하지 않음, 여기서 rawtypes 경고 발생PS C:\...> javac -Xlint AnnotationExample.java
AnnotationExample.java:8: warning: [rawtypes] found raw type: ArrayList
ArrayList list = new ArrayList();
^
missing type arguments for generic class ArrayList<E>
where E is a type-variable:
E extends Object declared in class ArrayList
AnnotationExample.java:8: warning: [rawtypes] found raw type: ArrayList
ArrayList list = new ArrayList();
^
missing type arguments for generic class ArrayList<E>
where E is a type-variable:
E extends Object declared in class ArrayList@SuppressWarnings("rawtypes") // rawtypes 경고 억제
ArrayList list = new ArrayList(); // 제네릭스를 사용하지 않음, 여기서 rawtypes 경고 발생PS C:\...> javac -Xlint AnnotationExample.java()안에 문자열로 지정하면 된다.@SuppressWarnings("deprecation")()안에 배열처럼 괄호{}를 추가로 사용해야한다.@SuppressWarnings({"deprecation", "unchecked", "varargs"})-Xlint 옵션으로 컴파일해서 나타나는 경고의 내용 중에서 대괄호[] 안에 있는 것이 바로 메시지의 종류이다.PS C:\...> javac -Xlint AnnotationExample.java
AnnotationExample.java:8: warning: [deprecation] getDate() in Date has been deprecated
System.out.println(date.getDate());
^
1 warning@SuppressWarnings 애노테이션을 추가하면, 나중에 추가된 코드에서 발생할 수도 있는 경고까지 억제될 수 있으므로 해당 대상에만 @SuppressWarnings 애노테이션을 붙인다.네이티브 메소드에 의해 참조되는 상수 필드에 붙이는 애노테이션
@Native public static final long MIN_VALUE = 0x8000000000000000L;
🤔네이티브 메소드?
JVM이 설치된 OS의 메소드이다.
- 보통 C언어로 작성되어 있는데, 자바에서는 메소드의 선언부만 정의하고 구현은 하지 않는다.
- 네이티브 메소드를 호출하면 실제로 호출되는 것은 OS의 메소드이다.
- 사용자 정의의 네이티브 메소드를 사용할 때는 반드시 해당 네이티브 메소드와 OS의 메소드를 연결해주는 작업이 추가로 필요하다.
- JNI(Java Native Interface)가 해당 역할을 한다.
- 예시
public class Object { private static native void registerNatives(); // 네이티브 메소드 static { registerNatives(); } protected native Object clone() throws CloneNotSupportedException; public final native Class<?> getClass(); public final native void notify(); public final native void notifyAll(); public final native void wait(long timeout) throws InterruptedException; public native int hashCode(); }
애노테이션을 위한 애노테이션
즉, 애노테이션에 붙이는 애노테이션으로 애노테이션을 정의할 때 애노테이션의 적용대상(target)이나 유지기간(retention)을 지정한다.
java.lang.annotation 패키지에 포함되어 있다.애노테이션이 적용되는 대상을 지정하는데 사용된다.
대상의 종류는 다음과 같다.
| 대상 타입 | 의미 |
|---|---|
| ANNOTATION_TYPE | 애노테이션. 즉, 메타 애노테이션을 의미함 |
| CONSTRUCTOR | 생성자 |
| FIELD | 필드(멤버변수, enum 상수) |
| METHOD | 메소드 |
| PARAMETER | 매개변수 |
| LOCAL_VARIABLE | 지역변수 |
| PACKAGE | 패키지 |
| TYPE | 타입(클래스, 인터페이스, enum) |
| TYPE_PARAMETER | 타입 매개변수(자바 8버전) |
| TYPE_USE | 타입이 사용되는 모든 곳. 즉, 해당 타입의 변수를 선언할 때(자바 8버전) |
java.lang.annotation.ElementType이라는 열거형에 정의되어 있다.FIELD는 기본형에 TYPE_USE는 참조형에 사용된다.다음은 @SuppressWarnings애노테이션의 정의이다.
@Target애노테이션으로 @SuppressWarnings의 적용 대상을 지정하였다.
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
...
public @interface SuppressWarnings {
String[] value();
}
애노테이션이 유지(retention)되는 기간
다음은 유지 정책의 종류이다.
java.lang.annotation.RetentionPolicy라는 열거형에도 정의되어 있다.
소스 파일에만 존재하고, 클래스파일에는 존재하지 않는다.
@Override, @SuppressWarnings클래스 파일에 존재하고, 실행시에는 사용이 불가능하다.
CLASS유지 정책이 기본값임에도 불구하고 잘 사용되지 않는 이유이다.클래스 파일에 존재하고, 실행시에 사용이 가능하다.
@FunctionalInterface : @Override처럼 컴파일러가 체크해주는 애노테이션이지만, 실행 시에도 사용되므로 유지 정책이 RUNTIME이다.@Documented가 붙은 애노테이션(D 애노테이션이라 하자)을 사용하는 대상이 D 애노테이션을 사용하고 있음을 javadoc에 표시해준다.
@Override와 @SuppressWarnings를 제외한 모든 애노테이션에는 이 애노테이션이 붙어있다.💡javadoc
자바에서 지정한 형태의 주석들을 인식하여 html로 된 api문서 형태를 만들어주는 도구이다.
@Inherited가 붙은 애노테이션이 자손 클래스에 상속되도록 한다.
보통은 하나의 대상에 같은 이름의 애노테이션은 한 번만 붙이는데,
@Repetable이 붙은 애노테이션은 하나의 대상에 여러 번 붙일 수 있다.
따라서 일반적인 애노테이션과는 달리 같은 이름의 애노테이션이 하나의 대상에 여러번 적용될 수 있기 때문에, 이 애노테이션들을 하나로 묶어서 다룰 수 있는 애노테이션도 추가로 정의해야 한다.
@Repetable(ToDos.class) // ToDo 애노테이션을 하나의 대상에 여러 번 반복해서 사용할 수 있다.
@interface ToDo {
String value();
}
// 여러 개의 ToDo 애노테이션을 담을 컨테이너 애노테이션 ToDos
@interface ToDos {
ToDo[] value(); // ToDo 애노테이션 타입의 배열을 선언. 이름이 반드시 value여야 한다.
}
@Todo("delete test codes.")
@Todo("override inherited methods.")
class MyClass {}
@Retention이 SOURCE인 애노테이션은 컴파일러가 사용하는 애노테이션이라는 뜻이다.
해당 애노테이션을 사용하기위해서는 해당 애노테이션을 인식할 수 있는 컴파일러를 직접 구현해야 한다. 이때 컴파일러 플러그인의 일종으로 사용자 정의 애노테이션을 인식할 수 있도록 사용하는 것이 애노테이션 프로세서이다.
즉, 애노테이션 프로세서란 컴파일 타임에 특정 애노테이션을 스캔하고 처리하기 위해 javac에서 확장해서 사용하는 도구이다.
애노테이션 프로세서 실행 과정은 다음과 같다.
해당 과정에서 주의할 점은 애노테이션 프로세서가 여러 개 있을 때, 프로세서의 실행 순서가 잘못 되어있을 경우 오류가 날 수 있다.
애노테이션 프로세서의 대표적인 종류로는 Lombok, JPA, QueryDSL, MapStruct가 있다.
AbstractProcessor라는 Processor 인터페이스를 구현한 추상클래스를 확장해서 자체적인 애노테이션 프로세서를 만들 수 있다.
AbstractProcessor는 javax.annotation.processing 패키지안의 애노테이션 프로세서 API에 존재한다.@SupportedSourceVersion(SourceVersion.RELEASE_11)
@SupportedAnnotationTypes("*")
public class MyProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
return false;
}
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}
}
process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)main() 메소드로, 애노테이션에 대한 스캔, 평가, 프로세싱과 자바 파일 생성에 대한 코드를 작성한다.RoundEnvironment 파라미터는 애노테이션 프로세서가 애노테이션 프로세싱의 라운드를 쿼리할 수 있도록 해준다.@SupportedAnnotationTypes({애노테이션명1, ...}) @SupportedSourceVersion(SourceVersion.RELEASE_11)SourceVersion.latestSupported()를 사용하지만, 특정 버전을 정의하는 경우엔 SourceVersion.RELEASE_6과 같은 식으로 정의해줄 수 있다. init(ProcessingEnvironment processingEnv) init()메서드를 사용하는 방법이 있다. ProcessingEnvironment 타입은 몇가지 유용한 유틸리티 클래스인 Elements, Types, Filer를 제공한다.MyProcessor.jar
- dev
- annotationprocessor
- MyProcessor.class
- META-INF
- services
- javax.annotation.processing.Processorjavax.annotation.processing.Processor 파일에 애노테이션 프로세서 클래스들의 목록을 작성한다.dev.annotationprocessor.MyProcessor
com.foo.BarProcessor
...jar 파일로 패키징한다.MyProcessor.jar를 둔다. javax.annotation.processing.Processor를 감지한다. MyProcessor 애노테이션 프로세서를 등록한다.애노테이션 프로세서 생성에 대한 자세한 설명은 다음 사이트를 참고하면 좋을 것 같다.
Reference
- 자바의 정석 3rd Edition, 남궁성 지음
- 백기선님 온라인 스터디 12주차 - Annotation
- 온라인 자바 스터디#12- 어노테이션(@retention, @target, @documented, 어노테이션 프로세서) - 슝스