애너테이션을 사용해보자.
참고 도서 : 카이호스트만의 코어 자바 8
핵심 내용 정리
- 애너테이션은 선언부에 public이나 static 같은 제어자를 사용하는 것처럼 붙일 수 있다.
- 애너테이션은 선언부, 타입 변환 instanceof 검사, 메서드 참조에 나타나는 타입에도 붙일 수 있다.
- 애너테이션은 @ 기호로 시작하며, 요소라고 부르는 키/값 쌍을 둘 수 있다.
- 애너테이션 값은 반드시 컴파일 시간 상수(기본 타입, enum 상수, Class 리터럴, 다른 애너테이션이나 배열 상수)여야 한다.
- 애너테이션 아이템은 반복 애너테이션이나 타입이 다른 애너테이션을 둘 수 있다.
- 애너테이션을 정의하려면 애너테이션 인터페이스를 명시해야 하는데, 애너테이션 인터페이스에 애너테이션 요소에 대응하는 메서드가 있어야 한다.
- 자바 라이브러리는 십여 개가 넘는 애너테이션을 정의하며, 자바 엔터프라이즈 에디션에서 광범위하게 사용한다.
- 실행 중인 자바 프로그램에서 애너테이션을 처리하려면 리플렉션을 사용한 후 리플렉션으로 나온 아이템에 애너테이션이 있는지 조사해야 한다.
- 애너테이션 핸들러는 컴파일 과정 중에 자바 언어 모델 API로 애너테이션이 붙은 아이템을 찾으며 소스 파일을 처리한다.
1.1 애너테이션 요소
애너테이션에는 키/값 쌍 요소를 둘 수 있다.
@Test(timeout=10000)
- 애너테이션의 요소
- 기본 타입 값
- String
- Class 객체
- enum 인스턴스
- 애너테이션
- 위의 항목의 배열(배열의 배열을 안 됨)
// - 애너테이션의 요소는 기본값을 가질 수 있다. Junit @Test 애노테이션의 timeout요소는 기본값이 0L이다.
@Test @Test(timeout=0L)
// - 요소 이름이 value이면 value= 생략할 수 있다.
@SuppressWarnings @SuppressWarnings(value="unchecked")
- 요소의 값이 배열이면 해당 배열의 구성 요소를 중괄호로 감싼다.
@BugReport(reportedBy={"Herray", "Kim"})
- 배열에 구성 요소가 하나만 있으면 중괄호를 생략할 수 있다.
@BugReport(reportedBy="Herry") @BugReport(reportedBy={"Herry"})
- 애너테이션 요소로 다른 애너테이션 요소를 사용할 수 있다.
@BugReport(ref=@Reference(id=12312412), ...)
1.2 다중 애너테이션과 반복 애너테이션
- 아이템 하나에 여러 애너테이션을 둘 수 있다.
- 애너테이션 작성자가 애너테이션을 반복 가능으로 선언했다면 같은 애너테이션을 여러 번 반복할 수 있다.
@Test
@BugReport(showStopper=true, reportedBy="Joe")
public void checkRandomInsertions()
@BugReport(showStopper=true, reportedBy="Joe")
@BugReport(reportedBy={"Harry", "Carl"})
public void checkRandomInsertions()
1.3 선언부에 애너테이션 붙이기
지금 까지 선언부에 적용하는 애너테이션을 다뤘고, 메서드 외에도 애너테이션을 사용할 수 있는 곳은 다양하다. 크게 선언과 타입 사용 카테고리로 나뉜다. 일단 선언부 부터 다룬다.
선언부를 사용할 수 있는 아이템들
- 클래스(enum포함)와 인터페이스(애너테이션 인터페이스 포함)
- 메서드
- 생성자
- 인스턴스 변수(enum 상수 포함)
- 지역변수
- 파라미터 변수와 catch절의 파라미터
- 타입 파라미터
- 패키지
제네릭 클래스나 메서드의 타입 파라미터에는 다음과 같이 붙인다.
public class Cache<@Imutable V> {...}
패키지에는 다음과 같이 붙인다.
@GPL(version="3")
package com.my.proj.hello;
1.4 타입 사용에 애너테이션 붙이기
// 파라미터
public User getUser(@NonNull String userId)
// 제네릭 타입 인자
List<@NonNull String>
// 배열이 모든 위치
@NonNull String[][]
// 슈퍼클래스와 구현 대상 인터페이스
class Warning extends @Localized Message
// 생성자 호출
new @Localized String{...}
// 중첩 타입
Map.@Localized Entry.
// 타입 변환과 instanceof 검사
(@Localized String) text, if (text instanceof @Localized String)
// 예외 명세
public String read() throws @Localized IOException
// 와일드카드와 타입 경계
List<@Localized ? extends Message>, List<? extends @Localized Message>
// 메서드 참조와 생성자 참조
@Localized Message::getText
클래스 리터럴과 import에는 애너테이션을 붙일 수 없다.
@NonNull String.class // (x)
import java.lang.@NonNull String; // (x)
1.5 수신자를 명확하게 지정하기
@Target({ElementType.TYPE_USE, ElementType.PARAMETER})
public @interface ReadOnly {
}
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public String toString(@ReadOnly Point this) { return "(" + x + "," + y + ")"; }
@Override
public boolean equals(@ReadOnly Point this, @ReadOnly Object obj) {
Point other = (Point) obj;
return x == other.x && y == other.y;
}
public static void main(String[] args) {
public static void main(String[] args) {
Point p = new Point(1, 2);
Point q = new Point(1, 2);
System.out.println(p.equals(q)); // true
}
}
메서드에서 변경되지 않는 파라미터에 애너테이션을 붙인다. 애너테이션을 처리하는 도구에서 q가 변경되지 않았다고 할 수 있을까?
equals 메서드를 호출할 때 수신자 변수 this는 p에 연결되어 있지만, this는 선언한 적이 없으므로 애너테이션을 붙일 수 없다.
실제로는 this를 거의 사용하지 않는 문법 형태를 이용해 선언할 수 있는데, 그저 애너테이션을 붙이는 용도일 뿐이다. this를 수신자 파라미터라 한다.
수신자 파라미터는 메서드에만 제공할 수 있고 생성자에서는 사용할 수 없다.
2. 애너테이션 정의하기
@Target(ElementType.METHOD) // 애너테이션 적용 위치
@Retention(RetentionPolicy.RUNTIME) // 애너테이션 접근 위치
public @interface Test {
long timeout() default 0L;
}
@Target, @Retention은 메타애너테이션이다.
@Target 애너테이션용 요소 타입
| 요소 타입 | 애너테이션 적용 대상 |
|---|---|
| ANNOTATION_TYPE | 애너테이션 타입 선언 |
| PACKAGE | 패키지 |
| TYPE | 클래스(enum포함)와 인터페이스(애너테이션 타입 포함) |
| METHOD | 메서드 |
| CONSTRUCTOR | 생성자 |
| FIELD | 인스턴스 변수(enum 상수포함) |
| PARAMETER | 메서드나 생성자의 파라미터 |
| LOCAL_VARIABLE | 지역 변수 |
| TYPE_PARAMETER | 타입 파라미터 |
| TYPE_USE | 타입 사용 |
@Retention
- RetentionPolicy.SOURCE
- 애너테이션이 소스 핸들러에는 보이지만 클래스 파일에는 포함되지 않는다.
- RetentionPolicy.CLASS
- 애너테이션이 클래스 파일에 포함되지만 가상 머신은 해당 애너테이션을 로드하지 않는다. 기본값니다.
- RetentionPolicy.RUTIME
- 애너테이션을 실행 시간에 이용할 수 있고 리플렉션 API를 통해 접근할 수 있다.
2.1 애너테이션 정의하기 예
요소의 기본 값을 지정하기 메서드 뒤에 defalt 절 추가
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {
long timeout() default 0L;
}
기본값으로 빈 배열을 지정하고 애너테이션의 기본값을 지정하는 방법
public @interface Reference {
long id();
}
//=======================
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BugReport {
// 기본 값은 비어있는 배열이다.
String[] reprotedBy() default {};
// 애너테이션의 기본값을 지정한다.
Reference ref() default @Reference(id=0);
}
기본 값은 애너테이션과 함계 저장되지 않고 동적으로 계산된다. 기본값 변경하고 컴파일해도, 모든 애너테이션 요소가 새로운 기본값을 사용한다.
애너테이션 인터페이스는 확장할 수 없으므로 애너테이션 인터페이스를 구현하는 클래스를 만들 수 없다. 대신 소스 처리 도구와 가상 머신은 필요할 떄 프록시 클래스와 객체를 만들어 낸다.
3. 표준 애너테이션
자바 API는 java.lang.java.lang.annotation, javax.annotation 패키지에 애너테이션을 정의해 놓았다. 그 중 네개는 애너테이션 동작을 기술하는 메타애너테이션이고 나머지는 소스 코드에서 아이템을 설명하는 데 사용하는 일반 애너테이션이다.
| 요소 타입 | 애너테이션 적용 대상 | 용도 |
|---|---|---|
| Override | 메서드 | 메서드가 슈퍼클래스 메서드를 오버라이드하는지 확인 |
| Deprecated | 모든 선언 | 아이템 사용을 권장하지 않음을 표시 |
| SuppressWarnings | 패키지를 제외한 모든 선언 | 주언진 타입의 경고를 억제 |
| SafeVarargs | 메서드와 생성자 | 가변 인자 파라미터 안전하게 사용한다고 단정 |
| FunctionalInterface | 인터페이스 | 인터페이스를 함수형으로 표시 |
| PostConstruct Predestroy | 메서드 | 주입 대상 객체를 생성하는 즉시 또는 객체를 제거하기 직전에 메서드를 호출해야 함을 나타냄 |
| Resource | 클래스, 인터페이스, 메서드, 필드 | 리소스 표시, 의존성 주입 |
| Generated | 모든 선언 | 도구를 이용해 만든 소스 코드로 아이템을 표시 |
| Target | 애너테이션 | 애너테이션 적용 위치 지정 |
| Retention | 애너테이션 | 애너테이션을 사용할 수 있는 위치를 지정 |
| Documented | 애너테이션 | 애너테이션을 붙인 아이템의 문서에 이 애너테이션을 포함해야 함을 나타냄 |
| Inherited | 애너테이션 | 서브클래스가 애너테이션을 상속받음을 나타냄 |
| Repeatable | 애너테이션 | 같은 아이템에 애너테이션을 여러 번 적용할 수 있음을 나타냄 |
3.1 컴파일용 애너테이션
- @deprecated
- @Override
- @SafeVarargs
- @Generated
- @FunctionalInterface
3.2 리소스 관리용 애너테이션
- @PostConstruct
- @PreDestroy
- @Resource
3.3 메타 애너테이션
- @Target
- @Retention
- @Documented
- @Inherited
- @Repeatable
4. 런타임에 애너테이션 처리하기
리플렉션 API로 실행 시간에 애너테이션을 처리하는 간단한 예를 설명한다.
toString 메서드를 구현하는 지루함을 줄이고 싶다고 하자.
@ToString(includeName = false)
public class Point {
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@ToString(includeName = false)
private int x;
@ToString(includeName = false)
private int y;
}
@ToString(includeName = false)
public class Rectangle {
public Rectangle(Point topLeft, int width, int height) {
this.topLeft = topLeft;
this.width = width;
this.height = height;
}
@ToString(includeName = false)
private Point topLeft;
@ToString(includeName = false)
private int width;
@ToString(includeName = false)
private int height;
}
public class ToStrings {
public static String toString(Object obj) {
if (obj == null) return "null";
Class<?> cl = obj.getClass();
ToString ts = cl.getAnnotation(ToString.class);
if (ts == null) return obj.toString();
StringBuilder result = new StringBuilder();
if (ts.includeName()) result.append(cl.getName());
result.append("[");
boolean first = true;
for (Field f : cl.getDeclaredFields()) {
ts = f.getAnnotation(ToString.class);
if (ts != null) {
if (first) first = false; else result.append(",");
f.setAccessible(true);
if (ts.includeName()) {
result.append(f.getName());
result.append("=");
}
try {
result.append(ToStrings.toString(f.get(obj)));
} catch (ReflectiveOperationException ex) {
ex.printStackTrace();
}
}
}
result.append("]");
return result.toString();
}
}
public class Main {
public static void main(String[] args) {
Rectangle rect = new Rectangle(new Point(10, 10), 20, 30);
System.out.println(rect.toString());
System.out.println(ToStrings.toString(rect));
}
}
리플렉션을 사용해서 클래스와 필드 등을 찾고, 잠재적으로 애너테이션이 붙을 만한 요소에 getAnnotaion이나 getAnnotationByType을 호출해서 애너테이션을 추출한다. 그런 다음 해당 애너테이션 인터페이스의 메서드를 호출해서 요소의 값을 얻는다.
5. 소스 수준 애너테이션 처리
toString 메서드의 예를 다시 반복하여 소스 수준에서 처리한다. 자바 소스로 생성한다. 생성된 메서드가 프로그램의 나머지 부분과 함께 컴파일되어 리플렉션을 사용하지 않고도 온전한 속도로 실행된다.
5.1 애너테이션 핸들러
애너테이션 처리는 자바 컴파일러에 통합되어 있다. 다음 명령을 실행하면 컴파일 도중에 애너테이션 핸들러를 호출할 수 있다.
jaavc -processor 핸들러클래스이름1, 핸들러클래스이름2, ... 소스파일
그러면 컴파일러가 소스 파일에서 애너테이션을 찾고, 각 애너테이션 프로세서가 차례로 호출되며 각각 관심을 표한 애너테이션을 받는다. 애너테이션 핸들러가 새로운 소스 파일을 생성하면 처리를 반복한다. 처리 라운드에서 더 이상 소스 파일을 생성하지 않을 때 모든 소스 파일을 컴파일 한다.
애너테이션 핸들러는 새로운 소스 파일을 생성할 수 있다. 기존 소스파일을 수정할 수 없다.
애너테이션 핸들러는 보통 AbstractProcessor 클래스를 확장하는 방법으로 Processor 인터페이스를 구현한다. 그리고 만드려는 애너테이션 핸들러에서 지원하는 애너테이션을 명시해야 한다.
@SupportedAnnotationTypes("me.seungui.annotaions.toString")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
class ToStringAnnotationProcessor extends AbstractProcessor {
@Override
public boolean process(Set<? extends TypeElement> annotations,
RoundEnvironment currentRound) {
...
}
5.2 언어 모델 API
소스 수준 애너테이션을 분석하는 데는 언어 모텔 API를 사용한다. ???
5.3 애너테이션으로 소스 코드 생성하기
애너테이션 핸들러는 기존 클래스를 수정할 수 없으므로 모든 메서드를 유틸리티 클래스에 추가한다.
public class ToStrings {
public static String toString(me.seungui.ch11.sec05.Point obj) {
// 생성된 코드
}
public static String toString(me.seungui.ch11.sec05.Rectangle obj) {
// 생성된 코드
}
public static String toString(Object obj) {
return java.util.Objects.toString(obj);
}
}
이 파트는 사실 스킵하는 것이 좋겠다. ㅇㅇ