애노테이션

파이 ఇ·2023년 8월 10일
1
post-thumbnail

💡 목표 : 자바의 애노테이션에 대해 학습해보자.

⚡️ 목차

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

Annotation

애노테이션이란 주석처럼 프로그래밍 언어에 영향을 미치지 않으며, 유용한 정보를 제공한다. 애노테이션은 프로그램에 대한 추가적인 정보를 제공하는데 사용한다. 쉽게 이해하기 위해 밑에 그림을 보자.

위 그림은 과거의 파일 관리 방법이었다. 자바 코드와 관련 설정 파일을 따로 저장하고 ver@.@로 구분하여 관리했다. 위와 같이 관리를 하는데 두 가지의 어려움이 있었다.

  1. 사람들이 자바 코드는 변경하는데 설정 파일을 업데이트 하지 않는 어려움.
  2. 설정과 코드가 분리되어 있어, 개발에 대한 어려움.

그래서 다음과 같은 관리 방법을 사용하게 되었다.

  • 개발자들은 소스코드에 대한 문서를 따로 만들기보다 소스 코드와 문서를 하나의 파일로 관리하는 것이 낫다고 판단했다.
  • 그래서 소스코드의 주석 /** ~ */에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML 문서를 생성해내는 javadoc.exe라는 프로그램을 만들어서 사용했다.
  • 위와 같이 소스코드 안에 다른 프로그램 (ex. javadoc.exe)을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 애노테이션이다.
  • 애노테이션은 주석처럼 프로그래밍 언어에 영향을 미치지 않으면서, 다른 프로그램에게 정보를 제공한다 (ex. @Test 애노테이션 같은 경우는 테스트 프로그램인 Junit에게 테스트를 해야한다고 알리기만 할 뿐 메서드가 포함된 프로그램 자체에는 아무런 영향을 끼치지 않는다.)
  • JDK에서 기본적으로 제공하는 애노테이션이 아닌 Junit과 같은 다른 프로그램에서 제공하는 약속된 형식으로 해당 정보를 애노테이션으로 제공하기만 하면 된다.

    javadoc ?
    메타데이터의 한 형태인 주석은 프로그램 내에 일부가 아닌, 프로그램에 대한 데이터를 제공한다.
    메타데이터 ?
    데이터에 대한 데이터이며, 간단히 정리하자면 어떤 목적을 가지고 만들어진 데이터라고 할 수 있다.

애노테이션의 사용 예시

@Test // 해당 메서드는 Test 대상임을 프로그램에게 알리는 역할을 한다.
public void method() {
	.....
}

Annotation의 용도

애노테이션은 사용할 때 여러가지의 용도가 있다.

  • Information for the compiler -- Annotations can be used by the compiler to detect errors or suppress warings.
    • 컴파일러를 위한 정보 -- 애노테이션을 사용하여 컴파일러에게 오류를 감지하거나 경고를 차단할 수 있게끔 해준다.
  • Compile-time and deployment-time processing -- Software tools can process annotation information to generate code, XML files, and so forth
    • 컴파일 시간 및 배포 시간 처리 -- 소프트 웨어 도구(IDE?)는 주석 정보를 처리하여 코드, XML 파일 등을 생성 할 수 있게 해준다.
  • Runtime processing -- Some annotations are available to be examined at runtime
    • 런타임 처리 -- 일부 주석은 런타임에 검사할 수 있다.

애노테이션 정의

커스텀 애노테이션이라고 부르며 이와 관련된 소스는 java.lang.annotation 패키지에 속해있다. 커스텀 애노테이션을 정의하려면 interface 키워드를 쓰고 앞에 @를 붙여주면 애노테이션이 정의된다.

@interface MyAnnotation {
	// 애노테이션의 요소를 선언한다.
}

햄버거를 나타내고 두 개의 요소를 가지고 있는 애노테이션을 테스트로 만들어보면

@interface Hamburger {
	String patty(); // 패티를 의미하는 요소
    String source(); // 소스를 의미하는 요소
}

인터페이스 공부할 때 인터페이스에서 추상 메서드를 선언할 수 없는 것처럼, 애노테이션도 추상 메서드로 선언한다. 추상 메서드로 선언하지만 구현할 필요는 없다. 대신 사용하는 쪽에서 애노테이션에 있는 모든 요소들의 값을 다 넣어줘야 한다. 이때 순서는 상관없다.

@interface BicMacAnnotation {
	int pickle();
    String bread();
    String[] pattys();
    String source();
}

@BicMacAnnotation(pickle = 3, bread = "BicMacBurn",
 source = "BicMacSource", pattys = {"10 : 1 patty", "10 : 1 patty"})
public class CustomAnnotationTest {
	.....
}

특징

  • 애노테이션을 만들 때 값을 지정하지 않으면 사용될 수 있는 기본값을 지정할 수 있다. (null값 제외)
@interface MyAnnotation {
	int count() default 1;
}

@MyAnnotation // @MyAnnotation(count = 1)과 동일한 결과.
public class MyClass {
	.....
}
  • 요소가 하나이고 이름이 value일 때는 이름을 생략할 수 있다.
@interface MyAnnotation {
	String value();
}

@MyAnnotation("test")  // @MyAnnotation(value = "test")과 동일한 결과.
public class MyClass {
	.....
}
  • 요소 타입이 배열인 경우, {} 중괄호를 사용해야 한다.
@interface MyAnnotationArr {
	String[] texts();
}

@MyAnnotation(texts={"test", "test1"}) 
@MyAnnotation(texts="test") // 값이 하나있을 땐 {} 괄호를 작성 X
@MyAnnotation({}) // 값이 없을 땐 {} 괄호가 반드시 있어야 한다. 
public class MyClass {
  ......
}

애노테이션의 조상

java.lang.annotation.Annotation 인터페이스가 모든 애노테이션의 조상이다.

package java.lang.annotation;

/**
 * The common interface extended by all annotation types.  Note that an
 * interface that manually extends this one does <i>not</i> define
 * an annotation type.  Also note that this interface does not itself
 * define an annotation type.
 *
 * More information about annotation types can be found in section 9.6 of
 * <cite>The Java&trade; Language Specification</cite>.
 *
 * The {@link java.lang.reflect.AnnotatedElement} interface discusses
 * compatibility concerns when evolving an annotation type from being
 * non-repeatable to being repeatable.
 *
 * @author  Josh Bloch
 * @since   1.5
 */
public interface Annotation {
    /**
     * Returns true if the specified object represents an annotation
     * that is logically equivalent to this one.  In other words,
     * returns true if the specified object is an instance of the same
     * annotation type as this instance, all of whose members are equal
     * to the corresponding member of this annotation, as defined below:
     * <ul>
     *    <li>Two corresponding primitive typed members whose values are
     *    <tt>x</tt> and <tt>y</tt> are considered equal if <tt>x == y</tt>,
     *    unless their type is <tt>float</tt> or <tt>double</tt>.
     *
     *    <li>Two corresponding <tt>float</tt> members whose values
     *    are <tt>x</tt> and <tt>y</tt> are considered equal if
     *    <tt>Float.valueOf(x).equals(Float.valueOf(y))</tt>.
     *    (Unlike the <tt>==</tt> operator, NaN is considered equal
     *    to itself, and <tt>0.0f</tt> unequal to <tt>-0.0f</tt>.)
     *
     *    <li>Two corresponding <tt>double</tt> members whose values
     *    are <tt>x</tt> and <tt>y</tt> are considered equal if
     *    <tt>Double.valueOf(x).equals(Double.valueOf(y))</tt>.
     *    (Unlike the <tt>==</tt> operator, NaN is considered equal
     *    to itself, and <tt>0.0</tt> unequal to <tt>-0.0</tt>.)
     *
     *    <li>Two corresponding <tt>String</tt>, <tt>Class</tt>, enum, or
     *    annotation typed members whose values are <tt>x</tt> and <tt>y</tt>
     *    are considered equal if <tt>x.equals(y)</tt>.  (Note that this
     *    definition is recursive for annotation typed members.)
     *
     *    <li>Two corresponding array typed members <tt>x</tt> and <tt>y</tt>
     *    are considered equal if <tt>Arrays.equals(x, y)</tt>, for the
     *    appropriate overloading of {@link java.util.Arrays#equals}.
     * </ul>
     *
     * @return true if the specified object represents an annotation
     *     that is logically equivalent to this one, otherwise false
     */
    boolean equals(Object obj);

    /**
     * Returns the hash code of this annotation, as defined below:
     *
     * <p>The hash code of an annotation is the sum of the hash codes
     * of its members (including those with default values), as defined
     * below:
     *
     * The hash code of an annotation member is (127 times the hash code
     * of the member-name as computed by {@link String#hashCode()}) XOR
     * the hash code of the member-value, as defined below:
     *
     * <p>The hash code of a member-value depends on its type:
     * <ul>
     * <li>The hash code of a primitive value <tt><i>v</i></tt> is equal to
     *     <tt><i>WrapperType</i>.valueOf(<i>v</i>).hashCode()</tt>, where
     *     <tt><i>WrapperType</i></tt> is the wrapper type corresponding
     *     to the primitive type of <tt><i>v</i></tt> ({@link Byte},
     *     {@link Character}, {@link Double}, {@link Float}, {@link Integer},
     *     {@link Long}, {@link Short}, or {@link Boolean}).
     *
     * <li>The hash code of a string, enum, class, or annotation member-value
     I     <tt><i>v</i></tt> is computed as by calling
     *     <tt><i>v</i>.hashCode()</tt>.  (In the case of annotation
     *     member values, this is a recursive definition.)
     *
     * <li>The hash code of an array member-value is computed by calling
     *     the appropriate overloading of
     *     {@link java.util.Arrays#hashCode(long[]) Arrays.hashCode}
     *     on the value.  (There is one overloading for each primitive
     *     type, and one for object reference types.)
     * </ul>
     *
     * @return the hash code of this annotation
     */
    int hashCode();

    /**
     * Returns a string representation of this annotation.  The details
     * of the representation are implementation-dependent, but the following
     * may be regarded as typical:
     * <pre>
     *   &#064;com.acme.util.Name(first=Alfred, middle=E., last=Neuman)
     * </pre>
     *
     * @return a string representation of this annotation
     */
    String toString();

    /**
     * Returns the annotation type of this annotation.
     * @return the annotation type of this annotation
     */
    Class<? extends Annotation> annotationType(); // 
}

Annotation 인터페이스는 모두 추상메서드로 이루어져 있으며, 갯수도 4개 밖에 없다.

애노테이션 요소의 규칙

애노테이션의 요소와 관련되서 몇가지 규칙이 있다. (굳이 외울 필요는 없지만 알아두면 좋을 법 하다.)

  • 애노테이션의 요소로 선언할 때 기본형, String, enum, 애노테이션, class 타입만 허용할 수 있다.
  • 괄호() 안에 매개변수를 선언할 수 없다.
  • 예외를 선언할 수 없다.
  • 요소를 타입 매개변수로 정의할 수 없다.
@interface WrongAnnotation {
	int id = 100; // 인터페이스와 동일하게 static final이 숨겨져있기 때문에 이는 상수로써 사용 가능
    String method(int i, int j); // 매개변수 X
    String method2() throws Exception; // 예외 선언 X
    ArrayList<T> list(); // 요소의 타입을 매개변수로 정의할 수 없다.
}

Annotaiton의 분류

애노테이션은 메타데이터의 저장을 위해 클래스처럼 멤버를 가질 수 있는데, 이때 멤버의 갯수에 따라 Marker Annotation, Single Value Annotaiton, Full Annotation으로 분류할 수 있다.

✔️ Marker Annotation

멤버 변수가 없으며, 단순히 표식으로써 사용되는 애노테이션이다. 컴파일러에게 어떤 의미를 전달한다. 대표적으로는 @Override 애노테이션이 Marker Annotation의 예시이다.

@Override
public void method() ....

✔️ Single Value Annotation

멤버로 단일 변수만을 갖는 애노테이션이다. 단일 변수밖에 없기 때문에 (값)만을 명시하여 데이터를 전달할 수 있다.

ex : @Resource("mappedName = PERSON")

✔️ Full Annotation

멤버를 둘 이상 갖는 애노테이션으로, 데이터를 (값 = 쌍)의 형태로 전달된다.

ex : @FullAnnotation(var1 = "data value1", var2 = "data value2", var3 = "data value3")

커스텀 애노테이션을 위한 메타 애노테이션

커스텀 애노테이션을 작성시 애노테이션을 설명하기 위한 메타 애노테이션이 있다. 메타 애노테이션은 5개가 존재한다.

  • @Documented : javadoc 및 기타 문서툴에 의해 문서화될 때, 해당 애노테이션이 문서에 표기된다.
  • @Target : 애노테이션 적용 가능한 대상을 지정할 수 있다.
  • @Retention : 애노테이션 유지 범위를 지정할 수 있다.
  • @Inherited : 자식클래스에서 부모클래스에 선언된 애노테이션을 상속받을 수 있다.
  • @Repeatable : 동일한 위치에 같은 애노테이션을 여러 개 선언할 수 있다.

@Documented

javadoc으로 작성한 문서를 포함시키려면 @Documented를 붙인다. 여기서 javadoc을 작성한 문서는 /**로 시작해서 */로 끝난다.

package java.lang.annotation;

/**
 * Indicates that annotations with a type are to be documented by javadoc
 * and similar tools by default.  This type should be used to annotate the
 * declarations of types whose annotations affect the use of annotated
 * elements by their clients.  If a type declaration is annotated with
 * Documented, its annotations become part of the public API
 * of the annotated elements.
 *
 * @author  Joshua Bloch
 * @since 1.5
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

@Target

애노테이션을 적용할 수 있는 대상(위치)을 나타내는 애노테이션이다. 만약 Target에 선언된 대상과 다른 대상에 이 애노테이션을 적용할 경우 컴파일 에러가 발생한다. 타입으로 enum인 ElementType[]을 받는다.

  • Type : class, interface, annotation, enum에만 적용 가능.
  • FIELD : 필드, enum 상수에만 적용 가능.
  • METHOD: 메서드에만 적용 가능.
  • PARAMETER : 파라미터에만 적용 가능.
  • CONSTRUCTOR : 생성자에만 적용 가능.
  • LOCAL_VARIABLE : 지역변수에만 적용 가능.
  • ANNOTATION_TYPE : 애노테이션에만 적용 가능.
  • PACKAGE: 패키지에만 적용 가능.
  • TYPE_PARAMETER : 자바 8부터 추가되었으며, 타입 파라미터(T, E 와 같은)에만 적용 가능.
  • TYPE_USE : TYPE_PARAMETER와 동일하게 자바 8부터 추가되었으며, JLS의 15가지 타입과 타입 파라미터에 적용 가능.

    자바 언어 명세서 (Java Language Specification, JLS)
    자바 언어의 명세서를 뜻하며, 자바 프로그래밍 언어를 위한 문법과 정상적인/비정상적인 규칙들을 보여준다. 그리고 정상적인 프로그램을 실행하기 위한 프로그램 방법들도 보여준다.

@Target(ElementType.FIELD) // 필드에만 @MyTarget Annotation이 적용 가능하게끔 선언
public @interface MyTarget {...}

@Retention

애노테이션이 어느 시점까지 유지되는지를 나타낼 수 있다. enum RetentionPolicy에 3가지 정책이 있다.

  • SOURCE : 컴파일 시점에 컴파일러에 의해 제거된다. 즉, java 파일 내에서만 적용되고, class파일 형태에선 적용되지 않는다.
  • CLASS : SOURCE의 범위뿐만 아니라, class파일까지 적용된다
  • RUNTIME : SOURCE, CLASS 범위뿐만 아니라 JVM에서 실행될 때도 적용돼 리플렉션으로 애노테이션을 조회할 수 있다.

@Inherited

해당 애노테이션을 적용하면 부모클래스에 선언된 애노테이션이 자식클래스에 상속된다. getAnnotation()을 호출하여 각 클래스별로 적용된 애노테이션을 확인할 수 있다. 실제로 어떻게 작동하는지 보기 위해 @Inherited를 사용하지 않는 애노테이션과, 사용하는 애노테이션을 만든다.

@Retention(RetentionPolicy.RUNTIME)
@interface MyInherited {
	....
}

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface ChildAnnotation {
	.....
}

Parent, Child, GrandChild 클래스를 만든다.

@MyInherited
class Parant{}

@ChildAnnotation
class Child extends Parent{}

class GrandChild extends Child{}

실제 코드를 통해 해당 애노테이션들이 어디에 적용됐는지 확인해보면 @Inherited 애노테이션을 사용하지 않은 @MyInherited 애노테이션은 자기 자신의 정보를 출력하고, 클래스 상속관계에서 조차 @MyInherited가 적용되지 않는다는 것을 확인할 수 있다. 하지만 @Inherited를 사용한 @ChildAnnotation을 조회해보면 Child, GrandChild에 애노테이션만 출력된다. GrandChild는 선언된 애노테이션은 없지만, 상속받은 Child 애노테이션이 있기 때문에 동일하게 적용된다.

public class InheritedAnnotationTest {
	public static void main(String[]args) {
		System.out.println("inherited 애노테이션을 사용 전");
        System.out.println("Before inherited A : " 
			+ new Parent().getClass().getAnnotation(MyInherited.class));
        System.out.println("Before inherited B : "
			+ new Child().getClass().getAnnotation(MyInherited.class));
        System.out.println("Before inherited C : "
			+ new GrandChild().getClass().getAnnotation(MyInherited.class));

        System.out.println("inherited 애노테이션 사용 후");
        System.out.println("After inherited A : " 
			+ new Parent().getClass().getAnnotation(ChildAnnotation.class));
        System.out.println("After inherited B : "	
        	+ new Child().getClass().getAnnotation(ChildAnnotation.class));
        System.out.println("After inherited C : " 
			+ new GrandChild().getClass().getAnnotation(ChildAnnotation.class));
    }
}

Output
inherited 애노테이션 사용 전
Before inherited A : @annotationtest.MyInherited()
Before inherited B : null
Before inherited C : null

inherited 애노테이션 사용 후
After inherited A : null
After inherited B : @annotationtest.ChildAnnotation()
After inherited C : @annotationtest.ChildAnnotation()

@Repeatable

동일한 애노테이션을 여러개 선언할 경우 에러가 발생하지만, @Repeatable을 적용하면 여러개의 동일한 애노테이션을 선언할 수 있다.

@Repeatable(ToDos.class) // @ToDo 애노테이션을 여러번 반복해서 쓸 수 있게한다.
@interface ToDo {
	String value();
}

일반적인 애노테이션과 달리 같은 이름의 애노테이션 여러개가 하나의 대상에 적용될 수 있기 때문에, 이 애노테이션들을 하나로 묶어서 다룰 수 있는 애노테이션도 추가로 정의해야한다 → 마치 애노테이션들을 담을 배열을 만든다고 생각하면 이해가 쉽다.

// @ToDo 애노테이션의 @Repeatable 애노테이션을 위한 ToDos 애노테이션
@interface ToDos {
	ToDo[] value();
}

위와 같이 ToDo 애노테이션을 담을 컨테이너 ToDos를 만들어야 한다. 또한 ToDo 애노테이션 배열타입의 요소를 선언해줘야 하고 컨테이너 애노테이션 안에 요소의 이름이 반드시 value이어야 한다.

애노테이션 프로세서

"Annotation Processing" is a hook into the compile process of the java compiler, to analyse the source code for user defined annotations and handle then (by producing compiler errors, compiler warning, emitting source code, byte code ...)
애노테이션 프로세싱은 자바 컴파일러의 컴파일 단계에서, 유저가 정의한 애노테이션의 소스코드를 분석하고 처리하기 위해 사용하는 훅이다. 컴파일 에러나 컴파일경고를 만들어 내거나, 소스코드(.java)와 바이트코드(.class)를 내보내기도 한다.
출처 : https://stackoverflow.com/questions/2146104/what-is-annotation-processing-in-java [stackoverflow]

여기서 나오는 이란? 후킹(Hooking) 이라고 불리며 이미 작성되어 있는 코드의 특정 지점을 가로채서 동작 방식에 변화를 주는 일체의 기술이라고 의미를 지닌 용어이다.

  • 소스코드 레벨에서 소스코드에 붙어있는 애노테이션 정보를 읽어와 컴파일러가 컴파일 중에 새로운 소스코드를 생성하거나 기존의 코드 변경을 가능하게 한다. (코드 변경은 비추.)
  • 클래스 즉, 바이트 코드도 생성 가능.
  • 소스코드와 별개의 리소스도 생성 가능.

✔️ 대표적인 예제

  • 롬복(Lombok)
    • @Getter, @Setter, @Builder 등
  • AutoService : java.util.ServiceLoader용 파일 생성 유틸리티
    • 리소스 파일을 만들어준다.
    • META-INF 밑의 service 밑에 ServiceLoader용 레지스트리 파일을 만들어준다.
  • Override
    • 컴파일러가 오버라이딩하는 메서드가 잘못된 대상임을 체크해주는것도 애노테이션 프로세서

애노테이션 프로세서의 장점

  • 컴파일 시점에 조작하기 때문에 런타임 비용이 제로!

애노테이션 프로세서의 단점

  • 기존의 코드를 고치는 방법 → 현재로써는 public한 api가 없다.

끝 !

한끼 5천원인 세상 지났다. . 밥 값은 해야지. . .


[참고]
https://parkadd.tistory.com/54
https://velog.io/@ljs0429777/12주차-과제-애노테이션

profile
⋆。゚★⋆⁺₊⋆ ゚☾ ゚。⋆ ☁︎。₊⋆

1개의 댓글

comment-user-thumbnail
2023년 8월 10일

좋은 정보 감사합니다

답글 달기