주의
이 문건은 과거 Hexo 블로그 (2017-12-14) 에서 이동된 문서입니다.
시간이 지남에 따라 최신 기술과 다를 수 있으니 주의 바랍니다.
19년 9월 8일 추가
커스텀 어노테이션을 만들어 보자.
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;
}
}
}
변수는 다음과 같이 myAge와 defaultAge 두 가지인데 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;
}
}
변수는 다음과 같이 myData와 defaultData 두 가지인데 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
정리
뭔가 글이 복잡하고 장황하게 쓴 것 같지만...최대한 내용을 쉽게 풀어서 작성하였습니다.
잘못되었거나 문제가 있는 부분은 알려주시면 수정하도록 하겠습니다.