본 포스팅은 백기선님이 진행하시는 자바 스터디 를 진행하며 혼자 공부하고 이해한 내용을 바탕으로 정리한 글입니다. 오류나 지적 사항이 있다면 댓글로 알려주시면 감사하겠습니다.
애노테이션(Annotation)이란?
애노테이션은 소스코드에 메타데이터(혹은 설정정보)를 추가하는 매커니즘이다.
예전에는 소스코드와 그에 대한 문서를 따로 작성하였다. 이런 경우 소스코드를 변경하고나서 문서를 이에 맞춰 변경하지 않으면, 소스코드와 문서가 일치하지 않는 불상사가 생긴다. 그래서 자바 개발자들은 소스코드와 문서를 하나의 파일로 저장하는 방안을 생각해냈다. 그렇게 소스코드의 주석으로부터 HTML 문서를 생성해내는 javadoc.exe가 탄생했다.
문서 뿐만 아니라 설정 파일도 소스코드와 분리해서 관리했었다. 예전에는 설정 파일을 XML 형태로 따로 관리했지만 지금은 문서와 마찬가지로 소스코드와 함께 관리한다. 이때 사용하는 것이 애노테이션이다. 애노테이션을 사용해서 프로그램이 특정 기능을 수행하도록 메타데이터를 제공할 수 있다.
자바에서 애노테이션을 통해 다음과 같은 일을 할 수 있다.
- 컴파일러에게 문법 에러를 체크하도록 정보 제공
- 컴파일 타임에 자동으로 코드를 생산할 수 있도록 정보 제공 (애노테이션 프로세서 활용)
- 런타임에 특정 기능을 수행할 수 있도록 정보 제공 (리플렉션 활용)
정의하는 방법
애노테이션 타입은 조금 특별한 타입의 인터페이스로 몇 가지 예외를 제외하면 인터페이스를 정의하는 것과 거의 비슷하다.
우선 애노테이션은 @interface 키워드로 선언한다.
public @interface 애노테이션 이름 {
타입 요소이름();
}
애노테이션내에 정의된 메서드를 요소(element)라고 하며, 요소를 선언할 때는 몇 가지 제약 사항이 있다.
- 요소의 타입은 primitive, String, enum, annotation, Class, 그리고 이들의 배열만 허용된다.
- 매개변수를 가질 수 없다.
- 예외를 선언할 수 없다.
- 요소를 타입 매개변수로 정의할 수 없다.
따라서 아래와 같은 요소들은 정의될 수 없다.
public @interface MyAnnotation {
String key(int i); // 매개변수 사용 불가
int[] value() throws Exception; // 예외를 던질 수 없음
Object foo(); // 가능한 타입 이외의 타입
}
정의된 요소는 해당 애노테이션을 사용할 때 지정한다. default로 기본값을 지정할 수도 있다. 만약 default를 지정하지 않았다면 해당 애너테이션을 사용할 때 반드시 값을 지정해야한다.
필요하다면 메타 애노테이션을 붙여서 추가적인 정보를 제공할 수 있다. 메타 애노테이션은 애노테이션을 위한 애노테이션으로 @Retention, @Target 등이 있다. 자세한 내용은 뒷부분에서 다루겠다.
만약 정의된 요소가 하나라면 요소의 이름을 생략할 수 있다. 정의된 요소가 두 개 이상이라면 value = "값" 형태로 지정하며 콤마로 구분한다.
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
@interface Author {
String name();
String created();
int revision() default 1;
String[] reviewers() default {};
}
@Author(name = "awesomeo184", created = "2021/01/31")
class Book {}
표준 애노테이션
자바에서 기본적으로 제공하는 애노테이션을 표준 애노테이션이라고 하며, 'java.lang.annotation' 패키지에 포함되어 있다. 자주 사용되는 표준 애노테이션을 간단히 정리해봤다.
@Override
해당 메서드가 오버라이딩 된 메서드라는 것을 컴파일러에게 알려준다. 만약 이 애노테이션을 달고 제대로 오버라이드를 하지 않으면 컴파일 에러가 발생한다.
public class Parent {
void method1() { }
}
class Child extends Parent {
@Override
void methodd1() { } // 메서드 이름에 오타를 냈다
}
Output
java: method does not override or implement a method from a supertype
@FunctionalInterface
함수형 인터페이스를 정의할 때 사용한다. 함수형 인터페이스는 단일 메서드를 가져야하는 규약이 있으므로, 이 애노테이션을 붙이고 메서드를 두개 정의할 경우 컴파일 에러가 발생한다.
@Deprecated
더 이상 사용되지 않는 필드나 메서드에 @Deprecated를 달면, 이 메서드를 사용할 때, 경고 메시지가 뜬다. 컴파일 오류는 발생하지 않는다.
class NewClass {
int newField;
void newMethod() { }
@Deprecated
int oldField;
@Deprecated
void oldMethod() { }
}
public class AnnotationDeprecated {
public static void main(String[] args) {
NewClass newClass = new NewClass();
newClass.oldMethod(); //Deprecated된 멤버를 사용
}
}
이때 "-Xlint:deprecation" 옵션을 붙여서 컴파일하면 자세한 내용을 확인할 수 있다.
@SuppressWarnigs
컴파일러 경고메시지를 무시한다. 때에 따라 경고 메시지가 날 것을 알면서도 묵인해야 할 때가 있는데, 이걸 그대로 두면 다른 경고 메시지가 생겼을 때 알아차리지 못할 수도 있다. 그럴 때 이 애노테이션을 사용한다.
class NewClass {
int newField;
void newMethod() { }
@Deprecated
int oldField;
@Deprecated
void oldMethod() { }
}
public class AnnotationDeprecated {
@SuppressWarnings("deprecation")
public static void main(String[] args) {
NewClass newClass = new NewClass();
newClass.oldMethod();
}
}
경고 메시지 타입을 명시해주면 해당 타입의 경고 메시지를 무시한다. 타입에는 "deprecation", "unchecked", "rawtypes", "varargs" 등이 있다. 여러 타입의 경고를 무시하고 싶다면 콤마(,)로 구분해서 넘겨주면 된다.
메타 애노테이션
메타 애노테이션은 애노테이션을 위한 애노테이션으로, 애노테이션을 정의할 때 사용하는 애노테이션이다. 애노테이션의 적용대상(Target), 유지기간(Retention)등을 지정하는데 사용된다. 위에서 살펴본 @SuppressWarnings의 소스코드를 보면 다음과 같이 메타 애노테이션이 붙어있는 것을 확인할 수 있다.
@Target
해당 애노테이션이 어디에 적용될 수 있는지를 지정한다. @Target으로 지정할 수 있는 애노테이션 적용대상의 종류는 다음과 같다.
대상 타입 | 의미 |
ANNOTATION_TYPE | 애노테이션 |
CONSTRUCTOR | 생성자 |
FILED | 필드 (멤버 변수, enum 상수) |
LOCAL_VARIABLE | 지역변수 |
METHOD | 메서드 |
PACKAGE | 패키지 |
PARAMETER | 매개변수 |
TYPE | 타입 (클래스, 인터페이스, enum) |
TYPE_PARAMETER | 타입 매개변수 (JDK 1.8) |
TYPE_USE | 타입이 사용되는 모든 곳 (JDK 1.8) |
여러 개의 값을 지정할 때는 중괄호( {} ) 안에 콤마로 구분해서 지정한다.
TYPE은 타입을 선언할 때 해당 애노테이션을 붙일 수 있다는 뜻이고, TYPE_USE는 해당 타입의 변수를 선언할 때 붙일 수 있다는 뜻이다.
import static java.lang.annotation.ElementType.*;
import java.lang.annotation.Target;
@Target({TYPE, FIELD, TYPE_USE})
public @interface MyAnnotation {}
@MyAnnotation // 적용 대상이 TYPE인 경우
class MyClass {
@MyAnnotation // 적용 대상이 FIELD인 경우
int i;
@MyAnnotation // 적용 대상이 TYPE_USE인 경우
MyClass myClass;
}
@Retention
@Retention은 해당 애노테이션의 유효기간을 설정한다. 유효기간을 설정한다는 것은 어느 시점까지 애노테이션 정보를 메모리에 저장할 것인가를 설정한다는 것이다. enum type인 RetentionPolicy를 값으로 넘겨줌으로써 설정할 수 있는데, RetentionPolicy에는 세 가지가 정의되어 있으며 default는 CLASS이다.
타입 | 의미 |
SOURCE | 컴파일 전까지. 컴파일할 때 해당 애노테이션이 제거된다. |
CLASS | 바이너리 파일에 해당 애노테이션의 정보가 저장되지만 Runtime에서 사라진다. |
RUNTIME | 런타임에서 JVM이 해당 애노테이션에 대한 정보를 사용할 수 있다. |
위와 같이 RetentionPolicy를 SOURCE로 작성한 뒤 컴파일해보면
MyClass의 바이트코드 파일(MyClass.class)에는 애노테이션이 사라져있다.
아래처럼 MyAnnotation의 RetentionPolicy를 RUNTIME으로 바꿔준 후 다시 컴파일을 해보면 바이트코드 파일에 애노테이션 정보가 남아있다. CLASS로 해도 마찬가지이다.
그럼 유지정책이 CLASS인 것과 RUNTIME인 것은 무슨 차이일까?
위 사진에서 왼쪽은 유지정책이 RUNTIME인 애노테이션이 붙은 클래스의 바이트코드 파일이고 오른쪽은 유지정책이 CLASS인 애노테이션이 붙은 클래스의 바이트코드 파일이다. CLASS 애노테이션이 붙은 클래스에 invisible 이라는 주석이 달려있는 것 말고는 차이점이 없다. 위에서 설명한대로 런타임에 사용되느냐 아니냐 말고는 차이점이 없는 것으로 보인다.
@Documented
@Documented를 붙여주면 javadoc.exe가 해당 코드를 자바 문서 형태의 HTML로 만들어준다. 직접 라이브러리나 프레임워크를 만드는 게 아니라면 거의 사용할 일이 없을 것 같다.
애노테이션 프로세서(Annotation Processor)
애노테이션 프로세서는 자바 컴파일러의 컴파일 단계에서 애노테이션 정보를 스캔하고 처리하는 Hook이다. 쉽게 이야기하면, 컴파일 도중에 애노테이션을 만나면 특정한 동작을 하도록 만들어진 코드를 말한다. 애노테이션 프로세서는 소스코드 파일(.java)이나 클래스 파일(.class)을 입력으로 받아서 새로운 소스코드 파일 혹은 클래스 파일을 생산해낸다.
Lombok을 예시로 들어보자. 롬복은 애너테이션을 이용해서 보일러 플레이트 코드를 생산해주는 라이브러리이다. 예시를 통해 살펴보자.
아래와 같이 정의된 클래스를
public class Book {
private String title;
private String author;
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
}
롬복을 이용하면 다음과 같이 코드를 줄일 수 있다.
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
public class Book {
private String title;
private String author;
}
이는 해당 클래스의 클래스파일을 보면 더 명확히 알 수 있는데, 롬복을 사용한 위의 코드가 컴파일 된 모습은 다음과 같다.
단지 애노테이션만 달았을 뿐인데, 클래스 파일에 getter, setter가 추가되어 있다!
이는 롬복 라이브러리 내부에 작성된 애노테이션 프로세서가 컴파일 타임에 내 코드에 붙은 애노테이션을 읽어서 그에 맞게 코드를 재생산해냈기 때문에 일어난 일이다. 애노테이션 프로세서가 어떻게 작성되어 있는지 직접 확인해보자.
외부 라이브러리에서 lombok.launch.AnnotationProcessorHider 클래스를 찾아 들어가면 내부에 static class로AbstractProcessor를 상속받은 AnnotationProcessor 클래스가 있고 init(), process() 같은 메서드들이 작성되어 있는 것을 확인할 수 있다.
모든 Processor는 javax.annotation.processing 패키지에 포함된 AbstractProcessor를 상속받는데, 다음과 같은 함수들을 정의할 수 있다.
public class MyProcessor extends AbstractProcessor {
@Override
public synchronized void init(ProcessingEnvironment env){ }
@Override
public boolean process(Set<? extends TypeElement> annoations, RoundEnvironment env) { }
@Override
public Set<String> getSupportedAnnotationTypes() { }
@Override
public SourceVersion getSupportedSourceVersion() { }
}
여기서 process(Set<? extends TypeElement> annotations, RoundEnvironment env) 는 프로세서의 메인 메서드와 같은 것이다. 여기서 애노테이션을 스캐닝하고 처리하는 등의 작업을 작성할 수 있다.
getSupportedAnnotationTypes()는 어떤 애노테이션에 대해 동작을 수행할지를 지정할 수 있다. 즉 내가 작업하고 싶은 애노테이션을 등록하는 것이다. FQCN(Fully Qualified Class Name)으로 전달한다. Java 7부터 메서드 대신 애노테이션으로 등록할 수 있는데, 위에서 본 롬복의 AnnotationProcessor에 SupportedAnnotationTypes는 다음과 같이 등록되어 있다.
즉 lombok. 으로 시작하는 애노테이션에 대해서는 특정 동작을 수행하도록 정의되어 있는 것이다.
앞서 애노테이션 프로세싱은 컴파일 타임에서 일어난다고 했다. 컴파일 타임에 내가 작성한 코드를 낚아채서 새로운 클래스파일을 생산하는 것이다. 그럼 또 한 가지 궁금증이 생긴다. 자바 컴파일러가 어떻게 내가 작성한 프로세서를 인식하고 동작을 수행하지?
내가 작성한 프로세서 파일과 javax.annotation.processing.Processor 라는 특별한 파일을 META-INF/services에 위치시킨 뒤 .jar 파일로 패키징해서 제공하면 된다. 즉 내가 작성한 MyProcessor.jar 파일의 구조는 다음과 같다.
MyProcessor.jar
- com
- example
- MyProcessor.class
- META-INF
- services
- javax.annotation.processing.Processor
javax.annotation.processing.Processor 파일의 내용은 프로세서들의 FQCN을 개행으로 구분해서 작성한다.
롬복의 javax.annotation.processing.Processor 파일을 보면 아까 살펴봤던 lombok.launch.AnnotationProcessorHider가 등록되어 있다.
이렇게 하면 자바 컴파일러가 MyProcessor.jar 를 이용해서 자동으로 javax.annotation.processing.Processor 파일을 발견하고 읽어서 MyProcessor를 애노테이션 프로세서로 등록한다.
구체적으로 애노테이션 프로세서를 어떻게 작성하는지 예시를 보고 싶다면 아래의 글을 참조하면 좋을 것 같다.
http://hannesdorfmann.com/annotation-processing/annotationprocessing101/
'Java > Java-basic' 카테고리의 다른 글
[Java] JDBC란 무엇인가 (2) | 2021.02.20 |
---|---|
[Java Study 13주차] I/O (0) | 2021.02.18 |
[Java Study 11주차] Enum (0) | 2021.01.23 |
[Java] isBlank() vs isEmpty() 차이 (0) | 2021.01.15 |
[Java Study 10주차] 멀티쓰레드 프로그래밍 (6) | 2021.01.13 |
댓글