<Java> 애노테이션

라모스·2021년 9월 3일
0

Java☕

목록 보기
12/14
post-thumbnail

" 자바의 애노테이션에 대해 학습하세요. "

학습할 것

  • 애노테이션 정의하는 방법
  • @retention
  • @target
  • @documented
  • 애노테이션 프로세서

Intro. annotation이란?

애노테이션은 프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이다. 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다.

// java.lang.annotation.Annotation이 애노테이션의 조상이다.

📌 메타 데이터란?
데이터를 위한 데이터를 의미하며, 한 데이터에 대한 설명을 의미하는 데이터

일반적인 사용 사례

  • 애노테이션을 통해 컴파일러는 오류를 감지하거나 경고를 억제할 수 있다.
  • 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보를 제공한다.
  • 실행 시 특정 기능을 실행하도록 정보를 제공한다.

애노테이션 정의하는 방법

새로운 애노테이션을 정의하는 방법은 '@' 기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일합니다.

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

애노테이션의 요소

애노테이션 내에 선언된 메서드를 '에너테이션의 요소(element)'라고 하며, 아래에 선언된 TestInfo 애너테이션은 다섯 개의 요소를 갖는다.

@interface TestInfo {
    int count();
    String testedBy();
    String[] testTools();
    TestType testType();	// enum TestType { FIRST, FINAL }
    DateTime testDate();	// 자신이 아닌 다른 애너테이션(@DateTime)을 포함할 수 있다.
}

애너테이션의 요소는 반환값이 있고 매개변수는 없는 추상 메서드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다. 다만, 애너테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다. 요소의 이름도 같이 적어주므로 순서는 상관 없다.

애노테이션 요소의 규칙

애노테이션의 요소를 선언할 때 반드시 지켜야 하는 규칙은 다음과 같다.

  • 요소의 타입은 기본형, String, enum, 애노테이션, Class만 허용된다.
  • ()안에 매개변수를 선언할 수 없다.
  • 예외를 선언할 수 없다.
  • 요소를 타입 매개변수로 정의할 수 없다.
@interface AnnoTest {
    int id = 100; // OK. 상수 선언. static final int id = 100;
    String major(int i, int j);	// 에러. 매개변수를 선언할 수 없음
    String minor() throws Exception; // 에러. 예외를 선언할 수 없음
    ArrayList<T> list(); // 에러. 요소의 타입에 타입 매개변수 사용불가
}

표준 애노테이션

자바에서 제공되는 애노테이션은 크게 2가지로 구성되어있다.
하나는 자바 코드를 작성할 때 사용되는 어노테이션이고, 다른 하나는 어노테이션을 정의하기 위해 필요한 것들이다.

@Override

메소드 앞에만 붙일 수 있는 애노테이션으로 오버라이드를 할 때 사용되며, 메소드가 오버라이드 되었는지 컴파일러에게 알려주는 역할을 한다. 이것은 생략 가능하다.

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

위와 같은 구조로 되어있으며 다음과 같이 사용한다.

class Parent {
    void parentMethod() { }
}

class Child extends Parent {
    @Override
    void parentMethod() { }
}

@Deprecated

이 애노테이션이 붙은 대상은 다른 것으로 대체되었으니 더 이상 사용하지 않을 것을 권한다는 의미이다. 예를 들어, java.util.Date 클래스의 대부분의 메소드에는 @Deprecated가 붙어있는데, Java API에서 Date클래스의 getDate()를 보면 다음과 같이 적혀있다.

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

이 클래스, 메소드는 더이상 사용하지 말고, Calendar 클래스 사용을 권장하고 있다.

더 이상 사용을 권장하지 않는 클래스를 자바 자체에서 삭제하면 되지 않을까란 의문이 생길 수도 있겠으나, 이 방법을 사용하면 기존에 Date 클래스를 통해 잘 동작하던 프로그램이 동작하지 않게 되어 개발자가 일일이 그것들을 찾아다니면서 수정해야한다. 결국, 호환성 때문에 존재하는 어노테이션이 @Deprecated 이다.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE})
public @interface Deprecated {
    /**
     * Returns the version in which the annotated element became deprecated.
     * The version string is in the same format and namespace as the value of
     * the {@code @since} javadoc tag. The default value is the empty
     * string.
     *
     * @return the version string
     * @since 9
     */
    String since() default "";

    /**
     * Indicates whether the annotated element is subject to removal in a
     * future version. The default value is {@code false}.
     *
     * @return whether the element is subject to removal
     * @since 9
     */
    boolean forRemoval() default false;
}

애노테이션은 인터페이스로 되어있기 때문에 구현이 되지 않는다는 것을 알 수 있다. 다만, default, static은 작성되지 않는다.

@FunctionalInterface

'함수형 인터페이스(functional interface)'를 선언할 때, 이 애노테이션을 붙이면 컴파일러가 함수형 인터페이스를 올바르게 선언했는지 확인하고, 잘못된 경우 에러를 발생시킨다.

함수형으로 작성할 때는 몇 가지 규칙이 있는데, 그 중 중요한 것은 '인터페이스는 단 하나의 메소드만 존재해야 한다' 는 것이다. 이 규칙을 깨면 함수형으로 만들 수 없다.
// 추후에 람다에서 자세히 다룰 예정임.

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

@SuppressWarnings

컴파일러가 보여주는 경고 메시지가 나타나지 않게 억제해준다.

억제 코드억제 내용
deprecation@Deprecated가 붙은 대상에 발생하는 경고
unchecked제네릭 타입을 지정하지 않을 때 발생하는 경고
rawtypes제네릭을 사용하지 않아서 발생하는 경고
varargs가변인자의 타입이 제네릭 타입일 때 발생하는 경고

어노테이션 정보는 다음과 같다.

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, MODULE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
    
    String[] value();
}

이 밖에도 다른 표준 애노테이션들이 있다.

  • @safeVarags: 제네릭 타입의 가변인자에 사용됨
  • @Native: native 메서드에서 참조되는 상수 앞에 붙임

메타 애노테이션

메타 애노테이션은 애노테이션을 위한 애노테이션이다. 즉, 애노테이션을 정의할 때 애노테이션의 적용대상(target)이나 유지기간(retention)등을 지정하는데 사용된다.

@Target

애노테이션이 적용가능한 대상을 지정하는데 사용된다. 여러 개의 값을 지정할 때는 배열처럼 괄호{}를 사용해야 한다.

적용 대상은 다음과 같다.

대상 타입의미
ANNOTATION_TYPE애노테이션
CONSTRUCTOR생성자
FIELD필드(멤버변수, enum상수)
LOCAL_VARIABLE지역변수
METHOD메소드
PACKAGE패키지
PARAMETER매개변수
TYPE타입(클래스, 인터페이스, enum)
TYPE_PARAMETER타입 매개변수(JDK1.8)
TYPE_USE타입이 사용되는 모든 곳(JDK1.8)

예시)

@Target(ElementType.METHOD) // 이 애노테이션은 메소드에서 사용되는 것임을 명시함.
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

@Documented

애노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 한다. Java에서 제공하는 기본 애노테이션 중에 @Override@SuppressWarnings를 제외하고는 모두 이 메타 애노테이션이 붙어 있다.

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

주의 해야 할 점은 내 코드가 javadoc에 올라간다는 말이 아니라, 내가 직접 javadoc을 만들 수 있다는 것을 생각하자.

@Retention

애노테이션이 유지되는 범위(기간)를 지정하는데 사용된다. 애노테이션의 유지 정책의 종류는 다음과 같다.

유지 정책의미
SOURCE소스 파일에만 존재. 클래스파일에는 존재하지 않음.
CLASS클래스 파일에 존재. 실행시에 사용불가. 기본값
RUNTIME클래스 파일에 존재. 실행시에 사용가능.
@Documented
@Retention(RetentionPolicy.RUNTIME) // RUNTIME 까지 유지됨.
@Target(ElementType.TYPE)
public @interface FunctionalInterface {}

@FunctionalInterface를 예시로 보자면, 유지 정책이 RUNTIME이기 때문에 실행시에 사용가능 하다는 점을 확인할 수 있다.

한편, SOURCE로 사용되면 일반 주석처럼 사용된다는 뜻이다. 소스단에서만 사용하고 컴파일 될 때는 이 애노테이션이 필요없다는 뜻이 된다. 이는 @Override가 대표적이다.

@Target(ElementType.METHOD)
@Retention(RetetionPolicy.SOURCE) // SOURCE: 소스 파일에만 존재
public @interface Override {}

애노테이션 프로세서(Annotation Processor)

애노테이션을 이용해서 프로세스를 처리하는 것을 의미한다. 애노테이션을 사용하기 위해서는 애노테이션 프로세서가 필요하다. 특히 애노테이션 프로세서의 특징은 컴파일 단계에서 애노테이션에 정의된 액션을 처리하는 데, 이는 우리의 애플리케이션이 실행되기 전 체킹을 해주기 때문에 애노테이션으로 의도한 대로 이루어지지 않을 경우 에러나 경고를 보여주기도 하며, 소스코드(.java)와 바이트코드(.class)를 생성해주기도 한다.

📌 동작 구조

  1. 애노테이션 프로세서를 사용한다는 것을 자바 컴파일러가 알고 있는 상태에서 컴파일을 수행한다.
  2. 애노테이션 프로세서들이 각자의 역할에 맞게 구현되어 있는 상태에서 실행되지 않은 애노테이션 프로세서를 실행한다.
  3. 애노테이션 프로세서 내부에서 애노테이션에 대한 처리를 한다.
  4. 자바 컴파일러가 모든 애노테이션 프로세서가 실행되었는지 검사하고, 모든 애노테이션 프로세서가 실행되지 않았다면 반복한다.

애노테이션 프로세서의 대표적인 기술로는 Lombok이 있다.
롬복은 다음과 같이 동작한다.

@Data
public class Test {
    private int num;
}

이것을 돌려보면 분명히 setter, getter를 추가한 적이 없지만, 신기하게도 클래스 파일에 다음과 같이 추가되어 있음을 확인할 수 있다.

public class Test {
    private int num;

    public Test() {
    }

    public int getNum() {
        return this.num;
    }

    public void setNum(final int num) {
        this.num = num;
    }

    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        } else if (!(o instanceof Test)) {
            return false;
        } else {
            Test other = (Test)o;
            if (!other.canEqual(this)) {
                return false;
            } else {
                return this.getNum() == other.getNum();
            }
        }
    }

    protected boolean canEqual(final Object other) {
        return other instanceof Test;
    }

    public int hashCode() {
        int PRIME = true;
        int result = 1;
        int result = result * 59 + this.getNum();
        return result;
    }

    public String toString() {
        return "Test(num=" + this.getNum() + ")";
    }
}

이 것은 Runtime시 생성된 것이 아닌 컴파일 단계에서 생성된 것이다.
@Data라는 애노테이션이 붙으면 Java는 이 애노테이션이 붙은 곳을 찾아 마치 모자에서 토끼를 꺼내듯 위와 같은 코드를 만들어 준다.

비슷한 예시로 Spring의 IoC도 마찬가지다.
@Component라는 애노테이션이 붙으면 Spring은 이 애노테이션이 붙은 곳을 Bean으로 만들어 준다.
(쉽게 말해 자바 객체를 만들어 준다고 생각하면 이해하기 쉬움)

애노테이션은 단독으로 사용되면 단순한 주석에 불과하지만, 애노테이션 프로세서라는 기술 덕분에 위와 같이 다양하게 사용할 수 있다.

자바 리플렉션(Java Reflection)

모든 클래스 파일은 클래스로더(Classloader)에 의해 메모리에 올라갈 때, 클래스에 대한 정보가 담긴 객체를 생성하는데 이 객체를 클래스 객체라고 한다. 이 객체를 참조할 때는 '클래스이름.class'의 형식을 사용한다.

'클래스이름.class'를 사용해서 클래스의 필드, 생성자, 메소드에 대한 정보를 얻을 수 있다.

메서드명리턴타입설명
getFields()Field[]접근 제어자가 public인 필드들을 Field 배열로 반환. 부모 클래스의 필드들도 함께 반환한다.
getConstructors()Constructor[]접근 제어자가 public인 생성자들을 Constructor 배열로 반환. 부모 클래스의 생성자들도 함께 반환한다.
getMethods()Method[]접근 제어자가 public인 메서드들을 Method 배열로 반환. 부모 클래스의 메서드들도 함께 반환한다.
getDeclaredFields()Field[]접근 제어자에 상관없이 모든 필드들을 Field 배열로 반환. 부모 클래스의 필드들은 반환하지 않는다.
getDeclaredConstructors()Constructor[]접근 제어자에 상관없이 모든 생성자들을 Constructor 배열로 반환. 부모 클래스의 생성자들은 반환하지 않는다.
getDeclaredMethod()Method[]접근 제어자에 상관없이 모든 메서드들을 Method배열로 반환. 부모 클래스의 메서드들은 반환하지 않는다.

📌 애노테이션 정보를 얻기 위한 메소드 목록

리턴타입메소드명/설명
booleanisAnnotationPresent(Class<? Extends Annotation> annotationClass) / 지정한 애노테이션이 적용되었는지 여부를 확인. Class에서 호출했을 경우 상위 클래스에 적용된 경우에도 true를 리턴.
AnnotationgetAnnotation(Class<T> annotationClass) / 지정한 애노테이션이 적용되어 있으면 애노테이션을 반환하고 그렇지 않은 경우 null을 반환. Class에서 호출한 경우 상위 클래스에 적용된 애노테이션도 반환한다.
Annotation[]getAnnotations() / 적용된 모든 애노테이션을 반환한다. Class에 사용됐을 경우 상위 클래스에 적용된 애노테이션까지 전부 포함해서 반환. 애노테이션이 없을 경우 길이가 0인 배열을 반환한다.
Annotation[]getDeclaredAnnotation() / 직접 적용된 모든 애노테이션을 리턴한다. Class에서 호출했을 경우 상위 클래스에 적용된 애노테이션은 포함되지 않는다.(상위 클래스의 @Inherited가 붙은 애노테이션을 무시한다.)

References

profile
Step by step goes a long way.

0개의 댓글