Java에서 커스텀 어노테이션(Annotation) 만들고 사용하기

주의

이 문건은 과거 Hexo 블로그 (2017-12-14) 에서 이동된 문서입니다.

시간이 지남에 따라 최신 기술과 다를 수 있으니 주의 바랍니다.



19년 9월 8일 추가

19년 9월 8일 Github에 해당 소스를 등록하였다. Github이곳을 참고하면 된다.


커스텀 어노테이션을 만들어 보자.

Java에서 어노테이션(Annotation)이란? 포스팅에서 어노테이션에 대해 간단히 알아보았습니다.

이번 포스팅에서는 직접 커스텀 어노테이션을 작성하는 방법에 대하여 알아보도록 하겠습니다.
두 가지의 예제를 통해 알아보도록 하겠습니다.


1. 정수 값 주입 예제

처음 주제는 어노테이션을 선언한 정수형 변수에 값을 넣는 예제를 진행해보겠습니다.

간단한 예제 이므로 주석은 달지 않거나 간단한 설명으로 대체하겠습니다.

1. 어노테이션 인터페이스 작성

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InsertIntData {
    int data() default 0;
}

인터페이스를 만들어주는데 앞에 @ 표시를 붙이면 됩니다.
1번과 2번에 대한 설명은 어노테이션 포스팅에서 정리하였습니다.
일단 멤버 변수에 data라는 주입을 받을 값을 만들어 줍니다.

2. 어노테이션을 사용할 예제 클래스 작성

public class AnnotationExam01 {
    @InsertIntData(data = 30)
    private int myAge;

    @InsertIntData
    private int defaultAge;

    public AnnotationExam01() {
        this.myAge = -1;
        this.defaultAge = -1;
    }

    public int getMyAge() {
        return myAge;
    }

    public int getDefaultAge() {
        return defaultAge;
    }
}
}

변수는 다음과 같이 myAgedefaultAge 두 가지인데 myAge에 어노테이션에서는 30으로 값을 주입합니다.
하지만 defaultAge 에서는 값이 없는데 이 경우 어노테이션에서 정한 기본 값인 0으로 값이 주입이 됩니다.
생성자의 경우 값이 없을 경우 -1을 기본으로 저장합니다.

다음은 두 번째 예제인 문자열 값 주입을 보도록 하겠습니다.
두 번째 예제에서는 수행 클래스 및 실행 클래스까지 알아보겠습니다.


2. 문자열 값 주입 예제

두 번째 주제는 어노테이션을 선언한 정수형 변수에 값을 넣는 예제를 진행해보겠습니다.

간단한 예제 이므로 주석은 달지 않거나 간단한 설명으로 대체하겠습니다.

1. 어노테이션 인터페이스 작성

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InsertStringData {
    String data() default "default";
}```

위의 1-1와 비슷합니다.
기본 값으로는 **default** 문자열을 가집니다.


### 2. 어노테이션을 사용할 예제 클래스 작성

```java
public class AnnotationExam02 {

    @InsertStringData(data = "MHLab")
    private String myData;

    @InsertStringData
    private String defaultData;

    public AnnotationExam02() {
        myData = "No data";
        defaultData = "No data";
    }

    public String getMyData() {
        return myData;
    }

    public String getDefaultData() {
        return defaultData;
    }
}

변수는 다음과 같이 myDatadefaultData 두 가지인데 myData에 어노테이션에서는 “MHLab”으로 값을 주입합니다.
하지만 defaultData 에서는 값이 없는데 이 경우 어노테이션에서 정한 기본 값인 “default”로 값이 주입이 됩니다.
생성자의 경우 값이 없을 경우 “No data” 문자열을 기본으로 저장합니다.

3. 어노테이션을 수행하는 클래스 작성

public class AnnotationHandler {
    private <T> T checkAnnotation(T targetObj, Class annotationObj) {
        Field[] fields = targetObj.getClass().getDeclaredFields();
        for (Field f : fields) {
            if(annotationObj == InsertIntData.class) {
                return checkAnnotation4InsertInt(targetObj, f);
            }
            else if(annotationObj == InsertStringData.class) {
                return checkAnnotation4InsertString(targetObj, f);
            }
        }
        return targetObj;
    }

    private <T> T checkAnnotation4InsertInt(T targetObj, Field field) {
        InsertIntData annotation = field.getAnnotation(InsertIntData.class);
        if(annotation != null && field.getType() == int.class) {
            field.setAccessible(true);
            try {  field.set(targetObj, annotation.data()); }
            catch (IllegalAccessException e) { System.out.println(e.getMessage()); }
        }
        return targetObj;
    }

    private <T> T checkAnnotation4InsertString(T targetObj, Field field) {
        InsertStringData annotation = field.getAnnotation(InsertStringData.class);
        if(annotation != null && field.getType() == String.class) {
            field.setAccessible(true);
            try { field.set(targetObj, annotation.data()); }
            catch (IllegalAccessException e) { System.out.println(e.getMessage()); }
        }
        return targetObj;
    }

    public <T> Optional<T> getInstance(Class targetClass, Class annotationClass) {
        Optional optional = Optional.empty();
        Object object;
        try {
            object = targetClass.newInstance();
            object = checkAnnotation(object, annotationClass);
            optional = Optional.of(object);
        }catch (InstantiationException | IllegalAccessException e) { System.out.println(e.getMessage()); }
        return optional;
    }
}

약간 코드가 복잡한데 하나씩 설명드리겠습니다.
(코드 리펙토링이 필요하지만 예제를 위한 코드이기에 그냥 진행하겠습니다.)

getInstance 메서드

이 메서드는 두 가지의 전달인자를 받습니다.
첫 번째는 어노테이션이 적용되어 있는 2번에서 작성한 클래스, 두 번째는 체크할 어노테이션 클래스입니다.
반환 값은 Optional을 사용하여 반환하게 됩니다.
먼저 타겟 클래스의 인스턴스를 생성하고, checkAnnotation 메서드를 호출합니다.
여기서 전달인자에 어노테이션 클래스를 넣은 것은 향후 확장성을 고려 하였는데, 이 부분은 기호에 알맞게 메서드를 나눠서 구현을 해도 무방합니다.

checkAnnotation 메서드

이 메서드는 앞선 getInstance 메서드의 전달인자를 그대로 받습니다.
fields 변수는 타겟 객체에 선언된 것들을 모두 가져옵니다.
(Field는 리플렉션과 관련되어 있고, 이는 다음 포스팅에서 다루겠습니다.)
그 다음 전달인자 annotationObj 값에 따라 분기를 나눠 메서드를 호출하게 됩니다.

checkAnnotation4InsertInt 메서드 (checkAnnotation4InsertString 메서드도 동작이 비슷하게 여기 설명으로 대체합니다.)

해당 메서드는 한 라인씩 간략하게 짚고 넘어가겠습니다

InsertIntData annotation = field.getAnnotation(InsertIntData.class);
이 부분은 전달인자로 받은 Field에서 선언된 어노테이션을 가져옵니다.

if(annotation != null && field.getType() == int.class)
이 부분은 어노테이션이 null이 아니거나 선언된 변수의 타입이 int형일 경우에만 수행을 하게 됩니다.

field.setAccessible(true);
일반적으로 private로 선언된 변수(필드)의 경우 접근이 불가능하지만, 리플렉트를 통한 접근에 한하여 가능하게끔 해준다.

try { field.set(targetObj, annotation.data()); }
해당 변수의 값을 어노테이션의 값으로 치환하게 됩니다.

위와 같은 작업을 거치고 난 후 전달인자로 넘어온 어노테이션이 선언된 클래스 객체를 반환하게 됩니다.


4. 실행 클래스 작성

아래는 위의 두 가지 예제의 실행코드를 작성하였습니다.

public static void main(String[] args) {
        AnnotationHandler handler = new AnnotationHandler();
        AnnotationExam01 exam01 = handler.getInstance(AnnotationExam01.class, InsertIntData.class)
                .map(o -> (AnnotationExam01)o)
                .orElse(new AnnotationExam01());

        AnnotationExam02 exam02 = handler.getInstance(AnnotationExam02.class, InsertStringData.class)
                .map(o -> (AnnotationExam02)o)
                .orElse(new AnnotationExam02());

        System.out.println("myAge = " + exam01.getMyAge());
        System.out.println("defaultAge = " + exam01.getDefaultAge());

        System.out.println("myData = " + exam02.getMyData());
        System.out.println("defaultData = " + exam02.getDefaultData());
    }

실행결과는 다음과 같이 출력됩니다.

myAge = 30
defaultAge = -1
myData = MHLab
defaultData = No data
Process finished with exit code 0


정리

뭔가 글이 복잡하고 장황하게 쓴 것 같지만…최대한 내용을 쉽게 풀어서 작성하였습니다.
잘못되었거나 문제가 있는 부분은 알려주시면 수정하도록 하겠습니다.


Written by@MHLab
로또는 흑우집합소 🎲
와인관리, 시음노트, 셀러관리는 마와셀 🥂

🫥 My Service|  📜 Contact|  💻 GitHub