12-3. 애너테이션

Hyun Jun·2022년 2월 6일
0

자바의 정석

목록 보기
43/52
post-thumbnail
post-custom-banner

애너테이션 (Annotation)

애너테이션이란?

컴파일러를 비롯한 다른 프로그램들을 위해 소스코드 안에 약속된 형식으로 정보를 표시하는 것.

예를 들어 어떤 메서드 위에 @Test라는 애너테이션을 붙여놓으면, 테스트 프로그램에게 이 메서드를 테스트해야한다는 것을 알려줄 수 있음

@Test
public void method() {
    ...
}

외부 프로그램이 아닌 JDK에서 제공되는 표준 애너테이션은 자바 컴파일러를 위한 용도.

 

표준 애너테이션

@Override

메서드 앞에만 붙일 수 있음.

이 메서드가 조상의 메서드를 오버라이딩한 것이라는 의미.

애너테이션 없이 오버라이딩할 때 메서드 이름을 잘못 적는 실수를 하면, 컴파일러는 이 실수를 잡아주지 못하는데,

class Parent {
    void parentMethod() {
        ...
    }
}

class Child extends Parent {
    void parentmethod() { // 잘못된 오버라이딩인데, 에러 발생 없음
        ...
    }
}

@Override라고 명시해주면 조상 클래스에 해당 이름의 메서드가 있는지 확인하고 없는 경우 에러 메시지를 출력해 실수를 바로잡을 수 있게 해줌.

class Child extends Parent {
    @Override
    void parentmethod() { // 애너테이션에 의해 에러 발생
        ...
    }
}

 

@Deprecated

JDK 버전이 업데이트 될 때, 낡은 기능을 대체할 것이 추가되어도 하위 호환성을 위해 기존의 기능을 제거하지 않고 남겨둠.

더 이상 사용되지 않는 필드나 메서드에 @Deprecated을 붙여서 이 기능이 더 이상 쓰이지 않으니, 더 개선된 것을 사용하라고 권장하고 있음.

 

ex) Java API에서 Date 클래스에 정의된 getDate() 메서드의 설명은 아래와 같고,

getDate()
Deprecated.
As of JDK version 1.1, replaced by Calendar.get(Calendar.DAY_OF_MONTH).

Calendar 클래스의 get(Calendar.DAY_OF_MONTH)를 사용하도록 권장하고 있음.

실제 코드에도 애너테이션이 달려있음

@Deprecated
public int getDate() {
    ...
}

 

@FunctionalInterface

함수형 인터페이스 선언 시, 컴파일러가 올바로 선언했는지 확인해줌.

함수형 인터페이스에는 추상 메서드가 오직 1개만 있어야함

@FunctionalInterface
public interface Shareable {
    public abstract void share();
    public abstract void copy();
}
java: Unexpected @FunctionalInterface annotation
Shareable is not a functional interface
multiple non-overriding abstract methods found in interface Shareable

 

@SuppressWarnings

컴파일러의 경고 메시지를 억제(suppress)해줌.

특정 대상에서 경고가 발생하는 것을 알고도 묵인해야하는 경우, @SuppressWarnings를 붙이면 컴파일 시 대상으로부터 아무런 경고 메시지도 뜨지 않게 할 수 있음.

 

억제할 수 있는 경고 메시지의 종류

  • "deprecation": @Deprecated가 붙은 대상을 사용해서 발생하는 경고

  • "unchecked": 제네릭 타입을 지정하지 않았을 때 발생하는 경고

  • "rawtypes": 제네릭을 사용하지 않고 원시 타입으로 사용할 때 발생하는 경고

  • "varargs": 가변 인자의 타입이 제네릭 타입일 때 발생하는 경고

...등이 있고, 이들 중 하나를 애너테이션 뒤의 괄호에 넣어 사용

@SuppressWarnings("unchecked")
ArrayList list = new ArrayList(); // 원시 타입으로 선언
list.add(obj); // 메서드를 호출하면서 unchecked 경고 발생

add 메서드를 호출해서 원시 타입인 list에 무슨 타입인지 모르는 obj라는 요소를 추가하려고 시도하면서 "unchecked call ..." 경고가 발생.

하지만 애너테이션으로 막았기 때문에 해당 경고는 표시되지 않음

 

2개 이상의 경고를 억제할 때는 {}안에 넣음

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

JDK가 계속 업데이트 되면서 새로운 종류의 경고가 나오게 될텐데, 이를 파악하기 위해서는 컴파일시 "-Xlint" 옵션을 붙임.

나타난 경고의 내용에서 [] 안에 있는 것이 경고의 종류

 

@SafeVarargs

메서드의 매개변수가 가변인자이고, 그 타입이 non-reifiable 타입인 경우,

해당 메서드를 선언하는 곳과 호출하는 곳에서 모두 "unchecked" 경고가 발생함.

non-reifiable 타입: 컴파일 이후에 제거되는 타입

제네릭 타입처럼 컴파일 타임에만 사용되고 컴파일 이후에는 남아있지 않다는 뜻

그러나 프로그래머가 코드에 문제가 없다고 확신할 때, "unchecked" 경고를 억제하기 위해 @SafeVarargs를 사용할 수 있음.

 

java.util.Arrays 클래스의 asList() 메서드가 좋은 예시.

public static <T> List<T> asList(T... item) {
    return new ArrayList<T>(item);
}

📌 [주의] 반환되는 ArrayListArrays 클래스의 내부 클래스이므로 일반적으로 쓰는 ArrayList와는 다름.

매개 변수로 넘겨 받은 값들(가변 인자)로 배열을 만든 뒤, 내부 클래스인 ArrayList의 인스턴스에 담아 반환하는 과정.

문제는 이 가변 인자가 제네릭 타입 T라는 것인데, 메서드에 선언된 원시 타입 T는 컴파일 시 Object로 바뀌면서 사라짐.

가변 인자는 여러 개의 인자를 뜻하므로, 결국 매개 변수 타입이 Object[]가 되는데

public static List asList(Object[] item) {
    return new ArrayList<T>(item);
}

Object[]에는 온갖 타입의 객체가 다 들어갈 수 있으므로, 이 짬뽕 같은 배열을 가지고서 T 타입 원소만을 가진 ArrayList로 만드는 것은 컴파일러 입장에서는 위험한 시도임.

그러나 실제로 메서드가 호출되는 부분에서는 asList()의 인자가 T 타입(혹은 T타입 요소로 이루어진 배열)이 맞는지 컴파일러가 확인할 것이기 때문에 문제가 되지 않음.

선언할 당시에는 컴파일러가 호출 과정을 겪어보지 않았으므로, 나중에 호출 과정에서 자기가 직접 메서드의 타입 체킹을 하게 될 거라는 건 모르고 있음.

따라서 이 메서드의 가변 인자가 type-safe 하다고 컴파일러에게 알리기 위해 @SafeVarargs를 붙임

이렇게 하면 선언하는 곳과 호출하는 곳 양쪽 모두 "unchecked" 경고가 억제되면서 경고창에 아무것도 뜨지 않게 되지만,

@SuppressWarnings("unchecked")는 사용한 곳에서만 억제가 되므로 양쪽에 모두 붙여놔야 같은 효과를 낼 수 있음

-Xlint 옵션으로 컴파일 해보면 "varargs" 경고가 남아있는 것을 볼 수 있는데, 이마저도 깔끔히 없애주기 위해 의례적으로 @SuppressWarnings("varargs")도 함께 사용해줌.

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... item) {
    return new ArrayList<T>(item);
}

 

메타 애너테이션

애너테이션을 위한 애너테이션.

애너테이션을 정의할 때 적용 대상과 유지 기간을 지정하기 위해 사용.

 

@Target

애너테이션을 적용할 수 있는 대상을 지정

이러한 대상들은 java.lang.annotation.ElementType 열거형에 미리 정의되어 있음

예를 들어 @SuppressWarnings가 붙을 수 있는 대상은 다음과 같이 지정

import static java.lang.annotation.ElementType.*;

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    String[] value();
}
import static java.lang.annotation.ElementType.*;

@Target({FIELD, TYPE, TYPE_USE})
public @interface MyAnnotation { ... }

@MyAnnotation // TYPE
class Dummy {
    @MyAnnotation // FIELD
    int num;

    @MyAnnotation // TYPE_USE
    String str;
}

📌 [주의] FIELD는 기본형에, TYPE_USE는 참조형에 쓰임

 

@Retention

애너테이션이 유지되는 기간을 지정하는데에 사용

3가지 종류의 retention policy가 존재

이름의미
SOURCE소스 파일(.java)에만 존재, 클래스 파일(.class)에는 존재하지 않음
CLASS클래스 파일에 존재, 실행 시에는 사용 불가 (default)
RUNTIME클래스 파일에 존재, 실행 시에 사용 가능

 

SOURCE를 사용하면 애너테이션 정보가 컴파일 과정에서 사라지므로, 컴파일러만이 사용하는 애너테이션에 붙임.

직접 컴파일러를 작성할 것이 아니라면 이 정책을 사용할 일은 없음

ex) @Override의 정의

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}

 

CLASS를 사용하면 애너테이션 정보가 컴파일 이후의 클래스 파일까지는 살아있지만, 결국 클래스 파일이 JVM의 클래스 로더에 로딩될 때는 무시되기 때문에 런타임에는 애너테이션 정보가 남아있지 않음.

그래서 RetentionPolicy의 기본값인데도 실제로는 잘 사용되지 않음

 

RUNTIME을 사용하면 런타임에도 Reflection을 통해 클래스 파일에 저장된 애너테이션 정보를 읽어와 처리할 수 있음.

Reflection: 구체적인 클래스 타입을 알지 못해도 해당 클래스의 타입, 변수, 메서드에 접근할 수 있게 해주는 Java API

"당장은 어떤 타입의 클래스인지 몰라도 일단 넘어가고, 런타임에는 클래스 저장소를 뒤져서 해당 클래스에 대한 정보를 얻을 수 있으니 그 때 제대로 사용하라"는 뜻인 것 같음. (뇌피셜)

ex) @FunctionalInterface의 정의

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

@FunctionalInterface는 컴파일러가 체크해주는 애너테이션이면서 런타임에도 사용되므로 유지 정책 RUNTIME 필요

 

@Documented

애너테이션 정보가 javadoc으로 작성한 문서에 포함되도록 함.

@Override@SuppressWarnings를 제외한 모든 애너테이션에 붙어있는 메타 애너테이션.

 

@Inherited

애너테이션이 자손 클래스에 상속되도록 함.

@Inherited를 붙인 애너테이션을 조상 클래스에 붙이면, 자손 클래스에도 애너테이션이 똑같이 적용됨.

@Inherited
@interface SuperAnnotation {}
@SuperAnnotation
class Parent {}

// @SuperAnnotation 을 달아놓진 않았지만 달아놓은 것과 마찬가지로 인식됨
class Child extends Parent {}

 

@Repeatable

하나의 애너테이션을 여러번 붙일 수 있게함.

@Repeatable(ToDos.class)
@interface ToDo {
    String value();
}
@ToDo("delete test codes")
@ToDo("override inherited methods")
class MyClass { ... }

여기서 @Repeatable옆의 괄호에 들어간 ToDos.class는 여러번 반복 사용될 수 있는 @ToDo 애너테이션을 묶어서 다루게 해주는 컨테이너 애너테이션. 반드시 정의되어야 함

@interface ToDos {
    ToDo[] value();
}

 

@Native

Native Method에 의해 참조되는 상수 필드에 붙이는 애너테이션.

Native Method: JVM이 설치된 OS의 자체 메서드

Native Method는 Java 언어로 정의되어 있어서 호출도 Java의 일반적인 메서드 호출 방식과 같지만, 실제로 호출되는 것은 OS의 메서드임.

(이게 가능하게끔 Java로 정의해놓은 Native Method와 실제 OS의 메서드 사이의 중간다리 역할을 해주는 JNI(Java Native Interface)라는 개념이 있다고함.)

ex) java.lang.Long 클래스에 정의된 상수 MIN_VALUE

@Native public static final long MIN_VALUE = 0x800000000000000L;

 

애너테이션 타입 정의하기

직접 애너테이션을 만드는 방법은 아래와 같음

@interface 애너테이션이름 {
    타입 요소이름(); // 애너테이션의 요소 선언
    ...
}

 

애너테이션의 요소

애너테이션의 요소(element) == 애너테이션 내에 선언된 메서드

애너테이션 요소의 규칙:

  1. 요소의 타입은 기본형, String, enum, 애너테이션, Class만 허용

  2. 매개 변수는 선언 불가

  3. 예외 선언 불가

  4. 타입 매개변수 T 사용 불가

 

ex) @TestInfo 애너테이션을 선언해보자.

@interface TestInfo {
    int count();
    String tester();
    String[] testTools();
    TestType testType(); // TestType 열거형 타입
    DateTime testDate(); // DateTime 애너테이션 타입
}

@interface DateTime {
    String yymmdd();
    String hhmmss();
}

enum TestType {
    FIRST, FINAL
}

testDate()에서처럼 애너테이션 요소의 반환타입으로 다른 애너테이션을 사용할 수도 있음

 

  • 애너테이션 요소는 반환값만 있고, 매개변수는 없음.

  • 상속을 통한 구현 의무가 없음.

  • 애너테이션을 적용할 때는 요소의 값을 전부 지정해주어야함. (순서는 무관)

@TestInfo(
    count = 3, tester="Andy",
    testTools = {"JUnit", "AutoTester"},
    testType = TestType.FIRST,
    testDate = @DateTime(yymmdd = "220206", hhmmss = "171030")
)
public class Dummy { ... }

요소에 기본값을 설정해놓으면, 요소의 값이 지정되지 않았을 때 기본값이 사용됨.

단, 기본값으로 null은 사용 불가

@interface TestCount {
    int count() default 1;
}

@TestCount // count = 1 로 지정한것과 동일
public class Dummy { ... }

애너테이션의 요소가 1개 뿐이고, 이름이 value인 경우에는 애너테이션 적용 시 요소의 이름을 생략하고 값만 적어도 됨.

요소 타입이 배열이어도 이름이 value면 이름 생략 가능

@interface Tester {
    String value();
}

@Tester("Andy") // value = "Andy" 로 작성한 것과 동일
public class Dummy { ... }

요소의 반환타입이 배열인 경우네는 애너테이션 적용 시 {}로 묶어서 값을 지정함.

@interface TestTools {
    String[] toolName();
}

@TestTools(toolName = {"JUnit", "AutoTester"})
public class Dummy { ... }

📌 [주의] 값을 1개만 넣을 경우에는 {} 생략 가능. 값을 넣지 않을 때는 {} 생략 불가

@TestTools(toolName = "JUnit") // 값이 1개

@TestTools(toolName = {}) // 값이 없음

기본값을 지정할 때도 개수가 여러개면 {}로 묶음.

 

java.lang.Annotation

모든 애너테이션의 조상인 Annotation은 상속이 허용되지 않음.

@interface TestInfo extends Annotation { // (X) 상속 불가
    ...
}

Annotation은 일반 interface로 선언되어있음.

public interface Annotation {
    boolean equals(Object o);
    int hashCode();
    String toString();

    Class<? extends Annotation> annotationType(); // 애너테이션의 타입을 반환해줌
}

조상이 이렇게 생겼으므로, 모든 애너테이션의 객체는 equals(), hashCode(), toString() 호출 가능.

@DataAmount
@Threshold
class AnnotationTest {}

class Test {
    public static void main(String[] args) {
        Class<AnnotationTest> cls = AnnotationTest.class; // 클래스 객체
        Annotation[] annoArr = cls.getAnnotations(); // 모든 애너테이션을 배열로 얻어옴

        // Annotation anno = cls.getAnnotation(DataAmount.class);
        // 정보를 얻고자 하는 애너테이션의 클래스 객체를 매개변수로 지정

        for (Annotation a : annoArr) {
            System.out.println(a.toString());
            System.out.println(a.hashCode());
            System.out.println(a.equals(a));
            System.out.println(a.annotationType());
        }
    }
}

실행 결과)

@jdk.jfr.DataAmount("BYTES")
1280831300
true
interface jdk.jfr.DataAmount
@jdk.jfr.Threshold("0 ns")
1334472890
true
interface jdk.jfr.Threshold

이처럼 런타임에 애너테이션 정보를 얻고자 할 때는 클래스 객체를 통해 getAnnotation()이나 getAnnotations() 메서드를 사용함.

  • getAnnotation(): 매개변수에 정보를 얻고자 하는 애너테이션을 지정해야함.

  • getAnnotations(): 별도의 매개변수 지정 없이 모든 애너테이션을 배열로 받아옴.

 

마커 애너테이션 (Marker Annotation)

요소가 하나도 없는 비어있는 애너테이션

ex) @Override는 마커 애너테이션임.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {} // 요소가 없음
profile
Back-end Engineer 👨‍💻
post-custom-banner

0개의 댓글