[12주차] 애노테이션

janjanee·2022년 8월 1일
0
post-thumbnail

2021.02.25 작성글 이전

12. 애노테이션(annotation)

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

12-0. 애노테이션?

자바를 개발한 사람들은 소스코드에 대한 문서를 따로 만들기 보다 소스코드와 문서를 하나의 파일로 관리하는 방향을 생각했다.
소스코드의 주석 '/** ~ */'에 소스코드에 대한 정보를 저장하고, 소스코드의 주석으로부터 HTML 문서를 생성하는
프로그램(javadoc.exe)을 만들어 사용했다.

/**
 * 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 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 {
    ...
}

위는 모든 애노테이션의 조상인 Annotation 인터페이스의 소스코드 일부이다.
'/**'로 시작하는 주석 안에 소스코드 설명이 있고, 그 안에 '@'이 붙은 태그들이 눈에 띈다.
미리 정의된 태그들을 이용해서 주석안에 정보를 저장하고, javadoc.exe 라는 프로그램이 이 정보를 읽어 문서를 작성한다.

이러한 기능을 응용한 것이 애노테이션이다.

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

애노테이션은 주석처럼 프로그래밍 언어에 영향을 미치지 않으면서 다른 프로그램에 유용한 정보를 줄 수 있다.

애노테이션은 JDK에서 기본적으로 제공하는 것과, 다른 프로그램에서 제공하는 것들이 있다.
어느 것이든 약속된 형식으로 정보를 제공하기만 하면 될 뿐이다.

JDK에서 제공하는 표준 애노테이션은 주로 컴파일러에게 유용한 정보를 제공한다.
그리고 새로운 애노테이션을 정의할 때 사용하는 메타 애노테이션을 제공한다.

표준 애너테이션

@Override

  • 메소드 앞에만 붙일 수 있다.
  • 조상의 메소드를 오버라이딩 한 것이라는 걸 컴파일러에게 알려준다.
class Parent {
    void parentMethod() { }
}

class Child extends Parent {
    void parentMethood() { }
}

Parent를 상속받는 Child 클래스에서 parentMethod()를 오버라이딩 하려다 실수로 parentMethood() 라고 메소드 명을 잘못적었다.

컴파일러는 그저 parentMethood() 라는 새로운 메소드를 추가했다고 생각하고, 우리는 왜 자꾸 조상 메소드가 호출이 되는 걸까?
하고 원인을 바로 발견하기 힘들다.

class Parent {
    void parentMethod() { }
}

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

parentMethood() 메소드 위에 @Override 애노테이션을 추가해보자.
우선 인텔리제이 같은 IDE에서는 @Override에 빨간 밑줄이 나타나며 컴파일 에러가 발생한다고 알려준다.

method does not override or implement a method from a supertype

컴파일하면 위와 같은 에러 메시지가 나타난다. 오버라이딩을 해야하는데 하지 않았다는 뜻이다.
다시 메소드 명을 'parentMethod()'로 변경 하면 이번에는 컴파일 오류 메시지 없이 정상적으로 동작하게 된다.

@Deprecated

  • 기존의 기능을 대체하거나 앞으로 더 이상 사용되지 않는 필드나 메소드에 붙인다.
class NewClass {
    int newField;

    int getNewField() { return newField; }

    @Deprecated
    int oldField;

    @Deprecated
    int getOldField() { return oldField;}

}

public class AnnotationEx2 {
    public static void main(String[] args) {
        NewClass nc = new NewClass();

        nc.oldField = 10;                       // @Deprecated가 붙은 대상을 사용
        System.out.println(nc.getOldField());   // @Deprecated가 붙은 대상을 사용
    }
}

@Deprecated가 붙은 필드와 메소드를 사용한 예제이다.
컴파일을 하면 다음고 ㅏ같은 메시지가 나타나는데

...week12/AnnotationEx2.java uses or overrides a deprecated API.
...week12/AnnotationEx2.java: Recompile with -Xlint:deprecation for details.

deprecated된 대상을 사용하고 있으며 -Xlint:deprecation 옵션을 붙여서 컴파일하면 자세한 내용을 볼 수 있다는 뜻이다.

안내 메시지 정도일 뿐, @Deprecated가 붙은 대상을 절대 사용하지 못하도록 강제하는 것은 아니다.
정상적으로 컴파일도 완료되고 결과도 출력된다.

@FunctionalInterface

  • 함수형 인터페이스(functional interface)를 선언할 때 컴파일러가 '함수형 인터페이스'를 올바르게 선언했는지 확인용도(되도록 반드시 붙이자)
@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

앞 시간 쓰레드에서 배운 Runnable 인터페이스함수형 인터페이스이며, @FunctionalInterface가 붙은 것을 확인할 수 있다.

@SupperessWarnings

  • 컴파일러가 보여주는 경고메시지가 나타나지 않게 해준다.
public class AnnotationEx3 {

    @SuppressWarnings("deprecation")
    public static void main(String[] args) {
        NewClass nc = new NewClass();

        nc.oldField = 10;
        System.out.println(nc.getOldField());

        @SuppressWarnings("unchecked")
        ArrayList<NewClass> list = new ArrayList();
        list.add(nc);
    }
}

바로 예제로 살펴보면, 위의 @Deprecated 어노테이션이 붙은 필드와 메소드를 사용한 경우 컴파일 후 경고 메시지가 나타나는데
@SuppressWarning("deprecation")을 사용하여 경고 메시지를 나타나지 않도록 할 수 있다.
@SuppressWarning("unchecked")도 마찬가지로 지네릭스 타입을 지정하지 않은 경고를 억제한다.

@SuppressWarnings({ "deprecation", "unchecked", "varargs" })

둘 이상의 경고를 동시에 억제하려면 위와 같이 중괄호 안에 추가해서 작성한다.

@SafeVarargs

  • 메소드에 선언된 가변인자의 타입이 non-reifiable타입인 경우 "unchecked" 경고 발생
  • 코드에 문제가 없다면 위 경고를 억제하는 @SafeVarargs 사용
  • static이나 final이 붙은 메소드와 생성자에서만 사용가능(오버라이드 X)

💡 컴파일 후에도 제거되지 않는 타입을 reifiable, 제거되는 타입을 non-reifiable

@SafeVarargs                    // 'unchecked' 경고 억제
@SuppressWarnings("varargs")    // 'varargs' 경고 억제
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

위의 코드는 java.util.Arrays의 asList() 메소드이다. 이 메소드는 매개변수로 넘겨받은 값들로
배열을 만들어서 새로운 ArrayList 객체를 만들어서 반환하는데 이 과정에서 경고가 발생한다.

메소드에 선언된 타입 T는 컴파일 과정에서 Object로 바뀐다. 즉, Object[]가 된다.
Object[]에는 모든 타입의 객체가 들어올 수 있으므로, 이 배열로 ArrayList를 생성하는 것이 위험하다고 경고하는 것이다.

그러나, asList()가 호출되는 부분을 컴파일러가 체크해서 타입 T가 아닌 다른 타입이 들어가지 못하게
할 것이므로 위의 코드는 문제가 없다.

이럴 때 메소드 앞에 @SafeVarargs 를 붙여서 '이 메소드의 가변인자는 타입 안정성이 있다.'고
컴파일러에게 알려서 경고가 발생하지 않도록 한다.

@SafeVarargs대신, @SuppressWarnings("unchecked") 로 경고를 억제하면,
메소드 선언뿐만 아니라, 호출되는 곳에서도 애노테이션을 붙여줘야 한다.

그리고 @SafeVarargs로 'unchecked' 경고를 억제할 수 있지만, 'varargs'경고는 억제할 수 없기 때문에
습관적으로 @SafeVarargs와 @SupperessWarnings("varargs")를 같이 붙인다.

@SuppressWarning("varargs")를 붙이지 않아도 경고없이 컴파일 되기는 하지만,
-Xlint 옵션을 붙여서 컴파일 해보면, 'varargs' 경고가 발생한 것을 확인할 수 있다.
그래서 두 옵션을 같이 사용하는것이 좋다.

12-1. 애노테이션 정의하는 방법

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

'@' 기호를 붙이는 것을 제외하면 인터페이스를 정의하는 것과 동일하다.
엄밀히 말해서 @Override는 애노테이션이고, Override는 애노테이션의 타입이다.

애노테이션의 요소

애노테이션 내에 선언된 메소드를 '애노테이션의 요소(element)' 라고 한다.

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

@interface DateTime {
    String yymmdd();
    String hhmmss();
}

위에 선언된 TestInfo 애노테이션은 다섯 개의 요소를 갖는다.

  • 애노테이션의 요소는 반환값이 있고, 매개변수는 없는 추상 메소드의 형태를 가지며, 상속을 통해 구현하지 않아도 된다.
  • 애노테이션을 적용할 때 이 요소들의 값을 빠짐없이 지정해주어야 한다.
  • 요소의 이름을 적어주므로 순서는 상관없다.
@TestInfo(count = 3, testedBy = "jihan",
        testTools = {}, testType = TestType.FINAL, 
        testDate = @DateTime(yymmdd = "210208", hhmmss="223400"))
class NewClass {
    ...
}

TestInfo 애노테이션을 적용한 예제이다.

public @interface TestInfo {
    int count() default 1;      //  기본값을 1로 지정
}

@TestInfo   // @TestInfo(count=1)과 동일
public class NewClass {
    ...
}

각 요소에는 기본값을 가질 수 있다. 기본값을 지정 하면 애노테이션을 적용할 때 값을 지정하지 않아도 된다.

public @interface TestInfo {
    String value();
}

@TestInfo("passed")     // @TestInfo(value="passed")와 동일
class NewClass {
    ...
}

애노테이션 요소가 오직 하나뿐이고, 이름이 value인 경우 애노테이션을 적용할 때 요소의 이름을 생략하고 값만 적을 수 있다.

public @interface TestInfo {
    String[] testTools();
}

@TestInfo(testTools = {"JUnit", "AutoTester"})  // 값이 여러 개인 경우
@TestInfo(testTools = "JUnit")                  // 값이 하나일 때는 괄호 {} 생략가능
@TestInfo(testTools = {})                       // 값이 없을 때는 괄호 {}가 반드시 필요

요소의 타입이 배열인 경우, 괄호 {}를 사용하여 여러 개의 값을 지정할 수 있다.

public @interface TestInfo {
    String[] testTools() default { "JUnit", "AutoTester" };
}

기본값을 정할 때도 마찬가지로 괄호{}를 이용하여 여러 값을 지정할 수 있다.

@interface SuppressWarnings {
    String[] value();
}

// @SuppressWarnings (value = {"deprecation", "unchecked"}) 동일
@SuppressWarnings ({"deprecation", "unchecked"})

요소의 타입이 배열일 때도 요소의 이름이 value이면, 요소 이름이 생략 가능하다.

java.lang.annotation.Annotation

모든 애노테이션의 조상은 Annotation이다.
그러나, 애노테이션은 상속이 허용되지 않으므로 아래와 같이 명시적으로 Annotation을 상속받을 수 없다.

@interface TestInfo extends Annotation {    // 에러
    ...
}
package java.lang.annotation;

public interface Annotation {
    boolean equals(Object obj);
    int hashCode();
    String toString();
    Class<? extends Annotation> annotationType();
}

Annotation은 애노테이션이 아니라 일반적인 인터페이스로 정의되어 있다.
모든 애노테이션의 조상인 Annotation 인터페이스가 위와 같이 정의되어 있어서 모든 애노테이션 객체에 대해
equals(), hashCode(), toString()과 같은 메소드 호출이 가능한 것이다.

@MyAnnotation
@TestInfo(testTools = {"JUnit", "AutoTester"})
class NewClass {
    ...
}

public class AnnotationEx {
    public static void main(String[] args) {

        Annotation[] annotations = NewClass.class.getAnnotations();

        for (Annotation annotation : annotations) {
            System.out.println("toString(): " + annotation.toString());
            System.out.println("hashCode(): " + annotation.hashCode());
            System.out.println("equals(): " + annotation.equals(annotation));
            System.out.println("annotationType(): " + annotation.annotationType());

            System.out.println();
        }
    }
}
// 결과

toString(): @com.jihan.javastudycode.week12.MyAnnotation()
hashCode(): 0
equals(): true
annotationType(): interface com.jihan.javastudycode.week12.MyAnnotation

toString(): @com.jihan.javastudycode.week12.TestInfo(testTools={"JUnit", "AutoTester"})
hashCode(): 684224534
equals(): true
annotationType(): interface com.jihan.javastudycode.week12.TestInfo

NewClass 클래스에 적용된 모든 애노테이션에 대해 toString(), hashCode(), equals(), annotationType()을 호출한다.

💡 참고로, @Retention(RetentionPolicy.RUNTIME) 이어야 getAnnotations()를 호출 했을 때 애노테이션 정보를 가져올 수 있다.

12-2. @Retention

애노테이션이 유지(retention)되는 기간을 지정하는데 사용된다.

애노테이션 유지 정책의 종류는 다음과 같다.

유지정책의미
SOURCE소스 파일에만 존재, 클래스파일에는 존재하지 않음.
CLASS클래스 파일에 존재. 실행시에 사용불가. 기본값, 리플렉션을 이용해 정보 얻을 수 없음.
RUNTIME클래스 파일에 존재. 런타임 시 리플렉션으로 애노테이션 정보 얻을 수 있음.

SOURCE

@Override나 @SuppressWarnings 처럼 컴파일러가 사용하는 애노테이션은 유지 정책이 'SOURCE' 이다.
아래는 @Override 애노테이션 정의 소스이다.

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

RUNTIME

유지 정책을 'RUNTIME' 으로 하면, 실행 시에 '리플렉션(reflection)'을 통해 클래스 파일에 저장된
애노테이션의 정보를 읽어서 처리할 수 있다.

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

위의 @FunctionalInterface 정의는 @Override 처럼 컴파일러가 체크하는 애노테이션이지만, 실행 시에도 사용되므로
유지정책이 'RUNTIME'으로 되어 있다.

런타임 시 애노테이션 정보 사용

클래스에 적용된 애노테이션 정보를 얻으려면 java.lang.Class 를 이용하면 되지만, 필드, 생성자, 메소드에
적용된 애노테이션 정보를 얻으려면 Class의 다음 메소드를 통해서 java.lang.reflect 패키지의
Field, Constructor, Method 타입의 배열을 얻어야 한다.

아래는 예제 코드이다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintAnnotation {
    String value() default "-";
    int number() default 15;
}

PrintAnnotation 애노테이션을 만든다.

public class Service {

    @PrintAnnotation
    public void method1() {
        System.out.println("method 1");
    }

    @PrintAnnotation("*")
    public void method2() {
        System.out.println("method 2");
    }

    @PrintAnnotation(value="#", number=20)
    public void method3() {
        System.out.println("method 3");
    }

}

Service 클래스에서 3개의 메소드를 만들고 각각 @PrintAnnotation을 적용하고 각 요소의 값들을 적절히 변경하였다.

public class PrintAnnotationExample {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {

        Method[] declaredMethods = Service.class.getDeclaredMethods();

        for (Method method : declaredMethods) {

            // PrintAnnotation이 적용되었는지 확인
            if (method.isAnnotationPresent(PrintAnnotation.class)) {

                // PrintAnnotation 객체 얻기
                PrintAnnotation printAnnotation = method.getAnnotation(PrintAnnotation.class);

                // 메소드 이름 출력
                System.out.println("[" + method.getName() + "]");

                for (int i = 0; i < printAnnotation.number(); i++) {
                    System.out.printf(printAnnotation.value());
                }
                System.out.println();

                // 메소드 호출
                method.invoke(new Service());
                System.out.println();
            }

        }
    }
}
[method1]
---------------
method 1

[method2]
***************
method 2

[method3]
####################
method 3

Service 클래스의 Method 정보를 가져와서 Method[] 배열에 담는다.
Method 배열을 순회하며 메소드 명과 애노테이션 정보를 얻어 value를 number 수 만큼 출력하는 예제이다.

12-3. @Target

애노테이션이 적용가능한 대상을 지정하는데 사용된다.

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

위는 @SuppressWarning를 정의한 것인데, 이 애노테이션에 적용할 수 있는 대상을 '@Target'으로 지정하였다.

@Target으로 지정할 수 있는 애노테이션 적용대상의 종류는 아래와 같다.

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

위의 표는 'java.lang.annotation.ElementType' 열거형에 정의되어있다.

@Target({FIELD, TYPE, TYPE_USE})    // 적용대상이 FIELD, TYPE, TYPE_USE
public @interface MyAnnotation { }  // MyAnnotation 정의

@MyAnnotation       // 적용대상 TYPE
class MyClass {

    @MyAnnotation   // 적용대상 FIELD
    int i;

    @MyAnnotation   // 적용대상 TYPE_USE
    MyClass mc;

}

12-4. @Documented

애노테이션에 대한 정보가 javadoc으로 작성한 문서에 포함되도록 함

자바에서 제공하는 기본 애노테이션 중에 @Override와 @SuppressWarnings를 제외하고는 모두 이 메타 애노테이션이 붙어있다.

ex)

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

12-5. 그 외 여러 애노테이션

@Inherited

애노테이션이 자손 클래스에 상속되도록 함

@Inherited                  // @SuperAnno가 자손까지 영향 미치게 함
@interface SupperAnno {}

@SupperAnno
class Parent {}

class Child extends Parent {}

위 코드에서 Child 클래스에 @SuperAnno가 붙지 않았지만, 조상인 Parent 클래스에 붙은 애노테이션이
상속되어 Child 클래스에도 @SuperAnno가 붙은 것처럼 인식된다.

@Repeatable

@Repeatable이 붙은 애노테이션은 여러 번 붙일 수 있다.

@Repeatable(ToDos.class)
@interface ToDo {
    String value();
}

예를 들어, @ToDo 라는 애노테이션이 위와 같이 정의되어 있다면, 다음과 같이 @ToDo를 여러번 붙이는 것이 가능하다.

@ToDo("delete test codes")
@ToDo("override inherited methods")
class MyClass {
    ...
}

일반적인 애노테이션과 달리 같은 이름의 애노테이션이 여러 개가 하나의 대상에 적용될 수 있기때문에,
이 애노테이션들을 하나로 묶어서 다룰 수 있는 애노테이션도 추가로 정의해야 한다.

@interface ToDos {  //  여러 개의 ToDo 애노테이션을 담을 컨테이너 애노테이션 ToDos
    ToDo[] value(); //  ToDo 애노테이션 배열타입의 요소를 선언. 이름이 반드시 value 이어야 함.
}

@Repeatable(ToDos.class)    //  괄호 안에 컨테이너 애노테이션을 지정해 주어야함.
@interface ToDo {
    String value();
}

@Native

네이티브 메소드에 의해 참조되는 '상수 필드(constant field)'에 붙인다.

@Native public static final long MAX_VALUE = 0x7fffffffffffffffL;

위는 java.lang.Long 클래스에 정의된 상수이다.
네이티브 메소드는 JVM이 설치된 OS의 메소드를 말한다. 보통 C언어로 작성되어 있는데, 자바에서는
메소드의 선언부만 정의하고 구현은 하지 않는다. 그래서 추상 메소드처럼 선언부만 있고 몸통이 없다.

public class Object {

    ...

    public final native Class<?> getClass();
    public native int hashCode();
    protected native Object clone() throws CloneNotSupportedException;
    public final native void notifyAll();

    ...
}

모든 클래스의 조상인 Object 클래스의 메소드들은 대부분 네이티브 메소드이다.
네이티브 메소드는 자바로 정의되어 있기 때문에 호출하는 방법은 자바 일반 메소드와 동일하지만
실제로 호출되는 것은 OS의 메소드이다.

그냥 네이티브 메소드를 선언만 해놓고 호출하면 되는 건 아니고, 네이티브와 OS의 메소드를 연결해주는 작업은 별도로 필요하다.

💡 이 역할은 JNI(Java Native Interface)가 한다.

마커 애노테이션 (Marker Annotation)

값을 지정할 필요가 없는 경우, 애노테이션 요소를 하나도 정의하지 않을 수 있다.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {}   // 정의된 요소가 하나도 없음

애노테이션 요소의 규칙

  • 요소의 타입은 기본형, String, enum, 애노테이션, Class, 이들의 배열 타입 만 허용
  • ()안에 매개변수 선언 불가
  • 예외를 선언 불가
  • 요소를 타입 매개변수로 정의 불가

12-6. 애노테이션 프로세서

컴파일 시간에 애노테이션들을 스캔하고 프로세싱하는 javac에 속한 빌드 툴

위의 설명만으로는 와닿지 않는데 유명한 라이브러리 lombok을 생각해보자.

우리가 @Getter, @Setter 애노테이션만 붙였는데 컴파일된 코드에서 getter와 setter 코드가 생성되어있다.

@Getter
@Setter
public class Test {
    private String hello;
}

위는 lombok을 사용해 작성한 코드이고

public class Test {
    private String hello;

    public Test() {
    }

    public String getHello() {
        return this.hello;
    }

    public void setHello(final String hello) {
        this.hello = hello;
    }
}

컴파일된 Test.class 내용이다. @Getter, @Setter 애노테이션을 읽고 컴파일 단계에서
getHello()와 setHello()가 생겼다!

조금 더 감을 잡기 위해 애노테이션 프로세서를 만들어보자.

1. 새로운 프로젝트 생성 후 annotation 패키지를 생성 -> Hello 애노테이션을 작성한다.

package annotation;

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

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

Retention은 SOURCE 레벨로 지정하였고, METHOD에만 지정할 수 있도록 Target을 정하였다.

2. processor 패키지 생성 -> HelloProcessor 클래스를 작성한다.

import annotation.Hello;

import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.*;
import javax.tools.Diagnostic;
import java.util.Set;

public class HelloProcessor extends AbstractProcessor {

    private Messager messager;

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    @Override
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Hello.class.getName());
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(Hello.class);

        for (Element element : annotatedElements) {
            if (element.getKind() == ElementKind.METHOD) {

                checkMethod((ExecutableElement) element);
            }
        }

        return false;
    }

    private void checkMethod(ExecutableElement method) {
        // 메소드 이름 체크
        String name = method.getSimpleName().toString();
        if (!name.startsWith("hello")) {
            printError(method, "must start with \"hello\"");
        }

        // public 메소드 체크
        if (!method.getModifiers().contains(Modifier.PUBLIC)) {
            printError(method, "hello method must be public");
        }

        // static 메소드 체크
        if (method.getModifiers().contains(Modifier.STATIC)) {
            printError(method, "hello method must not be static");
        }

    }

    private void printError(Element element, String message) {
        messager.printMessage(Diagnostic.Kind.ERROR, message, element);
    }

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);

        messager = processingEnvironment.getMessager();
    }

}

모든 프로세서는 AbstractProcessor를 상속받아야 한다.

AbstractProcessor 메소드중 재정의한 주요 메소드는 아래와 같다.

메소드명설명
process각각의 프로세서의 main() 메소드 역할을 한다. 프로세서가 작업할 코드를 작성한다. RoundEnvironment 파라미터를 이용하여 특정 어노테이션이 달린 요소를 찾을 수 있다.
init모든 애노테이션 프로세서는 기본 생성자를 가져야한다. 대신, ProcessingEnvironment를 파라미터로 받아 애노테이션 프로세싱 툴이 호출하는 특별한 init() 메소드를 가지고 있다. Elements, Types, Filer와 같은 유용한 유틸 클래스를 제공.
getSupportedAnnotationTypes프로세서가 처리할 애노테이션들을 작성한다. 어노테이션으로도 사용가능 (1.7 이상)
getSupportedSourceVersion특정 자바 버전 명시. 어노테이션으로도 사용가능(1.7 이상)

@Hello 애노테이션을 적용했을 때, 컴파일 단계에서 3가지 체크를 할 것이다.

  • 메소드 명이 "hello"로 시작하는지
  • 접근 제어자가 public인 메소드 인지
  • static 메소드가 아닌지

위 3가지 조건 중 하나라도 맞지 않는다면 Messager를 통해 에러 메시지를 출력할 것이다.

3. 컴파일러가 애노테이션 프로세서를 읽을 수 있도록 패키징 작업

  • 우선 아래 파일을 생성하기 전에 먼저 [maven] clean -> install (이 작업을 안하면 maven 오류 발생!!!)
  • resources/META-INF/services/javax.annotation.processing.Processor 파일 생성
  • javax.annotation.processing.Processor 파일안에 FQCN 규칙으로 위에서 만든 프로세서를 작성

파일의 내용은 아래와 같다.

processor.HelloProcessor
  • [maven] install 실행

4. 애노테이션 프로세서 테스트

위에서 만들어진 jar 파일을 이용하여 위에서 만든 프로세서가 정상적으로 동작하는지 테스트해본다.

  • 다른 프로젝트를 띄워서 dependency를 추가하자.

    <dependency>
      <groupId>com.jihan</groupId>
      <artifactId>annotation-processor</artifactId>
      <version>1.0.0</version>
    </dependency>

    각자 앞에서 만든 dependency 정보를 pom.xml에 추가한다.

  • HelloTest 클래스 작성

import annotation.Hello;

public class HelloTest {

    @Hello
    private void helloString() {
        System.out.println("hello");
    }

    @Hello
    public static String helloHi() {
        return "hello hi";
    }

    @Hello
    public String hi() {
        return "hi";
    }

    public static void main(String[] args) {
        System.out.println("Hello Method~~~!~!~@");
    }

}

@Hello 애노테이션을 이용하여 코드를 작성하였고 실행해보자.
아마도 컴파일 시 에러 메시지가 발생할 텐데, 문제는 다음과 같다.

-> 첫 번째 helloString() 메소드는 private 이다.
-> 두 번째 helloHi() 메소드는 static 메소드이다.
-> 세 번째 hi() 메소드는 "hello"로 시작하지 않는다.

  • 컴파일 결과

    java: hello method must be public
    java: hello method must not be static
    java: must start with "hello"

예상대로 위의 문제에 대한 에러 메시지가 발생하였다.
문제 코드를 수정 하면 아래와 같고, 다시 실행하면 정상적으로 main 메소드가 실행된다.

References

profile
얍얍 개발 펀치

0개의 댓글