Annotation

뾰족머리삼돌이·2023년 12월 20일
0

JAVA

목록 보기
6/8

Annotation

기능이 있는 주석

@Oveeride // <= 애노테이션
public void hello(){ ... }

메타데이터로, 데이터를 설명해주는 데이터를 의미한다
기본적으로 주석과 같이 직접적인 영향은 미치지 않는다

Annotaion의 위치

클래스, 인터페이스, 생성자, 메서드, 매개변수, 필드, 지역변수

@Custom
public class AnnotaionSample {

    @Custom
    private int field;


    @Custom
    private void method(@Custom Object parameter){
        @Custom
        int localVal = 10;
    }

    @Custom
    private class innser{}
    
    @Custom
    private interface  innerInter{}
}

자바 8 이후에는 타입에도 애노테이션을 달 수 있다

new @NonNull AnnotaionSample();

표준 애노테이션

자바에서 기본 제공해주는 애노테이션 @Override, @Deprecated, @FunctionalInterface ...


커스텀 애노테이션

사용자가 직접 만든 애노테이션

public @interface MyAnno { // @interface로 생성이 가능하다
	int number = 10;

	int num() default 15;
    String string();
    String[] str_arr();
}

@interface 키워드를 이용하여 생성할 수 있다
상수 또는 애노테이션 내의 요소들을 설정할 수 있다 ( Enum 등등 )

@MyAnno(string = "Hello", str_arr = {"Hello","annotation"})
public void Temp(){
	System.out.println(MyAnno.number);

	Class<Main> cls = Main.class;
    MyAnno anno = cls.getAnnotation(MyAnno.class);
    System.out.println(anno.num()); // 15
    System.out.println(anno.str_arr()); // Hello, annotaion
    System.out.println(anno.string()); // Hello
}

애노테이션 내의 상수를 꺼내쓰는게 가능하다
상수의 경우 직접 클래스명을 통한 접근으로 사용할 수 있고, 나머지는 애노테이션 객체를 생성해야한다

public @interface MyAnno {
    class MyClass{ } // 클래스 선언

    interface MyInterface{} // 인터페이스 선언

    @interface InnerAnno{} // 애노테이션 선언
}

@MyAnno
@MyAnno.InnerAnno // 애노테이션 속 애노테이션
public static void main(String[] args) {

    MyAnno.MyClass myClass = new MyAnno.MyClass(); // 애노테이션 안의 객체

    MyAnno.MyInterface myInterface = new MyAnno.MyInterface() { ... }; // 애노테이션 안의 인터페이스
}

애노테이션 내부에는 클래스나 인터페이스 혹은 다른 애노테이션이 올 수 있다


메타 애노테이션

애노테이션을 위한 애노테이션

  • 생명주기 ( @Retention )
  • 설정한 타겟 ( @Target )
  • 상속 유무 ( @Inherited )
  • 문서화 유무 ( @Documented )

등을 설정할 수 있다

@Target

종류에따라 어느 요소를 목표로 생성한 애노테이션인지 명시한다


@Retention

CLASS, RUNTIME, SOURCE 3가지의 옵션을 선택할 수 있다
RUNTIME이 가장 오래 살아있고 SOURCE가 가장 빨리 사라진다


javax.annotation.processing.Processor, 애노테이션 프로세서

애노테이션이 붙어있는 코드의 정보를 참조해서 새로운 코드를 생성해낸다

Lombok과 같이 입력한 적 없는 코드를 컴파일 과정에서 생성하는 등, 중간에 끼어들어 작업을 수행한다
컴파일 단계에서 실행되기 때문에 사전에 오류를 점검할 수 있다 ( ex : @Override )

@Override 역시 상속유무를 컴파일단계에서 표시해준다

Processor 클래스에서 제공하는 메서드를 이용하여 특정 애노테이션이 설정된 위치의 코드정보를 읽고 코드를 생성할 수 있다

예시

@Target(ElementType.TYPE) // 클래스, 인터페이스, Enum
@Retention(RetentionPolicy.SOURCE) // 컴파일시에 코드가 추가되므로 SOURCE로 충분
public @interface Magic { ... }

먼저 애노테이션 프로세서가 적용될 애노테이션을 생성한다

이 과정에서 RetensionTarget을 지정해줄 수 있다

Target의 경우 애노테이션이 설정된 인터페이스를 상속하는 클래스를 만드는 것이 목표기 때문에 TYPE으로 지정했다

Retention은 클래스파일을 생성하고나면 더이상 존재할 이유가 없어 SOURCE로 지정했다

@SupportedAnnotationTypes({"me.ddings.Magic"}) // 탐색 대상이 될 Annotation의 FQCN
public class MyAnnotationProcessor extends AbstractProcessor {

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported(); // 지원할 소스버전
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for (Element element : elements){
            if(element.getKind() != ElementKind.INTERFACE){
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "지원하지 않는 위치입니다 =>" + element.getSimpleName());
            }else{
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "설정된 작업을 수행합니다 => " + element.getSimpleName());
            }
        }

        return true; // 이 프로세서에서 해당 애노테이션에 대한 작업을 마칠건지 선택
    }
}

이제 실제로 동작할 Processor의 구현체를 만들 차례다

기본적으로 애노테이션 프로세서는 특정한 라운드를 돌아가면서 작업을 수행한다

Annotation processing happens in a sequence of rounds.
On each round, a processor may be asked to process a subset of the annotations found on the source and class files produced by a prior round.

매 라운드마다이전 라운드에서 제공된 소스파일이나 클래스파일에서 해당하는 애노테이션이 처리되었는지 확인하는 작업을 수행한다

특정 라운드에서 원하는 애노테이션을 찾게되면 작업을 수행하고 이를 다음 라운드로 넘길지를 선택할 수 있으며, 이 라운드는 roundEnv로 접근할 수 있다

roundEnv를 통해 애노테이션이 설정된 element를 찾게되면 해당 element가 조건에 부합하는지를 확인한 후에 작업을 수행하면된다


이제 생성하길 원하는 코드를 생각해야한다

public class Console {

    public static void main(String[] args) {
//        Moja moja = new MagicMoja(); 
//        System.out.println(moja.pulllOut());
    }
}

동작하길 원하는 코드는 다음과 같으며, 현재 Moja위에 @Magic이 달려있는 상태다

Moja는 인터페이스고, pullOut()은 추상메서드이다

따라서 이 두가지 작업을 수행해야한다

  1. MagicMoja 클래스 생성
    1. Moja 를 구현해야함
  2. pullOut() 구현

plugins {
    id 'java'
}

group 'me.ddings'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    implementation group: 'com.squareup', name: 'javapoet', version: '1.11.1'

    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
    useJUnitPlatform()
}

코드 생성을 위한 javapoet 의존성을 추가했다
이하 작업은 생성될 코드를 명시하기위해 애노테이션 프로세서 클래스의 process() 에 작성한다


1. 메서드 정의

// 1. 메서드 정의
MethodSpec methodSpec = MethodSpec.methodBuilder("pullOut") // 메서드 이름
        .addModifiers(Modifier.PUBLIC) // 접근제한자
        .returns(String.class) // 반환타입
        .addStatement("return $S", "Rabbit!!!") // 코드 한 줄이다
        .build();

먼저 메서드를 정의한다

위 코드를 통해서 아래 메서드를 정의한다

public String pullOut(){
		return "Rabbit!!!";
}

2. 클래스 정의

// 2. 클래스 정의
TypeElement typeElement = (TypeElement) element; // 애노테이션이 붙은 element 형변환
ClassName className = ClassName.get(typeElement); // 애노테이션이 붙은 인터페이스 이름

TypeSpec typeSpec = TypeSpec.classBuilder("MagicMoja") // 클래스 이름
        .addModifiers(Modifier.PUBLIC) // 접근제한자
        .addSuperinterface(className) // 상속할 인터페이스
        .addMethod(methodSpec) // 클래스 내부의 메서드
        .build();

다음으로 클래스를 정의한 다음 앞서 정의한 메서드를 집어넣는다

위 코드를 통해 아래 클래스를 정의한다

public class MagicMoja implements Moja{
		public String pullOut(){
			return "Rabbit!!!";
		}
}

3. 실제 소스파일로 생성

// 3. 소스파일 생성
Filer filer = processingEnv.getFiler(); // 실제 소스파일로 만들기 위함
try {
    JavaFile.builder(className.packageName(), typeSpec) // element와 동일한 패키지경로
            .build() // 생성
            .writeTo(filer); // 파일로 추가
} catch (IOException e) {
    processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR : 소스코드 생성중에 문제가 발생했습니다. => " + e);
}

이 과정을 거치면 실제로 소스파일이 생성된다


검증

resources/META-INF/services밑에 Processor의 FQCN이름으로 파일을 생성하고 애노테이션 프로세서 구현체의 FQCN을 입력한다


jar {
    from {
        configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
    }
}

Gradle 7.x 이상이라면 위와 같이 의존성을 같이 포함하여 jar파일을 생성하도록 옵션을 주고

gradle을 이용하여 의존성을 포함한 jar파일을 생성한다


해당 jar파일을 의존성을 주입받을 프로젝트의 libs 폴더에 집어넣고 ( 없으면 생성하자 )

build.gradle에 다음과 같이 작성한다

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	annotationProcessor files('libs/moja-magic-1.0-SNAPSHOT.jar') // 이 부분

	developmentOnly 'org.springframework.boot:spring-boot-devtools'

	runtimeOnly 'com.mysql:mysql-connector-j'

	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'

}

annotationProcessor files('libs/moja-magic-1.0-SNAPSHOT.jar') 를 추가하면 된다


이후 gradle을 refresh하면 위 처럼 의존성이 들어온게 보인다

gradle의 comple을 수행하자

내가 원했던 모양대로 클래스가 생성된게 보인다


만들지도않은 MagicMoja 인스턴스를 생성하고 메서드를 실행하는데 성공했다

0개의 댓글