Retention

Retention

이 문서에서는 Retention 설정 옵션에 따른 차이를 다룬다.

@interface Retention

Library에서 Retention 코드를 찾아보면 다음과 같다. 편의상 주석은 제거하였다.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {
    RetentionPolicy value();
}
  • @Documented - 작성된 Annotation이 Javadoc에 문서화 됨을 표시

  • @Target - 이 Annotation이 다른 Annotation type에 작성될 수 있음을 표시

Annotation 내부에는 RetentionPolicy라는 Enum 값을 저장할 수 있는 value 속성이 존재한다.

enum RetentionPolicy

RetentionPolicy의 코드는 다음과 같다.

public enum RetentionPolicy {
    /**
     * Annotations are to be discarded by the compiler.
     */
    SOURCE,

    /**
     * Annotations are to be recorded in the class file by the compiler
     * but need not be retained by the VM at run time.  This is the default
     * behavior.
     */
    CLASS,

    /**
     * Annotations are to be recorded in the class file by the compiler and
     * retained by the VM at run time, so they may be read reflectively.
     *
     * @see java.lang.reflect.AnnotatedElement
     */
    RUNTIME
}

설정 가능한 RetentionPolicy 값은 다음과 같다.

  • SOURCE - 컴파일러에 의해 삭제되는 Annotation

  • CLASS - 컴파일러에 의해 클래스파일에 기록되지만 런타임 시 VM에서는 관리하지 않음

  • RUNTIME - 컴파일러에 의해 클래스파일에 기록되고 VM에서 관리하여 실행 중 읽을 수 있음

RetentionPolicy.SOURCE

RetentionPolicy.SOURCE로 설정된 Annotation은 컴파일과 동시에 사라진다. 따라서 다음 용도로 사용이 가능하다.

  1. 코드상에 단순한 표식(Marking) 생성

  2. 컴파일 시 특정 코드로 치환되도록 자동화 구현

코드상에 단순한 표식 설정

예를 들어 테스트가 진행중인 메소드에 다음과 같이 표시를 남겨 정보를 줄 수 있다.

먼저 Annotation을 다음과 같이 생성한다.

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.METHOD)
public @interface TestInProgress {
	String tester();
	String last() default "today";
}

그리고 특정 메소드에 Annotation을 작성하고 옵션으로 정보를 설정한다.

public class TestFunction {
	@TestInProgress(tester = "Hacademy")
	public static void print() {
		System.out.println("Hello world!");
	}
}

생성된 class 파일을 찾아서 디컴파일하면 다음과 같이 표시된다.

// 
// Decompiled by Procyon v0.5.36
// 

package com.hacademy.annotation;

public class TestFunction
{
    public static void print() {
        System.out.println("Hello world!");
    }
}

@TestInProgress Annotation이 사라진 것을 확인할 수 있다.

컴파일 시 특정 코드로 치환되도록 자동화 구현

대표적인 라이브러리로 Lombok이 있다. Lombok에서 사용하는 @Getter @Setter @ToString @EqualsAndHashCode 등이 해당된다.

Lombok을 이용하여 다음과 같이 클래스를 하나 만든다.

Book.java
package com.hacademy.annotation;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data @NoArgsConstructor @AllArgsConstructor @Builder
public class Book {
	private String name;
	private int price;
}

이 클래스에는 Lombok에서 제공하는 Annotation 4종류를 사용하였다.

  • @Data - Getter/Setter, ToString, EqualsAndHashCode 생성

  • @NoArgsConstructor - 기본 생성자 생성

  • @AllArgsConstructor - 모든 필드 초기화 생성자 생성

  • @Builder - 빌더 패턴을 위한 빌더 클래스 생성

디컴파일러를 이용하여 생성된 클래스를 살펴보면 다음과 같다.

Book.class
// 
// Decompiled by Procyon v0.5.36
// 

package com.hacademy.annotation;

public class Book
{
    private String name;
    private int price;
    
    public static Book.BookBuilder builder() {
        return new Book.BookBuilder();
    }
    
    public String getName() {
        return this.name;
    }
    
    public int getPrice() {
        return this.price;
    }
    
    public void setName(final String name) {
        this.name = name;
    }
    
    public void setPrice(final int price) {
        this.price = price;
    }
    
    @Override
    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        }
        if (!(o instanceof Book)) {
            return false;
        }
        final Book other = (Book)o;
        if (!other.canEqual(this)) {
            return false;
        }
        if (this.getPrice() != other.getPrice()) {
            return false;
        }
        final Object this$name = this.getName();
        final Object other$name = other.getName();
        if (this$name == null) {
            if (other$name == null) {
                return true;
            }
        }
        else if (this$name.equals(other$name)) {
            return true;
        }
        return false;
    }
    
    protected boolean canEqual(final Object other) {
        return other instanceof Book;
    }
    
    @Override
    public int hashCode() {
        final int PRIME = 59;
        int result = 1;
        result = result * 59 + this.getPrice();
        final Object $name = this.getName();
        result = result * 59 + (($name == null) ? 43 : $name.hashCode());
        return result;
    }
    
    @Override
    public String toString() {
        return "Book(name=" + this.getName() + ", price=" + this.getPrice() + ")";
    }
    
    public Book() {
    }
    
    public Book(final String name, final int price) {
        this.name = name;
        this.price = price;
    }
}

생성된 내부에 Java에서 작성한 적이 없는 코드들이 추가되어 있는 것을 확인할 수 있다. 또한 @Builder 로 인하여 빌더 클래스가 하나 더 생긴것을 확인할 수 있다.

Book$BookBuilder.class
// 
// Decompiled by Procyon v0.5.36
// 

package com.hacademy.annotation;

public static class BookBuilder
{
    private String name;
    private int price;
    
    BookBuilder() {
    }
    
    public BookBuilder name(final String name) {
        this.name = name;
        return this;
    }
    
    public BookBuilder price(final int price) {
        this.price = price;
        return this;
    }
    
    public Book build() {
        return new Book(this.name, this.price);
    }
    
    @Override
    public String toString() {
        return "Book.BookBuilder(name=" + this.name + ", price=" + this.price + ")";
    }
}

사용한 Lombok의 Annotation은 제거된 것을 확인할 수 있다. 각각의 Annotation을 보면 Retention 설정이 RetentionPolicy.SOURCE로 되어 있는 것을 확인할 수 있다.

Data.class
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Data {
    //(내용 생략)
}
NoArgsConstructor.class
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface NoArgsConstructor {
    //(내용 생략) 
}
AllArgsConstructor.class
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface AllArgsConstructor {
   //(내용 생략) 
}
Builder.class
@Target({TYPE, METHOD, CONSTRUCTOR})
@Retention(SOURCE)
public @interface Builder {
   //(내용 생략) 
}

해당 내용들을 직접 적용하려면 annotation processor 등의 처리가 필요하다.

RetentionPolicy.CLASS

RetentionPolicy.CLASS로 설정된 Annotation은 컴파일 이후의 바이트 코드까지 유지되지만 런타임 시점까지 반드시 유지할 필요는 없다. 따라서 RetentionPolicy.SOURCE와 RetentionPolicy.RUNTIME의 중간 정도로 이해할 수 있다. 보기에 따라서 상당히 애매해 보일 수 있지만 자바 애플리케이션을 만들 때 다수의 jar 파일을 사용하고, 이 jar 파일에는 class 파일만 들어있다는 것을 생각해보면 배포된 파일에는 포함되어야 하지만 실행 시에는 포함될 필요가 없는 경우 사용하는 형태라고 볼 수 있다.

RetentionPolicy.RUNTIME

RetentionPolicy.RUNTIME으로 설정된 Annotation은 VM에서 런타임 시점까지 유지한다. 런타임 시점까지 유지된다는 것은 코드를 통해 Annotation의 유무와 내부에 설정된 옵션을 읽을 수 있다는 의미이다. Java Reflection과 같은 기술을 사용하여 Annotation을 해석하고, 이에 따라 다른 작업을 수행할 수 있다.

다음 Book 클래스를 통해 좀 더 자세히 살펴본다.

Book.java
package com.hacademy.annotation;

@TestEntity(author = "hacdemy", date = "2022-03-03", comment = "테스트를 위한 Annotation")
public class Item {
	private String name;
	private int price;
	
	public void setName(String name) {
		this.name = name;
	}
	public void setPrice(int price) {
		this.price = price;
	}
	public String getName() {
		return name;
	}
	public int getPrice() {
		return price;
	}
	
	@Override
	public String toString() {
		return "Item [name=" + name + ", price=" + price + "]";
	}
}

Book 클래스에는 @TestEntity 라는 Annotation이 설정되어 있다. @TestEntity의 코드는 다음과 같다.

package com.hacademy.annotation;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestEntity {
	String author() default "";
	String date() default "N/A";
	String comment() default "";
}

Retention 설정이 RetentionPolicy.RUNTIME으로 되어 있기 때문에 실행 중에도 Annotation이 유지되며 Java Reflection을 이용하여 이를 알아낼 수 있다.

AnnotationCheckApplication.java
package com.hacademy.annotation;

import java.lang.annotation.Annotation;

public class AnnotationCheckApplication {
	public static void main(String[] args) throws ClassNotFoundException {
		Class<?> c = Class.forName("com.hacademy.annotation.Item");
		Annotation annotation = c.getDeclaredAnnotation(TestEntity.class);
		if(annotation == null) {
			System.out.println("일반 클래스");
		}
		else {
			System.out.println("테스트 진행중인 클래스");
			TestEntity test = (TestEntity)annotation;
			System.out.println(test.author());
			System.out.println(test.date());
			System.out.println(test.comment());
		}
	}
}

메소드는 Method, 생성자는 Constructor, 필드는 Field 클래스에 존재하는 annotation 반환 명령을 이용하여 특정 Annotation이 존재하는지 확인할 수 있고, 필요하다면 모든 Annotation 목록을 반환하도록 만들 수 있다.

  • getAnnotation(Class) - 상속된 Annotation을 포함하여 특정 Annotation의 유무 조회

  • getDeclaredAnnotation(Class) - 상속된 Annotation을 제외하고 특정 Annotation의 유무 조회

  • getAnnotations() - 상속된 Annotation을 포함하여 목록 조회

  • getDeclaredAnnotations() - 상속된 Annotation을 제외하고 목록 조회

Last updated