" 자바의 애노테이션에 대해 학습하세요. "
@retention
@target
@documented
애노테이션은 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이다. 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다.
// java.lang.annotation.Annotation
이 애노테이션의 조상이다.
📌 메타 데이터란?
데이터를 위한 데이터를 의미하며, 한 데이터에 대한 설명을 의미하는 데이터
새로운 애노테이션을 정의하는 방법은 '@' 기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일합니다.
@interface 애노테이션이름 {
타입 요소이름(); // 애노테이션의 요소를 선언한다.
}
애노테이션 내에 선언된 메서드를 '에너테이션의 요소(element)'라고 하며, 아래에 선언된 TestInfo 애너테이션은 다섯 개의 요소를 갖는다.
@interface TestInfo {
int count();
String testedBy();
String[] testTools();
TestType testType(); // enum TestType { FIRST, FINAL }
DateTime testDate(); // 자신이 아닌 다른 애너테이션(@DateTime)을 포함할 수 있다.
}
애너테이션의 요소는 반환값이 있고 매개변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다. 다만, 애너테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다. 요소의 이름도 같이 적어주므로 순서는 상관 없다.
애노테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙은 다음과 같다.
@interface AnnoTest {
int id = 100; // OK. 상수 선언. static final int id = 100;
String major(int i, int j); // 에러. 매개변수를 선언할 수 없음
String minor() throws Exception; // 에러. 예외를 선언할 수 없음
ArrayList<T> list(); // 에러. 요소의 타입에 타입 매개변수 사용불가
}
자바에서 제공되는 애노테이션은 크게 2가지로 구성되어있다.
하나는 자바 코드를 작성할 때 사용되는 어노테이션이고, 다른 하나는 어노테이션을 정의하기 위해 필요한 것들이다.
@Override
메소드 앞에만 붙일 수 있는 애노테이션으로 오버라이드를 할 때 사용되며, 메소드가 오버라이드 되었는지 컴파일러에게 알려주는 역할을 한다. 이것은 생략 가능하다.
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
위와 같은 구조로 되어있으며 다음과 같이 사용한다.
class Parent {
void parentMethod() { }
}
class Child extends Parent {
@Override
void parentMethod() { }
}
@Deprecated
이 애노테이션이 붙은 대상은 다른 것으로 대체되었으니 더 이상 사용하지 않을 것을 권한다는 의미이다. 예를 들어, java.util.Date
클래스의 대부분의 메소드에는 @Deprecated
가 붙어있는데, Java API에서 Date클래스의 getDate()
를 보면 다음과 같이 적혀있다.
int getDate() {
Deprecated.
As of JDK version 1.1, replaced by Calendar.get(Calendar.DAY_OF_MONTH).
}
이 클래스, 메소드는 더이상 사용하지 말고, Calendar 클래스 사용을 권장하고 있다.
더 이상 사용을 권장하지 않는 클래스를 자바 자체에서 삭제하면 되지 않을까란 의문이 생길 수도 있겠으나, 이 방법을 사용하면 기존에 Date 클래스를 통해 잘 동작하던 프로그램이 동작하지 않게 되어 개발자가 일일이 그것들을 찾아다니면서 수정해야한다. 결국, 호환성 때문에 존재하는 어노테이션이 @Deprecated
이다.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
/**
* Returns the version in which the annotated element became deprecated.
* The version string is in the same format and namespace as the value of
* the {@code @since} javadoc tag. The default value is the empty
* string.
*
* @return the version string
* @since 9
*/
String since() default "";
/**
* Indicates whether the annotated element is subject to removal in a
* future version. The default value is {@code false}.
*
* @return whether the element is subject to removal
* @since 9
*/
boolean forRemoval() default false;
}
애노테이션은 인터페이스로 되어있기 때문에 구현이 되지 않는다는 것을 알 수 있다. 다만, default, static은 작성되지 않는다.
@FunctionalInterface
'함수형 인터페이스(functional interface)'를 선언할 때, 이 애노테이션을 붙이면 컴파일러가 함수형 인터페이스를 올바르게 선언했는지 확인하고, 잘못된 경우 에러를 발생시킨다.
함수형으로 작성할 때는 몇 가지 규칙이 있는데, 그 중 중요한 것은 '인터페이스는 단 하나의 메소드만 존재해야 한다' 는 것이다. 이 규칙을 깨면 함수형으로 만들 수 없다.
// 추후에 람다에서 자세히 다룰 예정임.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
@SuppressWarnings
컴파일러가 보여주는 경고 메시지가 나타나지 않게 억제해준다.
억제 코드 | 억제 내용 |
---|---|
deprecation | @Deprecated 가 붙은 대상에 발생하는 경고 |
unchecked | 제네릭 타입을 지정하지 않을 때 발생하는 경고 |
rawtypes | 제네릭을 사용하지 않아서 발생하는 경고 |
varargs | 가변인자의 타입이 제네릭 타입일 때 발생하는 경고 |
어노테이션 정보는 다음과 같다.
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
String[] value();
}
이 밖에도 다른 표준 애노테이션들이 있다.
@safeVarags
: 제네릭 타입의 가변인자에 사용됨@Native
: native 메서드에서 참조되는 상수 앞에 붙임메타 애노테이션은 애노테이션을 위한 애노테이션이다. 즉, 애노테이션을 정의할 때 애노테이션의 적용대상(target)이나 유지기간(retention)등을 지정하는데 사용된다.
@Target
애노테이션이 적용가능한 대상을 지정하는데 사용된다. 여러 개의 값을 지정할 때는 배열처럼 괄호{}를 사용해야 한다.
적용 대상은 다음과 같다.
대상 타입 | 의미 |
---|---|
ANNOTATION_TYPE | 애노테이션 |
CONSTRUCTOR | 생성자 |
FIELD | 필드(멤버변수, enum상수) |
LOCAL_VARIABLE | 지역변수 |
METHOD | 메소드 |
PACKAGE | 패키지 |
PARAMETER | 매개변수 |
TYPE | 타입(클래스, 인터페이스, enum) |
TYPE_PARAMETER | 타입 매개변수(JDK1.8) |
TYPE_USE | 타입이 사용되는 모든 곳(JDK1.8) |
예시)
@Target(ElementType.METHOD) // 이 애노테이션은 메소드에서 사용되는 것임을 명시함.
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
@Documented
애노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다. Java에서 제공하는 기본 애노테이션 중에 @Override
와 @SuppressWarnings
를 제외하고는 모두 이 메타 애노테이션이 붙어 있다.
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
주의 해야 할 점은 내 코드가 javadoc에 올라간다는 말이 아니라, 내가 직접 javadoc을 만들 수 있다는 것을 생각하자.
@Retention
애노테이션이 유지되는 범위(기간)를 지정하는데 사용된다. 애노테이션의 유지 정책의 종류는 다음과 같다.
유지 정책 | 의미 |
---|---|
SOURCE | 소스 파일에만 존재. 클래스파일에는 존재하지 않음. |
CLASS | 클래스 파일에 존재. 실행시에 사용불가. 기본값 |
RUNTIME | 클래스 파일에 존재. 실행시에 사용가능. |
@Documented
@Retention(RetentionPolicy.RUNTIME) // RUNTIME 까지 유지됨.
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}
@FunctionalInterface
를 예시로 보자면, 유지 정책이 RUNTIME
이기 때문에 실행시에 사용가능 하다는 점을 확인할 수 있다.
한편, SOURCE
로 사용되면 일반 주석처럼 사용된다는 뜻이다. 소스단에서만 사용하고 컴파일 될 때는 이 애노테이션이 필요없다는 뜻이 된다. 이는 @Override
가 대표적이다.
@Target(ElementType.METHOD)
@Retention(RetetionPolicy.SOURCE) // SOURCE: 소스 파일에만 존재
public @interface Override {}
애노테이션을 이용해서 프로세스를 처리하는 것을 의미한다. 애노테이션을 사용하기 위해서는 애노테이션 프로세서가 필요하다. 특히 애노테이션 프로세서의 특징은 컴파일 단계에서 애노테이션에 정의된 액션을 처리하는 데, 이는 우리의 애플리케이션이 실행되기 전 체킹을 해주기 때문에 애노테이션으로 의도한 대로 이루어지지 않을 경우 에러나 경고를 보여주기도 하며, 소스코드(.java)와 바이트코드(.class)를 생성해주기도 한다.
애노테이션 프로세서의 대표적인 기술로는 Lombok이 있다.
롬복은 다음과 같이 동작한다.
@Data
public class Test {
private int num;
}
이것을 돌려보면 분명히 setter, getter를 추가한 적이 없지만, 신기하게도 클래스 파일에 다음과 같이 추가되어 있음을 확인할 수 있다.
public class Test {
private int num;
public Test() {
}
public int getNum() {
return this.num;
}
public void setNum(final int num) {
this.num = num;
}
public boolean equals(final Object o) {
if (o == this) {
return true;
} else if (!(o instanceof Test)) {
return false;
} else {
Test other = (Test)o;
if (!other.canEqual(this)) {
return false;
} else {
return this.getNum() == other.getNum();
}
}
}
protected boolean canEqual(final Object other) {
return other instanceof Test;
}
public int hashCode() {
int PRIME = true;
int result = 1;
int result = result * 59 + this.getNum();
return result;
}
public String toString() {
return "Test(num=" + this.getNum() + ")";
}
}
이 것은 Runtime시 생성된 것이 아닌 컴파일 단계에서 생성된 것이다.
@Data
라는 애노테이션이 붙으면 Java는 이 애노테이션이 붙은 곳을 찾아 마치 모자에서 토끼를 꺼내듯 위와 같은 코드를 만들어 준다.
비슷한 예시로 Spring의 IoC도 마찬가지다.
@Component
라는 애노테이션이 붙으면 Spring은 이 애노테이션이 붙은 곳을 Bean으로 만들어 준다.
(쉽게 말해 자바 객체를 만들어 준다고 생각하면 이해하기 쉬움)
애노테이션은 단독으로 사용되면 단순한 주석에 불과하지만, 애노테이션 프로세서라는 기술 덕분에 위와 같이 다양하게 사용할 수 있다.
모든 클래스 파일은 클래스로더(Classloader)에 의해 메모리에 올라갈 때, 클래스에 대한 정보가 담긴 객체를 생성하는데 이 객체를 클래스 객체라고 한다. 이 객체를 참조할 때는 '클래스이름.class'의 형식을 사용한다.
'클래스이름.class'를 사용해서 클래스의 필드, 생성자, 메소드에 대한 정보를 얻을 수 있다.
메서드명 | 리턴타입 | 설명 |
---|---|---|
getFields() | Field[] | 접근 제어자가 public인 필드들을 Field 배열로 반환. 부모 클래스의 필드들도 함께 반환한다. |
getConstructors() | Constructor[] | 접근 제어자가 public인 생성자들을 Constructor 배열로 반환. 부모 클래스의 생성자들도 함께 반환한다. |
getMethods() | Method[] | 접근 제어자가 public인 메서드들을 Method 배열로 반환. 부모 클래스의 메서드들도 함께 반환한다. |
getDeclaredFields() | Field[] | 접근 제어자에 상관없이 모든 필드들을 Field 배열로 반환. 부모 클래스의 필드들은 반환하지 않는다. |
getDeclaredConstructors() | Constructor[] | 접근 제어자에 상관없이 모든 생성자들을 Constructor 배열로 반환. 부모 클래스의 생성자들은 반환하지 않는다. |
getDeclaredMethod() | Method[] | 접근 제어자에 상관없이 모든 메서드들을 Method배열로 반환. 부모 클래스의 메서드들은 반환하지 않는다. |
리턴타입 | 메소드명/설명 |
---|---|
boolean | isAnnotationPresent(Class<? Extends Annotation> annotationClass) / 지정한 애노테이션이 적용되었는지 여부를 확인. Class에서 호출했을 경우 상위 클래스에 적용된 경우에도 true를 리턴. |
Annotation | getAnnotation(Class<T> annotationClass) / 지정한 애노테이션이 적용되어 있으면 애노테이션을 반환하고 그렇지 않은 경우 null을 반환. Class에서 호출한 경우 상위 클래스에 적용된 애노테이션도 반환한다. |
Annotation[] | getAnnotations() / 적용된 모든 애노테이션을 반환한다. Class에 사용됐을 경우 상위 클래스에 적용된 애노테이션까지 전부 포함해서 반환. 애노테이션이 없을 경우 길이가 0인 배열을 반환한다. |
Annotation[] | getDeclaredAnnotation() / 직접 적용된 모든 애노테이션을 리턴한다. Class에서 호출했을 경우 상위 클래스에 적용된 애노테이션은 포함되지 않는다.(상위 클래스의 @Inherited 가 붙은 애노테이션을 무시한다.) |