학습사항
- 어노테이션을 정의하는 방법
- @retention
- @target
- @documented
- 어노테이션 프로세서
Annotation 을 번역하면 '주석' 이다.
자바에서 주석(comment)을 작성하는 방법은 한 줄 주석에 사용하는 '//' 을 이용하거나
복수 줄 주석에 사용하는 ' /* ~~~ */ ' 이 있다.
public static void main(String[] args) {
System.out.println("주석문은 출력이 될까?");
// 주석으로 작성한 부분은 바이트코드가 메모리에 로드될 때 제외된다.
System.out.println("주석문은 무시된다.");
/*
여러 줄에 걸쳐서
주석문을 작성하는 것도
가능하다.
*/
}
---> console
주석문은 출력이 될까?
주석문은 무시된다.
하지만 이번에 학습할 Annotation 은 위와같은 '주석' 을 작성하는 방법이 아니라 java.lang.annotation 패키지의 Annotation 인터페이스 이다.
자바 1.5 버전부터 추가된 이 인터페이스는 자바컴파일러와 JVM 을 위한 메타데이터 정도로 생각하면 될 것 같다.
가장 쉽게 볼 수 있는 빌트인 Annotation 중 하나인 @Override 를 통해 이해를 돕자면
public interface A {
void someMethod();
}
public class AImpl implements A{
@Override
public void someMethod() {
System.out.println("Something");
}
}
public class AImpl2 implements A{
public void someMethod() {
System.out.println("Something");
}
}
A 인터페이스를 구현한 AImpl 이나 AImpl2는 동일하게 동작할 것이다. 하지만 AImple 에는 @Override 어노테이션을 통해 해당 메소드가 A인터페이스의 abstract 메소드를 구현한 메소드라는 정보를 컴파일러에게 전달해 주고 있다. 이를 통해서 얻을 수 있는 효용은 만약, 메소드를 오버라이딩 하는 과정에서 오타를 냈다거나 메소드 시그니처를 실수로 다르게 사용했다면 컴파일 단계에서 해당 메소드가 오버라이딩 메소드임에도 불구하고 상위타입에 해당 메소드가 존재하지 않음으로 해당 위치에서 컴파일 에러를 발생시켜 준다.
물론, 똑똑한 IDE 를 사용하면 이와같은 일이 크게 효용이 있을까 라는 생각이 들 수도 있지만 분명 해당 메소드가 오버라이딩 메소드임을 컴파일러가 인지할 수 있느냐 없느냐에는 큰 차이가 있다.
커스텀한 어노테이션을 만들어 사용하고 싶다면 다음과 같이 간단하게 Annotation 을 정의해 사용할 수 있다.
public @interface Custom {
}
@Custom
public class Test {
}
또한 Annotation 들은 enum 과 String, primitive type 에 대한 값을 가질 수 있고 default 값을 선언할 수 도 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface Custom {
String name() default "hello";
int age();
}
@Custom(age = 10)
public class Test {
public static void main(String[] args) {
Arrays.stream(Test.class.getAnnotations()).forEach( a ->{
Custom custom = (Custom) a;
System.out.println(custom.name());
System.out.println(custom.age());
});
}
}
-----> console
hello
10
참고로 만약 값을 하나만 사용한다면 value() 를 사용하면 사용하는 곳에서 값을 그냥 주어 사용할 수 있다.
@Retention(RetentionPolicy.RUNTIME)
public @interface Name {
String value();
}
@Name("This is name")
public class Tester {
public static void main(String[] args) {
Name annotation = Tester.class.getAnnotation(Name.class);
System.out.println(annotation.value());
}
}
---> console
This is name
어노테이션이 언제까지 영향을 미칠 수 있는지에 대해서 정의하기 위해 사용되는 메타어노테이션이다. 디폴트는 RetentionPolicy.CLASS 로 일반적인 comments 와 동일하게 .class 까지는 정보가 남지만 메모리에 로드 될 때는 제외된다.
다음 코드에서는 @Retention 을 통해 런타임까지 어노테이션의 정보가 남도록 해보겠다.
@Retention(RetentionPolicy.RUNTIME)
public @interface Custom {
}
@Custom
public class Test {
public static void main(String[] args) {
Arrays.stream(Test.class.getAnnotations()).forEach(System.out::println);
}
}
--> console
@weeks12.Custom()
콘솔에 어노테이션 정보가 찍히는 것을 보아, 런타임에도 어노테이션 정보가 살아있음을 알 수 있다.
어노테이션을 사용할 수 있는 위치에 대한 정보를 가지는 어노테이션이다.
default 로는 아무 곳에나 사용될 수 있지만 @Target 어노테이션을 활용해 사용을 제한할 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) // 메소드 선언에만 해당 어노테이션을 사용할 수 있다.
public @interface Custom {
}
@Custom // -> 사용 불가능
public class Test {
@Custom // -> 사용 가능
public static void main(String[] args) {
Arrays.stream(Test.class.getAnnotations()).forEach(System.out::println);
}
}
javadoc 으로 api 문서를 만들 때 해당 어노테이션에 대한 설명도 포함하도록 설정하는 어노테이션이다.
상속이 되는 어노테이션임을 설정하는 어노테이션이다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Custom {
}
@Custom
public class Test {
}
public class SubTest extends Test {
public static void main(String[] args) {
Arrays.stream(SubTest.class.getAnnotations()).forEach(System.out::println);
}
}
--->console
==================
콘솔에는 아무것도 찍히지 않는다.
이 때 커스텀하게 만든 어노테이션에 @Inherited 를 사용해보도록 하자
==================
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Inherited
public @interface Custom {
}
public class SubTest extends Test {
public static void main(String[] args) {
Arrays.stream(SubTest.class.getAnnotations()).forEach(System.out::println);
}
}
--->console
@weeks12.Custom()
AnnotationProcessor 는 컴파일타임에 특정한 어노테이션이 붙어있는 소스코드를 참조할 수 있는 기능을 제공한다.
이 기능을 활용하고 있는 대표적인 라이브러리 중 하나는 상당히 많은 개발자들이 사용하고 있는 lombok 이 있다.
간단하게 롬복이 어떻게 어노테이션 프로세서를 활용하는지를 살펴보자
import lombok.*;
@Getter @Setter @NoArgsConstructor @ToString
public class Member {
private String name;
private int age;
}
내가 작성한 Member 의 코드에는 getter 나 setter, toString 이 존재하지 않아 자바빈 이라고 할 수 없다.
public class MemberTester {
public static void main(String[] args) {
Member member = new Member();
member.setName("jaden");
member.setAge(999);
System.out.println(member.getName());
System.out.println(member.getAge());
System.out.println(member.toString());
}
}
---->console
jaden
999
Member(name=jaden, age=999)
그런데 작성하지도 않은 setter 와 getter, toString 을 사용할 수 있는데, 실제로 target 폴더에 생성된 소스코드를 보면
public class Member {
private String name;
private int age;
public String getName() {
return this.name;
}
public int getAge() {
return this.age;
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
public Member() {
}
public String toString() {
String var10000 = this.getName();
return "Member(name=" + var10000 + ", age=" + this.getAge() + ")";
}
}
다음과 같이 내가 작성하지 않은 코드가 생성되어 자바빈이 되어있는 모습을 확인할 수 있다.
사실, AnnotationProcessor 가 제공하는 API 는 어노테이션이 붙어 있는 클래스의 정보를 참조하는 기능까지만을 제공한다. 즉, 소스코드의 수정은 API 에서 제공하는 기능은 아니지만 롬복은 컴파일러 내부 클래스를 사용하여 코드를 수정하고 있다. AnnotationProcessor 개발한 개발자의 의도와 다르게 코드를 사용하는 일종의 해킹 이라고도 볼 수 있지만, 실제로 많은 편의를 제공해주는 라이브러리이기 때문에 많은 개발자들이 사용하고 있다.
AnnotationProcessor 를 이야기하면서 특정 라이브러리 이야기를 너무 많이 한 것 같은데, 결론을 말하자면 AnnotationProcessor 는 컴파일타임에 특정한 어노테이션이 붙어있는 소스코드를 참조할 수 있는 기능을 제공해주고 이를 활용할 수 있는 방안은 아주 많을 것이다.
스터디 깃헙주소 : https://github.com/whiteship/live-study/issues/12
예제코드 깃헙레포 : https://github.com/JadenKim940105/whiteship-study/tree/master/src/main/java/weeks12