Java 커스텀 Annotation(Custom Annotation 만들기)

Retrofit 2.0 best-3 소스를 보면서 공부하다가 관련하여 Annotation을 커스터마이징 해봐야겠다는 생각이 들었습니다. 오픈소스를 까서 보고 수정해보고 좋은 코드들을 보면서 학습하는 방법과 습관을 가지려합니다.

자바 1.5부터 등장한! Annotation을 이용한 프로그래밍은 자바 Spring Framework에서 처음 접해봤지만 안드로이드에서도 이제는 흔히 보이는 방법입니다. 제가 경험해보고 느낀 바로는 스프링의 Aop와 비슷한 성격을 갖는거 같습니다.

/** Class, interface (including annotation type), or enum declaration */
TYPE
,

/** Field declaration (includes enum constants) */
FIELD
,

/** Method declaration */
METHOD
,

/** Formal parameter declaration */
PARAMETER
,

/** Constructor declaration */
CONSTRUCTOR
,

/** Local variable declaration */
LOCAL_VARIABLE
,

/** Annotation type declaration */
ANNOTATION_TYPE
,

/** Package declaration */
PACKAGE
,

/**
* Type parameter declaration
*
*
@since 1.8
*/
TYPE_PARAMETER
,

/**
* Use of a type
*
*
@since 1.8
*/
TYPE_USE

이는 package java.lang.annotation 에 있는 ElementType입니다. 즉, Annotation을 사용하는 방법중에 @ Target 에 파라미터로 들어가는 인자라고 할 수 있습니다. 보시는 바와같이 필드, 생성자, 파라미터 등 어디 곳에서나 붙일 수 있습니다.(주석 같은 존재입니다.)

다음은 Retention이라는 아이가 있습니다.

/**
* 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

소스를 보면 잘 설명이 되어있습니다. 설명은 용도라는게 어울릴 거같습니다. 설명은 보면

  1. SOURCE는 컴파일러에게서 버려진다. 즉 클래스에는 포함이 안된다이고,
  2. Class는 default로서 Compiler에게 class file이 기록되지만 런타임시 가상머신에 의해 retain되지 않는다.
  3. Runtime은Compiler에게 class file이 기록되고 At runtime에 VM에 의해 retain 된다고 나와있습니다.

3번을 이해하기 위해서는 reflect를 참조하라고 하네요. 이는 매우 중요하다고 생각합니다. 이유는 스프링에서 BeanFactory라는 Container를 공부할 때 객체가 호출되면 객체의 인스턴스를 생성하게되는데 이 때 필요한 것이 Reflection입니다.

한번 간단하게 만들어보겠습니다. Annotation의 뜻이 주석인 것처럼 프로세서가 존재하지 않으면 그저 주석에 불과한거 같습니다. 하지만 이를 잘 이용하면 코드가 더 명시적이라 보기 좋습니다.

간단하게 Controller에 주석다는 것으로 붙여보겠습니다. Annotation 정의된 클래스는 다음과 같습니다.

package me.r2d2.util;

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

/**
* Created by Park Ji Hong, ggikko.
*/

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UrlDescription {

public String name() default "no name";

public String description() default "no description";


}

이렇게 정의해놓고

사용할때는 다음과 같이 해주시면 됩니다.

/**
* 유저 생성
*
@param create
*
@param result
*
@return
*/
@UrlDescription(name = "회원가입", description = "회원정보를 받아옵니다. 유효성검사 필수입니다")
@RequestMapping(value = "/users", method = POST)
public ResponseEntity createUser(@RequestBody @Valid UserDto.Create create, BindingResult result) throws Exception {

if(result.hasErrors()){
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setMessage("잘못된 요청입니다");
errorResponse.setCode("bad.request");
return new ResponseEntity(errorResponse, HttpStatus.BAD_REQUEST);
}

User newUser = service.createUser(create);
return new ResponseEntity(mapper.map(newUser, UserDto.CreateResponse.class), HttpStatus.CREATED);
}

이는 프로세서가 없으니 딱히 작동하는 부분은 없고 단순히 관련 Url에 대한 설명만 하는 기능입니다.

다음으로 프로세서를 구현하여 Annotation을 만들고 이를 적용시켜보겠습니다.

먼저 어노테이션 인터페이스를 정의하였습니다. 간단히 조건을 만들어 Field명이 ggikko이고 어노테이션이 TimeLog라는 것을 가지고 있을 경우 시간을 찍어보는 메소드를 작성해보겠습니다.

package me.ggikko.annotation;

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

/**
* Created by Park Ji Hong, ggikko.
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeLog {


}

위에서 설명했듯이 타겟은 필드, Retention은 런타임으로 정하였습니다.

package me.ggikko;

import me.ggikko.annotation.TimeLog;

/**
* Created by Park Ji Hong, ggikko.
*/
public class TestClass {

public String field;

@TimeLog
public String ggikko;

public void ggikko() {}

}

이와 같이 필드가 ggikko, TimeLog라는 어노테이션을 가지고 있을 경우 시간을 찍도록 해보겠습니다.

public static void test(Class<?> test) throws NoSuchFieldException {
/** 필드 불러 온다 */
Field ggikko = test.getDeclaredField("ggikko");

if(ggikko!= null){
/** 어노테이션 불러 온다 */
Annotation annotation = ggikko.getAnnotation(TimeLog.class);

if(annotation != null) {
/** 어노테이션이 Null 이 아니면 Time을 찍는다 */
System.out.printf("Time : " + String.valueOf(System.currentTimeMillis()));
}
}
}

다음과 같이 작성하였습니다.

이는 reflection API를 이용한 것이고, 런타임 도중에 시간을 찍게 작성하였습니다.(느낌상 Aop와 비슷하지만 다릅니다.)

이는 아주 간단하게만 작성한 부분이고 원하시면 Method와 Annotation interface를 커스터마이징하여 편리한 프로그래밍을 하실 수 있습니다.

대표적으로 예제는 Annotation을 이용하여 Validation체크하는 경우를 많이 보았습니다.

이 포스팅에 구체적으로 reflection을 어떻게 써야할지 또는 성능이슈, Annotation + Aop 적용 등으로 업데이트해 나가도록 하겠습니다.