Language/Java

[자바 스터디 12주차] 어노테이션

소프 2021. 2. 20.

목표

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

학습할 것

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

Annotation

자바를 개발한 사람들은 소스코드에 대한 문서를 따로 만들기보다 소스코드와 문서를 하나의 파일로 관리하는 것이 낫다고 생각했다.

그래서 소스코드의 주석 '/** ~ */'에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML 문서를 생성해내는 프로그램(javadoc.exe)를 만들어 사용했다.

 

프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것이 바로 애너테이션이다.

애너테이션은 주석(comment)처럼 프로그래밍 언어에 영향을 미치지 않으면서도 다른 프로그램에게 유용한 정보를 제공할 수 있다는 장점이 있다.

애너테이션(annotation)의 뜻은 주석, 주해, 메모이다.
package com.ssonsh.study.annotationstudy;

public @interface SsonAnnotation {
}

→ 컴파일된 결과 SsonAnnotation.class

Picked up JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true
Compiled from "SsonAnnotation.java"
public interface com.ssonsh.study.annotationstudy.SsonAnnotation extends java.lang.annotation.Annotation {
}

컴파일 시점에 extends 한적 없는 java.lang.annotation.Annotation이 extends 되어 있다.

 

 

 

 

 

(Java Doc) - 메타데이터의 한 형태인 주석은 프로그램 내에 일부가 아닌 프로그램에 대한 데이터를 제공한다.

  • @로 시작하며
  • 애노테이션으로 인하여 컴파일된 프로그램의 동작을 변경하지 않는다.
  • 애노테이션을 사용하면 인스턴스 변수, 생성자, 메서드, 클래스 등과 관련된 메타데이터를 연결할 수 있다.
  • 애노테이션은 컴파일러에 의해 프로그램이 처리되는 방식을 바꿀 수 있기 때문에 단순한 주석이 아니다.

애노테이션의 사용 예

@Test //밑에 선언된 해당 메소드가 테스트 대상임을 프로그램에게 알리는 역할
public void method() {
	//...
}

 

왜 애노테이션이 생겼을까?

탄생 이유는 프로그래머에게 그들의 코드에 대한 메타데이터를 자신의 코드에 직접적으로 기술할 수 있는 것을 제공하기 위함이다. 어노테이션이 만들어지기전에 프로그래머가 자신의 코드를 기술하는 방법은 transient 키워드를 사용한다던가, 주석을 통하여, 인터페이스를 이용 등 중구난방이었다. 그리고 여러타입의 어플리케이션에서 코드를 기술하는 메커니즘은 주로 XML이 사용되어졌는데 이것은 그리 좋은 방법은 아닌게 코드와 XML 사이에 디커플링이 발생되고 이것은 어플리케이션에서 유지보수하기 힘들다.

출처 : https://hamait.tistory.com/315 [HAMA 블로그]

※ 메타데이터

데이터에 대한 데이터. 즉, 다른 데이터를 설명해주는 데이터.

예를 들어, 카메라로 사진을 찍었을 때 사진에 대한 [촬영한 시간, 장소, 카메라 모델명 등] 것들이 메타데이터라고 할 수 있다.

 

Annotation의 용도

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

컴파일러를 위한 정보 -- 애노테이션을 사용하여 컴파일러에게 오류를 감지하거나 경고를 차단할 수 있게끔 해준다.

컴파일 시간 및 배포 시간 처리 -- 소프트웨어 도구는 주석 정보를 처리하여 코드, XML 파일 등을 생성할 수 있게 해준다.

런타임 처리 -- 일부 주석은 런타임에 검사할 수 있다.

 

 강의중 point

애노테이션은 코드에 넣는 주석이다.

완전히 주석같지 않지만, 주석과 비슷한 류의 장치다.

 

주석이기 때문에, 실행되는 코드라고 생각하면 안된다.

마치 애노테이션은 기능을 가지고 있는 것이라 생각할 수 있으나,

애노테이션은 마크, 표시 해놓는 주석이다.

애노테이션에 다이나믹하게 실행되는 코드는 들어가지 않는다.

즉, 런타임중에 알아내야 하는 값은 못들어 간다.

  • 컴파일러 수준에서 해석이 되거나,
  • 완전히 정적이어야 한다.
  • 동적으로 런타임중에 바뀌어야 하는 것들은 애노테이션에 사용할 수 없다.
@RestController
public HelloController{
	
	private static final String hello = "hello";

	@GetMapping(hello)
	public String hello(){
		return "hello";
	}
	
}
  • 위와 같이 hello 변수는 static final한 정적 변수임으로 GetMapping 애노테이션에 사용할 수 있다.
  • but. hello가 동적인 변수라면?
  • private String hello = "hello"; -> @GetMapping(hello)
  • 컴파일 에러 발생

 

 

 

 

Annotation 정의

커스텀 애노테이션이라고 부르며 이와 관련된 소스는 java.lang.annotation 패키지에 속해있다.

커스텀 애노테이션을 정의하려면 interface라는 키워드를 쓰고 그 앞에 @를 붙여주면 애노테이션이 정의된다. 공백을 넣어 @interface도 가능하다고는 하지만, 표준 스타일을 맞추기 위해서 공백없이 붙여주는 게 좋은 방법이다.

 

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

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

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

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

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

}

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

}

 

특징

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

@MyAnnotation // @MyAnnotatio(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 인터페이스가 모든 애노테이션의 조상이다.

 

애노테이션 요소의 규칙

  • 에노테이션의 요소로 선언할 때 기본형, 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(); //요소의 타입을 매개변수로 정의할 수 없다.
}

 

※ 자바 리플렉션

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

 

 

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

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

 

Annotation의 분류

 

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

 

Marker Annotation

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

 

Single Value Annotations

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

 

Full Annotations

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

 

Standard Annotations (표준 애노테이션)

 

자바에서 몇가지 애노테이션을 기본적으로 제공해준다. 모두 java.lang 패키지에 속하며, 총 5개의 애노테이션이 있다.

 

@Override

오버라이딩을 올바르게 했는지 컴파일러가 체크하게 해주는 애노테이션이다. 개발자가 오버라이딩할 때 메소드 이름을 오타내는 경우가 종종 있다.

class Drink {
    String makeDrink() {
        return "음료 만드는 중 입니다.";
    }
}

class Coke extends Drink {
    // 부모 클래스의 method()를 오버라이딩할려고 시도했으나 오타가 발생한 경우
    // 결국 메소드를 2개를 만든 결과
    String makDrink() {
        return "콜라를 만드는 중 입니다.";
    }
}

오버라이딩할 때는 메소드 선언부 앞에 @Override를 붙이면, 오타가 나더라도 IDE상에서 에러를 쉽게 확인할 수 있다.

 

 

@Deprecated

앞으로 사용하지 않을 것을 권장하는 필드나 메소드에 붙이고, 사용하면 위험하거나 해당 코드보다 개선된 코드가 존재하기 때문에 개발자에게 사용하지 말아야 하는 것을 알리기 위해 사용한다. @Deprecated의 사용 예를 확인해보기 위해 Date 클래스의 메소드를 몇 개 확인해봤다.

@Deprecated
    public int getDate() {
        return normalize().getDayOfMonth();
    }

    /**
     * Sets the day of the month of this <tt>Date</tt> object to the
     * specified value. This <tt>Date</tt> object is modified so that
     * it represents a point in time within the specified day of the
     * month, with the year, month, hour, minute, and second the same
     * as before, as interpreted in the local time zone. If the date
     * was April 30, for example, and the date is set to 31, then it
     * will be treated as if it were on May 1, because April has only
     * 30 days.
     *
     * @param   date   the day of the month value between 1-31.
     * @see     java.util.Calendar
     * @deprecated As of JDK version 1.1,
     * replaced by <code>Calendar.set(Calendar.DAY_OF_MONTH, int date)</code>.
     */
    @Deprecated
    public void setDate(int date) {
        getCalendarDate().setDayOfMonth(date);
    }

실제로 getDate(), setDate() 메소드에는 @Deprecated 라는 애노테이션이 붙어있다.

 

삭제하지 않고 @Deprecated를 붙이는 가장 큰 이유는 하위버전과의 호환성 때문이다. 예전에 만들었던 프로그램들이 최신 버전으로 업데이트 될 때마다 재기능을 하지 못할 경우가 있기 때문에 이를 방지하기 위해 삭제하지 않는다고 한다.

만약 @Deprecated가 선언된 메소드를 호출한다면

'getDate()'는 deprecated가 되었고 'Date.getDate()'의 결과는 무시된다는 결과를 얻는다.

 

@SafeVarargs(JDK 1.7)

Java 7이상에서 사용가능하고 제네릭같은 가변인자 매개변수 사용시 경고를 무시한다

(가변인자란 String... values와 같은 형태를 가변인자라고 한다.)

public class SafeVarargsTest {

    @SafeVarargs
    static void test(List<String>... stringLists) {
      
    }
}

 

@FuntionalInterface

함수형 인터페이스에는 하나의 추상메소드만 가져야 한다는 제약이 있는데, @FunctionalInterface를 붙이면 컴파일러가 올바르게 작성했는지 알려주는 역할을 한다. 즉, 이것도 컴파일러를 위한 애노테이션이다.

 

Runnable도 함수형 인터페이스이다.

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

 

만약 @FunctionalInterface 애노테이션을 사용했는데도 하나를 초과하는 메소드를 선언하면 어떤 에러가 발생하는지 확인하자.

@FunctionalInterface
interface TestInterface {
    void method1();
    void method2();
}

@Deprecated와는 다르게 컴파일하기도 전에 이미 문제가 있다고 알려주기 때문에 따로 더 분석할 필요도 없다.

 

@SuppressWarnings

이미 인지하고 있는 컴파일러의 경고메시지가 나타나지 않게 제거해줄때 사용한다. 쉽게 말하면 경고를 안뜨게 해준다는 의미이다.

 

@Deprecated를 공부하면서 사용했던 예제들을 다시 보면 getDate() 메소드는 deprecated되었기 때문에 사용하지 말라는 경고를 확인했다. 이 상태에서 개발자가 이미 deprecated된 상황을 인지하고 있고, 경고 메시지를 확인하고 싶지 않을 때, 이를 호출하는 선언부에 @SuppressWarnings 애노테이션을 사용하면 된다.

 

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

커스텀 애노테이션을 작성시 애노테이션을 설명하기 위한 메타 애노테이션이 있다. 

 

  • @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 {
}

위의 말이 잘 안와닿을수도 있는데, 다시 말하면 직접 자바docs를 만들 수 있다는 뜻이다.

 

인텔리제이서 만드는 법

 

Locale : ko_KR

Other command line arguments : -encoding UTF-8 -charset UTF-8 -docencoding UTF-8 (한글깨짐 방지)

output directory 경로 입력

 

0.코드

public class Korea implements Great{

    @Override
    @Make
    public String country() {
        return "한국";
    }
}

 

1. @Documented를 붙히지 않은 것

 

2. @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 {
}
public class TargetAnnotationTest {

    @MyTarget String field_variable = "field에 선언된 애노테이션 입니다.";

    public static void main(String[] args) {
        System.out.println(new TargetAnnotationTest().field_variable);
    }

}

 

Target에 선언된 위치와 다른곳에서 애노테이션을 사용할 경우

public class TargetAnnotationTest {

    @MyTarget String field_variable = "field에 선언된 애노테이션 입니다.";

    public static void main(String[] args) {
        @MyTarget String field_variable = "field에 선언된 애노테이션 입니다.";
        System.out.println(new TargetAnnotationTest().field_variable);
    }

}

실제로 @MyTarget 애노테이션을 붙힌 변수를 메인 메소드에서 위치하면 컴파일 에러가 발생한다.

 

이런 경우 ElementType[] 중 하나인 LOCAL_VARIABLE을 사용하면 이 문제를 해결할 수 있다.

public class TargetAnnotationTest {

    @MyTarget String field_variable = "field에 선언된 애노테이션 입니다.";


    public static void main(String[] args) {
        @MyTarget String local_variable = "ElementType에 LOCAL_VARIABLE enum을 추가한 애노테이션 입니다.";
        System.out.println(new TargetAnnotationTest().field_variable);
        System.out.println(local_variable);
    }
}

 

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

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


@MyAnnotation                // 적용 대상이 TYPE 인 경우
class MyClass{

	@MyAnnotation              // 적용 대상이 FIELD 인 경우
	int i;

	@MyAnnotation              // 적용 대상이 TYPE_USE 인 경우
	MyClass myClass;

}

 

@Retention

애노테이션이 어느 시점까지 유지되는지를 나타낼 수 있다. enum RetentionPolicy에 3가지의 정책이 있다. @Retention 애노테이션을 생략한다면 RetentionPolicy.CLASS가 적용된다.

 

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

※ 강의중 Point

SOURCE -> CLASS -> RUNTIME

  • SOURCE는 소스코드에만 유지하겠다.
  • 컴파일 시에만 사용하겠다는 의미이다.
  • 컴파일 하고 애노테이션은 없어진다.
    • 필요없으니까, 바이트 코드에 남아있지도 않다.
  • CLASS
    • 애노테이션에 대한 정보를 클래스 파일까지, 즉 바이트 코드에 남겨 두겠다.
    • 클래스 정보를 읽어들이는 방법(바이트 코드를 읽어들이는)을 바탕으로 애노테이션 정보를 읽어와서 처리할 수 있다.
      • 예) BYTE BUDDY, ASM 활용
    • 바이트코드엔 남아 있지만, 이 클래스 파일을 JVM이 실행할 때 클래스에 대한 정보를 클래스로더가 읽어서 메모리에 적재하게 되고, 이후 사용 시점에 메모리에서 읽어올 때 애노테이션 정보를 제외하고 읽어옴
  • RUNTIME
    • 위 CLASS와 동일하지만, 메모리에 적재된 클래스 정보를 읽어올 때 애노테이션 정보를 그대로 포함하는 것이다.

바이트 코드에서 읽어오는게 빠른가?

  • RetentionPolicy를 CLASS로 한 이후, 바이트코드를 읽어 처리하는 라이브러리를 활용

리플렉션으로 읽어오는게 빠른가?

  • RetentionPolicy를 RUNTIME으로 하여 리플렉션 기술 활용?

-> 모른다. 리플렉션 자체가 부하가 있긴 하다.

-> 바이트 코드의 양에 영향

-> 리플렉션은 메모리에 이미 올라와있는 정보를 읽는 것, 클래스 로더가 읽고 메모리에 적재시킨 후 읽어오는 것

-> 직접 확인해봐야 한다.

 

커스텀하게 만든 애노테이션이 정말로 RUNTIME까지 필요한 정보인가?

RUNTIME까지 사용할 필요가 없다면, CLASS 레벨로 내려가거나 SOURCE 레벨로 내려갈 수도 있을 것이다.

그냥 의례적으로 RUNTIME으로 작성하는 경우가 있었다면? 그 역할을 다시 살펴보고 명확한 Retention Policy를 정의하자.

 

표준 애너테이션 중 '@Override'나 '@SuppressWarnings'처럼

컴파일러가 사용하는 애너테이션은 유저 정책이 'SOURCE'이다.

-> 컴파일러는 직접 작성할 것이 아니면, SOURCE 이상의 유지정책을 가질 필요가 없다.

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

 

유저 정책을 "RUNTIME"으로 한다면

실행 시에 리플렉션을 통해 클래스 파일에 저장된 에너테이션의 정보를 읽어서 처리할 수 있다.

  • Retention 정책은 Runtime으로 정의하고,
  • Target은 TYPE과 FIELD로 정의한다.
package com.ssonsh.study.annotationstudy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface SsonAnnotation {
}

-> @Target이 TYPE과 FILED임으로 클래스에도 애노테이션을 선언할 수 있고,

-> 클래스 내부의 필드에도 애노테이션을 선언할 수 있다.

package com.ssonsh.study.annotationstudy;

@SsonAnnotation
public class SsonClass {

    @SsonAnnotation
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

SsonClass 클래스에 선언된 Annotation을 리플랙션을 이용해 확인할 수 있다.

package com.ssonsh.study.annotationstudy;

import java.lang.reflect.Field;
import java.util.Arrays;

public class App {

    public static void main(String[] args){
		
        Arrays.stream(SsonClass.class.getAnnotations()).forEach(System.out::println);
        
        Field[] declaredFields = SsonClass.class.getDeclaredFields();
        for(Field declaredField : declaredFields) {
        	Arrays.stream(declaredField.getAnnotations()).forEach(System.out::println);
        }
         
    }
}

 

결과

"C:\Program Files\Java\jdk1.8.0_251\bin\java.exe" -agentlib:jdwp=transport=dt_shmem,address=javadebug,suspend=y,server=n -javaagent:C:\Users\ssh1224\.IntelliJIdea2019.3\system\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_251\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\rt.jar;D:\dev\workspace\java-study\out\production\java-study;D:\dev\workspace\java-study\lib\archunit-0.12.0.jar;D:\dev\workspace\java-study\lib\slf4j-api-1.7.25.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar" com.ssonsh.study.annotationstudy.App
Connected to the target VM, address: 'javadebug', transport: 'shared memory'
@com.ssonsh.study.annotationstudy.SsonAnnotation()
@com.ssonsh.study.annotationstudy.SsonAnnotation()
Disconnected from the target VM, address: 'javadebug', transport: 'shared memory'
Picked up JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true

Process finished with exit code 0

 

 

표준 애너테이션 중 '@FunctionallInterface'는 '@Override'처럼 컴파일러가 체크해주는 애너테이션이지만, 실행 시에도사용되므로 유지 정책이 'RUNTIME'으로 되어 있다.

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

 

유저 정책을 "CLASS"로 한다면

컴파일러가 애너테이션의 정보를 클래스 파일에 저장할 수 있게 하지만,

클래스 파일이 JVM에 로딩 될 때는 애너테이션의 정보가 무시되어 실행 시에 애노테이션에 대한 정보를 얻을 수 없다.

-> CLASS가 유지정책의 기본값임에도 불구하고 잘 사용되지 않는 이유

지역 변수에 붙은 애너테이션은 컴파일러만 인식할 수 있으므로, 유지 정책이 RUNTIME인 에너테이션을 지역변수에 붙여도 실행 시에는 인식되지 않는다.

유지 정책을 CLASS로 변경하여 위에서 확인한 예시를 실행해보자.

package com.ssonsh.study.annotationstudy;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.FIELD})
public @interface SsonAnnotation {
}
package com.ssonsh.study.annotationstudy;

import java.lang.reflect.Field;
import java.util.Arrays;

public class App {

    public static void main(String[] args){

        Arrays.stream(SsonClass.class.getAnnotations()).forEach(System.out::println);

        Field[] declaredFields = SsonClass.class.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            Arrays.stream(declaredField.getAnnotations()).forEach(System.out::println);
        }
    }
}
"C:\Program Files\Java\jdk1.8.0_251\bin\java.exe" -agentlib:jdwp=transport=dt_shmem,address=javadebug,suspend=y,server=n -javaagent:C:\Users\ssh1224\.IntelliJIdea2019.3\system\captureAgent\debugger-agent.jar -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk1.8.0_251\jre\lib\charsets.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\deploy.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\javaws.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jce.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfr.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\jsse.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\management-agent.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\plugin.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\resources.jar;C:\Program Files\Java\jdk1.8.0_251\jre\lib\rt.jar;D:\dev\workspace\java-study\out\production\java-study;D:\dev\workspace\java-study\lib\archunit-0.12.0.jar;D:\dev\workspace\java-study\lib\slf4j-api-1.7.25.jar;C:\Program Files\JetBrains\IntelliJ IDEA 2019.3.5\lib\idea_rt.jar" com.ssonsh.study.annotationstudy.App
Connected to the target VM, address: 'javadebug', transport: 'shared memory'
Disconnected from the target VM, address: 'javadebug', transport: 'shared memory'
Picked up JAVA_TOOL_OPTIONS: -Djava.net.preferIPv4Stack=true

Process finished with exit code 0
런타임 시 리플랙션을 이용해 선언된 애노테이션을 가져오고자 하였으나, 아무런 정보가 출력되지 않음을 확인할 수 있다. (유지정책 → CLASS)

 

※ 박종훈님 글 발췌

- SOURCE: 소스코드까지만 유지 (즉, 컴파일 과정에서 어노테이션 정보는 사라짐)
- CLASS: 클래스파일 까지만 유지 (런타임시 유지안됨)
- RUNTIME: 런타임 시점까지 유지 (Reflection API 로 어노테이션 정보 조회 가능)

원할한 이해를 위해.. SOURCE -> RUNTIME -> CLASS 순으로 설명드릴께요.

SOURCE 정책
Getter / Setter 같은 경우 롬복이 바이트 '코드를 생성'해서 넣어주는 것이기 때문에, 굳이 바이트코드에 어노테이션 정보가 들어갈 필요가 없습니다. (왜냐하면 롬복이 코드를 생성해주니까..) 이 점에 대해서는 글에도 잘 설명되어 있네요^^

RUNTIME 정책
런타임에 어노테이션 정보를 뽑아 쓸수 있다는 의미입니다. 즉, Reflection API 등을 사용하여 어노테이션 정보를 알수가 있다는 의미입니다. 스프링 학습을 하시는 것 같아서 스프링을 예로 들자면, @Controller, @Service, @Autowired 등이 있습니다. 스프링이 올라오는 실행 중인 시점에 컴포넌트 스캔이 가능해야하기 때문에 RUNTIME 정책이 필요합니다. (스프링도 내부적으로 Reflection 등을 활용하여 어노테이션이 붙은 놈들만 가져옵니다.)

CLASS 정책
그러면, CLASS 정책은 왜 필요한지 궁금하실거에요. "아니 Reflection 같은걸로 정보를 얻을수도 없으면서 왜 필요한거지?" 말이죠 ㅎㅎ
그런데 이미 '참고사이트'에 첨부해주신 스택오버플로우('retention-of-java-type-checker-annotations') 글에 설명이 되어있습니다. 인텔리제이를 써보셨다면, @NonNull 등이 붙어있는 경우 null 값을 넣게되면 노랑색 경고로 알려줍니다.
"아니 그러면 SOURCE로 해도 될거 같은데?" 싶으실텐데요, 중요한점은 Maven/Gradle로 다운받은 라이브러리와 같이 jar 파일에는 소스가 포함되어있지 않다는 점입니다. class 파일만 포함되어있죠 (Download Sources 옵션은 논외로 할께요)

즉, class 파일만 존재하는 라이브러리 같은 경우에도 타입체커, IDE 부가기능 등을 사용할수 있으려면 CLASS 정책이 필요하게 됩니다. SOURCE 정책으로 사용한다면 컴파일된 라이브러리의 jar 파일에는 어노테이션 정보가 남아있지 않기 때문이죠.
그외에도 클래스로딩시 무언가를 하고 싶은 경우에도 사용될수도 있고요^^

출처: https://jeong-pro.tistory.com/234 [기본기를 쌓는 정아마추어 코딩블로그]

 

@Inherited

해당 어노테이션을 적용하면 부모클래스에 선언된 애노테이션이 자식클래스에 상속된다. 

( '@Inherited'가 붙은 애너테이션을 조상 클래스에 붙이면, 자손 클래스도 이 애너테이션이 붙은 것과 같이 인식된다.)

getAnnotation()를 호출하여 각 클래스별로 적용된 애노테이션을 확인할 수 있다.

 

실제로 어떻게 작동하는지 확인해보기 위해 @Inherited을 사용하지 않은 애노테이션과, @Inherited을 사용한 애노테이션을 만든다.

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

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

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

@MyInherited
class Parent {

}

@ChildAnnotation
class Child extends Parent{

}

class GrandChild extends Child{

}

실제로 코드를 통해 해당 애노테이션들이 어디에 적용됬는지 확인해보면 @Inherited 애노테이션을 사용하지 않은 @MyInherited 애노테이션은 자기 자신의 정보를 출력하고, 클래스 상속 관계에서 조차 @MyInherited 적용되지 않는다는 것을 확인할 수 있다.

 

하지만 @Inherited를 사용한 @ChildAnnotation을 조회해보면, Child, GrandChild에 애노테이션만 출력된다. GrandChild는 선언된 애노테이션이 없지만, 상속받은 Child 애노테이션이 있기 때문에 동일하게 적용된다.

@MyInherited
class Parent {

}

@ChildAnnotation
class Child extends Parent{

}

class GrandChild extends 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("\n");

        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()

 

※ 강의중 Poit

리플렉션의 getDeclaredFields(); 를 하면 클래스에 정의된(선언된) 것들을 가져와서 조작할 수 있다.

public이던, private이던,

Getter와 Setter에 대해 논의를 하며 큰 비용을 소모하는 것이 크게 가치가 없다.

객체지향을 애기하며 Getter, Setter의 정의 관련한 내용으로 애기할 수 있겠지만,

Getter와 Setter가 없더라도 리플렉션을 이용하여 충분히 가져오고 수정할 수 있기 때문이다.

 

 

 

@Repeatable

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

 

동일한 애노테이션을 여러 개 선언할 경우 어떠한 에러가 발생하는지 간단히 테스트 해보면

@Retention(RetentionPolicy.CLASS)
@interface MyRepeatable { } //간단하게 @Retention 애노테이션을 적용한 커스텀 애노테이션 생성
@Hamburger("Cheese")
@Hamburger("BicMac")
public class RepeatableAnnotationTest {
    public static void main(String[] args) {
    }
}

위코드 작성시 아래와 같은 컴파일 에러가 발생한다.

이러한 문제를 해결하기 위해 @Repeatable을 사용하면 위와 같은 문제를 해결할 수 있다.

하지만 이때 중요한 것은 @Repeatable인 @Hamburger를 하나로 묶을 컨테이너 애노테이션을 정의해야 한다.

 

@Repeatable(Hamburgers.class)
@interface Hamburger {
	String value();
}

// 여러 개의 Hamburger 애노테이션을 담을 컨테이너 애너테이션
@interface Hamburgers {
	Hamburger[] value(); // Hamburger 애노테이션 배열 타입의 요소로 선언
    					 // 이름이 반드시 value 이어야 한다.
}
@Hamburger("Cheese")
@Hamburger("BicMac")
public class RepeatableAnnotationTest {
	//TODO...
}

 

애노테이션 프로세서

 

애노테이션 프로세서

애노테이션 프로세서는 소스코드 레벨에서 소스코드에 붙어있는 어노테이션을 읽어서 컴파일러가 컴파일 하는 중에 새로운 소스코드를 생성하거나 기존 소스코드를 바꿀 수 있다.

 

또는, 클래스(바이트 코드)도 생성할 수 있고 별개의 리소스 파일을 생성할 수 있는 강력한 기능이다.

 

애노테이션 프로세서 사용 예

  • 롬복(기존코드를 변경)
  • AutoService(리소스 파일을 생성)
    • java.util.ServiceLoader 용 파일 생성 유틸리티
  • @Override

애노테이션 프로세서 장점

  • 바이트코드에 대한 조작은 런타임에 발생되는 조작임으로 런타임에 대한 비용이 발생
  • but. 애노테이션 프로세서는 애플리케이션을 구동하는 런타임 시점이 아니라,
  • 컴파일 시점에 조작하여 사용함으로 런타임에 대한 비용이 제로가 된다.

단점은 기존의 코드를 고치는 방법은 현재로써는 public한 API가 없다.

롬복 같은 경우, 기존 코드를 변경하는 방법이지만 public한 API를 이용하는 것이 아님으로 해킹이라고 할 수도 있다.

 

롬복 동작원리

  • 컴파일 시점에 "애노테이션 프로세서"를 사용하여 (자바가 제공하는 애노테이션 프로세서)
  • 소스코드의 AST(Abstract Syntax Tree)를 조작한다.

 

AST에 대한 참고 사이트

http://javaparser.org/inspecting-an-ast/

 

javax.annotation.processing || Interfaec Processor

=> 소스코드의 AST를 원래는 참조만 할 수 있다. //수정하지 못한다. 그리고 하면 안된다

=> 그러나 수정이 됬음을 알 수 있다.(컴파일 이후 바이트코드 확인)

=> 참조만 해야 하는 것을 내부 클래스를 사용하여 기존 코드를 조작하는 것임으로 "해킹"이라고 애기하기도 한다.

 

논란 거리

  • 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 기존 소스 코드를 조작한다.
  • 특히 이클립스의 경우에는 Java Agent를 사용하여 컴파일러 클래스까지 조작하여 사용한다.
  • 해당 클래스들 역시 공개된 API가 아니다보니 버전 호환성에 문제가 생길 수도 있고 언제라도 그런 문제가 발생해도 이상하지 않다.
  • 그럼에도 불구하고 엄청난 편리함 때문에 널리 쓰이고 있으며, 대안이 몇가지 있지만 롬복의 모든 기능과 편의성을 대체하지 못하는 상황이다.
    • AutoValue
    • Immutables

 

https://www.notion.so/e0ec4fb75e1044fcbbf398fbb4480bc8

 

애노테이션 프로세서 실습

Processor 인터페이스

www.notion.so

 

 

※ 강의 Point : 자바의 ServiceLoader

-> 스프링부트에서도 활용하고 있는 기능임으로 기본적 개념을 알고가는 것이 중요하다.

-> 스프링부트 이전에 자바 공부를 해야하는 이유

-> 스프링부트의 authoconfigure

  • autoconfigure -> META-INF -> spring.factories

나는 인터페이스만 제공하지만,

public interface HelloService{
	String hello();
}

이 인터페이스의 구현체는 누군지 모른다.

이 구현체를 내가 지정하지 않고, Jar 파일만 바꿔끼면 동작하도록 만들 수 있다.

-> 이것이 Service Loader이다.

docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html

 

Project : hello-service

//me.ssonsh.hello-service.HelloService



public interface HelloService{
	String hello();
}

→ maven install → jar 파일 생성

 

 

Project : my-hello

위 hello-service의 의존성 주입

//me.ssonsh.my-hello.MyHello


public class MyHello implements HelloService{
	@Override
	public String hello(){
		return "홧팅!";
	}	
}
  • 패키징 전 서비스 로더 매커니즘을 사용하기 위해서
  • resources 하위에 META-INF/services 디렉토리 생성
    • 파일 생성 -> 인터페이스의 풀패키지 경로로 파일을 생성한다.
      • 내용에는 구현체의 풀패키지 경로를 작성한다.
파일명 : resources/META-INF/services/me.ssonsh.hello-service.HelloService
파일 내용 : me.ssonsh.my-hello.MyHello

→ maven install → jar 파일 생성

 

Project : app

위 my-hello 의존성 주입

my-hello는 hello-service 의존성을 주입하고 있음.

public static void main(String[] args){
	ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
    for(HelloService helloService : loader) {
    	System.out.println(helloService.hello()); 
	}
}
  • HelloService 인터페이스의 구현체가 뭐가 있는지 모르는 상황에서 위와 같이 사용할 수 있다.
  • loader는 얼마나 있을지도 모름으로 위와 같이 ServiceLoader<T> loader 형태로 반환된다.
  • loop를 돌면서 위 예제에선 HelloService 인터페이스 구현체가 모두 처리된다.

참조

velog.io/@ljs0429777/12%EC%A3%BC%EC%B0%A8-%EA%B3%BC%EC%A0%9C-%EC%95%A0%EB%85%B8%ED%85%8C%EC%9D%B4%EC%85%98

b-programmer.tistory.com/264

www.notion.so/37d183f38389426d9700453f00253532

https://jeong-pro.tistory.com/234

 

댓글