어노테이션

어노테이션

소스 코드에 메타데이터를 붙이는 방법을 처음부터 살펴봐요. 어노테이션은 코드에 주석처럼 정보를 더해 주는데요, 단순한 설명이 아니라 컴파일러나 도구가 실제로 활용할 수 있는 정보라는 점이 달라요. Java SE 8 이후로는 타입이 쓰이는 자리나 반복 적용까지 지원해서, 할 수 있는 일이 훨씬 넓어졌어요.

출처: dev.java

본문

어노테이션은 여러 용도로 쓰여요. 대표적으로 이런 일을 해요.

  • 컴파일러를 위한 정보 — 컴파일러가 오류를 감지하거나 경고를 억제하는 데 써요.
  • 컴파일 시점·배포 시점 처리 — 소프트웨어 도구가 어노테이션 정보를 처리해 코드나 XML 파일 등을 생성할 수 있어요.
  • 런타임 처리 — 일부 어노테이션은 실행 중에 검사할 수 있어요.

이 장에서는 어노테이션을 어디에 쓸 수 있는지, 어떻게 적용하는지, Java SE API에서 미리 정의된 어노테이션 타입은 무엇이 있는지, 타입 어노테이션을 플러그 가능한 타입 시스템과 함께 사용해 더 강한 타입 검사를 하는 방법, 그리고 반복 어노테이션을 구현하는 방법을 차례로 다뤄요.

어노테이션의 형식

가장 단순한 형태의 어노테이션은 이렇게 생겼어요.


@Entity

@ 기호는 뒤에 오는 것이 어노테이션이라는 것을 컴파일러에 알려 줘요. 다음 예시에서 어노테이션의 이름은 Override예요.


@Override
void mySuperMethod() { ... }

어노테이션은 *요소(element)*를 가질 수 있는데, 이름을 붙일 수도 있고 붙이지 않을 수도 있어요. 요소에는 값이 따라와요.


@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass { ... }

또는


@SuppressWarnings(value = "unchecked")
void myMethod() { ... }

요소가 하나뿐이고 이름이 value라면 이름은 생략할 수 있어요.


@SuppressWarnings("unchecked")
void myMethod() { ... }

어노테이션에 요소가 없다면 괄호도 생략할 수 있어요. 아까 @Override 예시가 바로 그 경우예요.

같은 선언에 어노테이션을 여러 개 쓰는 것도 가능해요.


@Author(name = "Jane Doe")
@EBook
class MyClass { ... }

만약 동일한 타입의 어노테이션을 여러 번 반복한다면, 이를 **반복 어노테이션(repeating annotation)**이라고 불러요.


@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }

반복 어노테이션은 Java SE 8부터 지원돼요. 자세한 내용은 Repeating Annotations 섹션을 참고하세요.

어노테이션 타입은 Java SE API의 java.lang이나 java.lang.annotations 패키지에 정의된 타입 중 하나일 수 있어요. 앞선 예시에서 OverrideSuppressWarnings는 미리 정의된 자바 어노테이션이고, AuthorEbook은 직접 정의한 커스텀 어노테이션 타입이에요.

어노테이션을 쓸 수 있는 위치

어노테이션은 선언(declaration)에 적용할 수 있어요. 클래스, 필드, 메서드, 그리고 다른 프로그램 요소의 선언이죠. 선언에 적용할 때는 관례상 각 어노테이션을 한 줄에 하나씩 쓰는 경우가 많아요.

Java SE 8부터는 어노테이션을 **타입의 사용(use of type)**에도 적용할 수 있어요. 몇 가지 예를 볼게요.

  • 클래스 인스턴스 생성 표현식:

new @Interned MyObject();

  • 타입 캐스트:

myString = (@NonNull String) str;

  • implements 절:

class UnmodifiableList<T> implements @Readonly List<@Readonly T> { ... }

  • 던져지는 예외 선언:

void monitorTemperature() throws @Critical TemperatureException { ... }

이런 형태의 어노테이션을 **타입 어노테이션(type annotation)**이라고 불러요.

어노테이션 타입 선언하기

많은 어노테이션은 코드 안의 주석을 대체해요.

소프트웨어 그룹이 전통적으로 모든 클래스의 본문을 중요한 정보를 담은 주석으로 시작한다고 가정해 볼게요.


public class Generation3List extends Generation2List {
    // Author: John Doe
    // Date: 3/17/2002
    // Current revision: 6
    // Last modified: 4/12/2004
    // By: Jane Doe
    // Reviewers: Alice, Bill, Cindy

    // class code goes here
}

이 메타데이터를 어노테이션으로 추가하려면 먼저 어노테이션 타입을 정의해야 해요. 문법은 이래요.


@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   String[] reviewers();
}

어노테이션 타입 정의는 인터페이스 정의와 비슷하게 생겼어요. interface 키워드 앞에 @ 기호(@ = AT, annotation type의 약자)가 붙는 점이 다르죠. 어노테이션 타입은 인터페이스의 한 형태인데, 인터페이스는 뒷부분에서 다룰 거예요. 지금은 인터페이스를 이해할 필요는 없어요.

앞선 정의의 본문에는 메서드처럼 생긴 어노테이션 타입 요소 선언이 들어 있어요. 요소는 선택적인 기본값(default value)을 정의할 수 있다는 점을 눈여겨보세요.

어노테이션 타입이 정의되고 나면, 값을 채워서 그 타입의 어노테이션을 이렇게 사용할 수 있어요.


@ClassPreamble (
   author = "John Doe",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "Jane Doe",
   reviewers = {"Alice", "Bobby", "Cindy"}
)
public class Generation3List extends Generation2List {
    // class code goes here
}

참고: @ClassPreamble의 정보가 Javadoc이 만든 문서에 나타나게 하려면 @ClassPreamble 정의에 @Documented 어노테이션을 붙여야 해요.


@Documented
@interface ClassPreamble {
   // ...
}

미리 정의된 어노테이션 타입

Java SE API에는 미리 정의된 어노테이션 타입 모음이 있어요. 어떤 타입은 자바 컴파일러가 사용하고, 어떤 타입은 다른 어노테이션에 적용돼요.

자바 언어가 사용하는 어노테이션 타입

java.lang에 정의된 미리 정의된 어노테이션 타입은 @Deprecated, @Override, @SuppressWarnings 세 가지예요.

@Deprecated

@Deprecated 어노테이션은 표시된 요소가 deprecated(더 이상 사용을 권장하지 않음) 상태이며 더 이상 사용하면 안 된다는 것을 나타내요. 사용자가 @Deprecated가 붙은 메서드, 클래스, 필드를 프로그램에서 사용하면 컴파일러는 경고를 생성해요. 요소가 deprecated라면 Javadoc의 @deprecated 태그로도 문서화해야 해요. 다음 예시를 볼게요.


/**
 * @deprecated
 * explanation of why it was deprecated
 */
@Deprecated
static void deprecatedMethod() { }

주석과 어노테이션 모두에 @ 기호를 쓰는 것은 우연이 아니에요. 개념적으로 연관이 있기 때문이죠. 여기서 Javadoc 태그는 소문자 d(@deprecated)로, 어노테이션은 대문자 D(@Deprecated)로 시작한다는 점도 기억해 두세요.

Java SE 9부터 @DeprecatedforRemoval 속성이 추가됐어요. 이 속성은 어노테이션된 요소가 향후 버전에서 제거될 대상인지 여부를 나타내요. 기본값은 false예요.

@Override

@Override 어노테이션은 해당 요소가 슈퍼클래스에 선언된 요소를 오버라이드하기 위한 것임을 컴파일러에 알려 줘요. 오버라이딩 메서드는 Overriding and Hiding Methods 섹션에서 다뤄요.

메서드를 오버라이드할 때 이 어노테이션을 사용하는 것이 필수는 아니지만, 오류를 예방하는 데 도움이 돼요. @Override가 붙은 메서드가 슈퍼클래스의 메서드를 제대로 오버라이드하지 못하면 컴파일러가 오류를 생성해요.

@SuppressWarnings

@SuppressWarnings 어노테이션은 컴파일러가 생성할 특정 경고를 억제하라고 지시해요. 다음 예시에서는 deprecated 메서드가 사용되어 컴파일러가 평소라면 경고를 생성하는데, 이 어노테이션이 경고를 억제해요.


@SuppressWarnings("deprecation")
void useDeprecatedMethod() {
    // deprecate warning - suppressed
    object.deprecatedMethod();
}

모든 컴파일러 경고는 어떤 범주에 속해요. Java Language Specification은 네 가지 범주를 나열해요.

  • unchecked 문자열로 지정되는 unchecked 경고
  • deprecation 문자열로 지정되는 deprecation 경고
  • removal 문자열로 지정되는 removal 경고
  • preview 문자열로 지정되는 preview 경고

unchecked 경고는 제네릭이 등장하기 전에 작성된 레거시 코드와 상호작용할 때 발생할 수 있어요. 여러 범주의 경고를 억제하려면 이런 문법을 써요.


@SuppressWarnings({"unchecked", "deprecation"})
@SafeVarargs

@SafeVarargs 어노테이션은 메서드나 생성자에 적용하면, 코드가 가변 인자(varargs) 파라미터에 대해 안전하지 않은 연산을 수행하지 않는다고 단언해요. 이 어노테이션 타입을 사용하면 varargs 사용과 관련된 unchecked 경고가 억제돼요.

@FunctionalInterface

@FunctionalInterface 어노테이션은 Java SE 8에서 도입됐는데, 타입 선언이 Java Language Specification이 정의한 바대로 함수형 인터페이스로 의도되었음을 나타내요.

다른 어노테이션에 적용되는 어노테이션

다른 어노테이션에 적용되는 어노테이션을 **메타 어노테이션(meta-annotation)**이라고 불러요. java.lang.annotation에는 여러 메타 어노테이션 타입이 정의되어 있어요.

@Retention

@Retention 어노테이션은 표시된 어노테이션이 어떻게 저장되는지를 지정해요.

  • RetentionPolicy.SOURCE — 표시된 어노테이션은 소스 수준에서만 유지되고 컴파일러가 무시해요.
  • RetentionPolicy.CLASS — 표시된 어노테이션은 컴파일 시점에 컴파일러가 유지하지만 JVM(자바 가상 머신)은 무시해요.
  • RetentionPolicy.RUNTIME — 표시된 어노테이션은 JVM이 유지하므로 런타임 환경이 사용할 수 있어요.
@Documented

@Documented 어노테이션은 지정된 어노테이션이 사용될 때마다 그 요소들이 Javadoc 도구로 문서화되어야 한다는 것을 나타내요. (기본적으로 어노테이션은 Javadoc에 포함되지 않아요.) 자세한 내용은 Javadoc 도구 문서를 참고하세요.

@Target

@Target 어노테이션은 다른 어노테이션에 표시해서, 그 어노테이션이 적용될 수 있는 자바 요소의 종류를 제한해요. target 어노테이션은 값으로 다음 요소 타입 중 하나를 지정해요.

  • ElementType.ANNOTATION_TYPE — 어노테이션 타입에 적용할 수 있어요.
  • ElementType.CONSTRUCTOR — 생성자에 적용할 수 있어요.
  • ElementType.FIELD — 필드나 프로퍼티에 적용할 수 있어요.
  • ElementType.LOCAL_VARIABLE — 지역 변수에 적용할 수 있어요.
  • ElementType.METHOD — 메서드 수준의 어노테이션에 적용할 수 있어요.
  • ElementType.MODULE — 모듈 선언에 적용할 수 있어요.
  • ElementType.PACKAGE — 패키지 선언에 적용할 수 있어요.
  • ElementType.PARAMETER — 메서드의 파라미터에 적용할 수 있어요.
  • ElementType.RECORD_COMPONENT — 레코드의 컴포넌트에 적용할 수 있어요.
  • ElementType.TYPE — 클래스, 추상 클래스, 인터페이스, 어노테이션 인터페이스, 열거형 또는 레코드 선언에 적용할 수 있어요.
  • ElementType.TYPE_PARAMETER — 타입의 파라미터에 적용할 수 있어요.
  • ElementType.TYPE_USE — 타입이 사용되는 곳에 적용할 수 있어요. 예를 들어 필드 선언에 적용하죠.
@Inherited

@Inherited 어노테이션은 어노테이션 타입이 슈퍼클래스로부터 상속될 수 있음을 나타내요. (기본값은 상속 없음이에요.) 사용자가 어노테이션 타입을 조회했는데 해당 클래스에 그 타입의 어노테이션이 없다면, 클래스의 슈퍼클래스를 대상으로 그 어노테이션 타입을 조회해요. 이 어노테이션은 클래스 선언에만 적용돼요.

@Repeatable

@Repeatable 어노테이션은 Java SE 8에서 도입됐는데, 표시된 어노테이션이 동일한 선언이나 타입 사용에 두 번 이상 적용될 수 있음을 나타내요. 자세한 내용은 Repeating Annotations 섹션을 참고하세요.

타입 어노테이션과 플러그 가능한 타입 시스템

Java SE 8 이전에는 어노테이션을 선언에만 적용할 수 있었어요. Java SE 8부터는 어노테이션을 어떤 타입 사용에도 적용할 수 있게 됐어요. 이 말은 타입을 쓰는 어디서든 어노테이션을 쓸 수 있다는 뜻이에요. 타입이 사용되는 곳의 몇 가지 예로는 클래스 인스턴스 생성 표현식(new), 캐스트, implements 절, throws 절이 있어요. 이런 형태의 어노테이션을 타입 어노테이션이라고 부르고, 몇 가지 예시는 Where Annotations Can Be Used 섹션에 있어요.

타입 어노테이션은 자바 프로그램을 더 강한 타입 검사로 분석할 수 있도록 도입됐어요. Java SE 8 릴리스 자체는 타입 검사 프레임워크를 제공하지 않지만, 자바 컴파일러와 함께 사용되는 하나 이상의 플러그 가능한 모듈로 구현된 타입 검사 프레임워크를 직접 작성(또는 다운로드)할 수 있게 허용해요.

예를 들어 프로그램의 특정 변수가 절대 null로 할당되지 않도록, NullPointerException을 유발하지 않도록 보장하고 싶다고 해 볼게요. 커스텀 플러그인을 작성해 이를 검사할 수 있어요. 그러면 코드를 수정해서 그 특정 변수에 어노테이션을 붙여 절대 null로 할당되지 않는다고 표시할 수 있어요. 변수 선언은 이렇게 생겼을 거예요.


@NonNull String str;

명령줄에 NonNull 모듈을 포함해 코드를 컴파일하면, 컴파일러가 잠재적 문제를 감지했을 때 경고를 출력해요. 그러면 오류를 피하도록 코드를 수정할 수 있죠. 모든 경고가 사라지도록 코드를 고치고 나면, 프로그램 실행 시 그 특정 오류는 더 이상 발생하지 않아요.

각각 다른 종류의 오류를 검사하는 타입 검사 모듈을 여러 개 사용할 수도 있어요. 이렇게 하면 자바 타입 시스템 위에 원하는 시점과 위치에 특정 검사를 추가해서 쌓아 올릴 수 있어요.

타입 어노테이션을 현명하게 사용하고 플러그 가능한 타입 검사기를 함께 쓰면, 더 강하고 오류에 덜 취약한 코드를 작성할 수 있어요.

많은 경우 직접 타입 검사 모듈을 작성할 필요가 없어요. 이미 이 작업을 해 둔 제3자가 있거든요. 예를 들어 워싱턴 대학교가 만든 Checker Framework를 활용할 수 있어요. 이 프레임워크에는 NonNull 모듈뿐 아니라 정규식 모듈과 뮤텍스 락 모듈도 포함되어 있어요. 자세한 내용은 Checker Framework를 참고하세요.

반복 어노테이션

동일한 어노테이션을 한 선언이나 타입 사용에 여러 번 적용하고 싶은 상황이 있어요. Java SE 8부터 반복 어노테이션이 이걸 가능하게 해 줘요.

예를 들어 UNIX의 cron 서비스처럼, 주어진 시간이나 특정 일정에 메서드를 실행하게 해 주는 타이머 서비스를 사용하는 코드를 작성한다고 해 볼게요. 이제 메서드 doPeriodicCleanup()을 매월 마지막 날과 매주 금요일 오후 11시에 실행하도록 타이머를 설정하고 싶어요. 타이머를 실행하도록 설정하려면 @Schedule 어노테이션을 만들고 doPeriodicCleanup() 메서드에 두 번 적용하면 돼요. 첫 번째 사용은 월말 마지막 날을, 두 번째는 금요일 오후 11시를 지정해요. 다음 코드 예시를 볼게요.


@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

앞선 예시는 메서드에 어노테이션을 적용했어요. 표준 어노테이션을 쓸 수 있는 곳이라면 어디든 어노테이션을 반복할 수 있어요. 예를 들어 무단 접근 예외를 처리하는 클래스가 있다고 해 볼게요. 관리자용 @Alert 어노테이션 하나와 관리자 외 직원용 @Alert 어노테이션을 하나씩 클래스에 붙일 수 있어요.


@Alert(role="Manager")
@Alert(role="Administrator")
public class UnauthorizedAccessException extends SecurityException { ... }

호환성 때문에 반복 어노테이션은 자바 컴파일러가 자동으로 생성하는 **컨테이너 어노테이션(container annotation)**에 저장돼요. 컴파일러가 이렇게 하려면 코드에 두 가지 선언이 필요해요.

1단계: 반복 어노테이션 타입 선언

어노테이션 타입은 @Repeatable 메타 어노테이션으로 표시해야 해요. 다음 예시는 커스텀 @Schedule 반복 어노테이션 타입을 정의해요.


@Repeatable(Schedules.class)
public @interface Schedule {
   String dayOfMonth() default "first";
   String dayOfWeek() default "Mon";
   int hour() default 12;
}

괄호 안에 있는 @Repeatable 메타 어노테이션의 값은, 자바 컴파일러가 반복 어노테이션을 저장할 때 생성하는 컨테이너 어노테이션의 타입이에요. 이 예시에서 컨테이너 어노테이션 타입은 @Schedules이므로, 반복되는 @Schedule 어노테이션은 @Schedules 어노테이션에 저장돼요.

먼저 반복 가능하다고 선언하지 않고 동일한 어노테이션을 선언에 적용하면 컴파일 타임 오류가 발생해요.

2단계: 컨테이너 어노테이션 타입 선언

컨테이너 어노테이션 타입은 배열 타입의 value 요소를 가져야 해요. 배열 타입의 컴포넌트 타입은 반복 가능한 어노테이션 타입이어야 해요. @Schedules 컨테이너 어노테이션 타입의 선언은 다음과 같아요.


public @interface Schedules {
   Schedule[] value();
}

어노테이션 가져오기

Reflection API에는 어노테이션을 가져오는 여러 메서드가 있어요. 단일 어노테이션을 반환하는 메서드(예: AnnotatedElement.getAnnotation(Class))의 동작은 변하지 않았는데, 요청된 타입의 어노테이션이 하나 있을 때만 어노테이션 하나를 반환해요. 요청된 타입의 어노테이션이 둘 이상 있으면, 먼저 컨테이너 어노테이션을 가져온 뒤에 얻을 수 있어요. 이렇게 해서 레거시 코드가 계속 동작해요. Java SE 8에는 컨테이너 어노테이션을 훑어서 한 번에 여러 어노테이션을 반환하는 다른 메서드도 도입됐어요. 예를 들면 AnnotatedElement.getAnnotationsByType(Class)가 있어요. 사용 가능한 모든 메서드에 대한 정보는 AnnotatedElement 클래스 명세를 참고하세요.

설계 고려 사항

어노테이션 타입을 설계할 때는 그 타입의 어노테이션 **카디널리티(cardinality)**를 고려해야 해요. 이제 어노테이션을 0번, 1번, 또는(타입이 @Repeatable로 표시된 경우) 여러 번 사용할 수 있게 됐어요. @Target 메타 어노테이션으로 어노테이션 타입을 사용할 수 있는 위치를 제한할 수도 있어요. 예를 들어 메서드와 필드에만 사용할 수 있는 반복 어노테이션 타입을 만들 수 있죠. 어노테이션 타입을 신중하게 설계해서, 사용하는 프로그래머가 최대한 유연하고 강력하게 활용할 수 있게 하는 것이 중요해요.

더 알아보기 (Learn more)

이 장에서 배운 내용을 더 깊게 파고 싶다면 공식 문서의 관련 섹션을 이어서 보면 좋아요. 어노테이션을 런타임에 꺼내 쓰는 방법은 Reflection API 문서에서, 오버라이딩과 관련된 어노테이션 사용은 상속 관련 장에서 더 확인할 수 있어요.