본문 바로가기
👨‍🏫Study/JAVA

[JAVA] 06 - 7 애노테이션

by 코푸는 개발자 2022. 3. 23.
728x90

애노테이션

  • 애노테이션은 메타데이터이다.
    • 애플리케이션이 처리하는 데이터가 아닌 컴파일 과정과 실행 과정에서 코드를 어떻게 컴파일하고 처리할 것인지를 알려주는 정보이다.

애노테이션의 용도

  • 컴파일러에게 코드 문법 에러를 체크하도록 정보 제공
  • 소프트웨어 개발 툴이 빌드나 배치 시 코드를 자동으로 생성할 수 있도록 정보 제공
  • 실행 시 특정 기능을 실행하도록 정보 제공

대표적 애노테이션으로 @Override가 있다. @Override는 메소드가 오버라이드 됐음을 알려주며, 컴파일러가 오버라이드 검사를 하고 정확히 오버라이드가 되지 않았다면 에러를 발생시킨다.

애노테이션은 빌드 시 자동으로 XML 설정 파일을 생성하거나, 배포를 위한 JAR 압축 파일을 생성하는데도 사용된다.

애노테이션은 클래스의 역할을 정의하기도 한다. (스프링에서 @Component, @SpringBootApplication과 같은 것들..)

애노테이션의 타입 정의와 적용

  • 애노테이션의 타입 정의는 인터페이스 정의와 유사하다.
public @interface AnnotationName {
  ...
}
  • 정의된 애노테이션은 @AnnotationName의 형태로 이용하면 된다.

애노테이션의 엘리먼트(element)

  • 엘리먼트는 애노테이션의 멤버이다.
  • 엘리먼트는 타입과 이름으로 구성되어 있으며, 기본 값을 가질 수 있다.
  • 타입은 어떤 기본 데이터타입이나 참조타입 등이 가능하다.
  • 엘리먼트의 이름 뒤에는 메소드처럼 ()를 붙여야 한다.
public @interface AnnotationName {
  타입 elementName() {default 값}; // 엘리먼트 선언
}
public @interface AnnotationName {
  String elementName1();
  int elementName2() default 5;
}
@AnnotationName(elementName1="안녕하세요", elementName2=10);

애노테이션 기본 엘리먼트 value

애노테이션의 엘리먼트 이름이 value인 경우에는 애노테이션 속성에 간단히 값만 입력해도 된다.

public @interface AnnotationName {
  String value();
  int elementName() default 5;
}
@AnnotationName("안녕하세요");

애노테이션 적용 대상

java.lang.annotation.ElementType에 열거 상수로 정의되어 있다.

각 열거 상수와 적용 대상은 아래와 같다.

  • TYPE: 클래스, 인터페이스, 열거 타입
  • ANNOTATION_TYPE: 애노테이션
  • FIELD: 필드
  • CONSTRUCTOR: 생성자
  • METHOD: 메소드
  • LOCAL_VARIABLE: 로컬 변수
  • PACKAGE: 패키지

@Target으로 애노테이션 적용 대상 설정하기

@Target 애노테이션의 기본 엘리먼트인 value()는 ElementType 타입을 배열로 가진다.

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
public @interface Annotation {

}

위의 예제의 경우 클래스, 인터페이스, 열거 타입, 필드, 메소드에 애노테이션 적용이 가능하다.

애노테이션 유지 정책

애노테이션을 용도에 따라 어디까지 유지할 것인지 정할 수 있다. 애노테이션에 @Retnetion 애노테이션을 붙임으로써 지정할 수 있다. @Retention의 기본 엘리먼트인 value는 RetentionPolicy 타입이므로 아래 세가지 상수 중 하나를 지정하면 된다.

java.lang.annotation.RetentionPolicy에 유지정책이 열거 상수로 정의되어있다.

  • SOURCE: 소스상에만 유지한다. 바이트코드 파일에는 정보가 남지 않는다.
  • CLASS: 컴파일된 클래스(바이트코드 파일)까지 유지한다. 리플렉션을 이용해서 애노테이션 정보를 얻을 수는 없다.
  • RUNTIME: 컴파일된 클래스 이후 런타임시에도 유지한다. 리플렉션을 이용해서 런타임에 애노테이션 정보도 얻을 수 있다.

위에서 언급되는 용어인 '리플렉션'이란 클래스의 메타정보를 얻는 기능이다. 클래스의 필드, 생성자, 메소드, 적용된 애노테이션이 무엇인지를 알아내는 것이다. 리플렉션을 이용해 런타임에 애노테이션의 정보를 얻으려면 정책을 RUNTIME으로 설정해야 한다.

@Target({ElementType.TYPE, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AnnotationName{

}

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

런타임 시에 애노테이션이 적용되었는지 확인하고 엘리먼트의 값을 이용해서 특정 작업을 수행할 수 있다. 애노테이션 자체는 단순 메타데이터지만, 리플렉션을 이용하면 애노테이션의 적용 여부, 엘리먼트 값을 읽고 처리가 가능하다.

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

  • getFields()
    • 리턴 타입: Field[]
    • 필드 정보를 Field 배열로 리턴
  • getConstructors()
    • 리턴 타입: Constructor[]
    • 생성자 정보를 Constructor 배열로 리턴
  • getDeclaredMethods()
    • 리턴 타입: Method[]
    • 메소드 정보를 Method 배열로 리턴

위의 메소드를 통해 Class, Field, Constructor, Method가 가지고 있는 다음 메소드를 호출해서 적용된 애노테이션 정보를 얻을 수 있다.

  • isAnnotationPresent(Class<? extends Annotation> annotationClass)
    • 리턴 타입: boolean
    • 지정한 애노테이션이 적용되었는지 여부이다. Class에서 호출했을 때, 상위 클래스에 적용된 경우에도 true를 리턴한다.
  • getAnnotation(Class<T> annotationClass)
    • 리턴 타입: Annotation
    • 지정한 애노테이션이 적용되어 있으면 애노테이션을 리턴하고, 그렇지 않다면 null을 리턴한다. Class에서 호출했을 때 상위 클래스에 적용된 경우에도 애노테이션을 리턴한다.
  • getAnnotations()
    • 리턴 타입: Annotation[]
    • 적용된 모든 애노테이션을 리턴한다. Class에서 호출했을 때 상위 클래스에서 적용된 애노테이션도 모두 포함한다. 적용된 애노테이션이 없을 경우 길이가 0인 배열을 리턴한다.
  • getDeclaredAnnotations()
    • 리턴 타입: Annotation[]
    • 직접 적용된 모든 애노테이션을 리턴한다. Class에서 호출했을 때, 상위 클래스에 적용된 애노테이션은 포함되지 않는다.

애노테이션 예제 실행해보기

@PrintAnnotation 애노테이션

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

애노테이션은 단순히 메타데이터로 어떠한 정보들을 갖고 있다.

위의 애노테이션의 타겟은 ElementType.Method라 메소드에 적용되며,
애노테이션의 유지기한은 RetentionPolicy.RUNTIME이므로 런타임까지 유지된다.

기본 엘리멘트인 value()에는 "-"가 기본 값으로 들어있다.
number() 엘리먼트에는 15가 기본 값으로 들어있다.

Service 클래스

public class Service {
    @PrintAnnotation
    public void method1() {
        System.out.println("실행 내용1");
    }

    @PrintAnnotation("*")
    public void method2() {
        System.out.println("실행 내용2");
    }

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

모든 메소드에 @PrintAnnotation을 주었고, 엘리먼트에 대한 값들은 각각 조금씩 다르게 주었다.

PrintAnnotationExample 클래스

public class PrintAnnotationExample {
    public static void main(String[] args) {
        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 = " + method.getName());

                // 구분선 출력
                for (int i = 0; i < printAnnotation.number(); i++) {
                    System.out.print(printAnnotation.value());
                }

                System.out.println();

                try {
                    // 메소드 호출
                    method.invoke(new Service());
                } catch (Exception e) {

                }

                System.out.println();
            }
        }
    }
}
  • @PrintAnnotation의 메타데이터를 이용해 처리되는 메인 메소드를 가지고 있다.
  • .class에서 getDeclaredMethods() 메소드로 상속되지 않고 해당 클래스에 직접 적용된 애노테이션을 불러온다.
  • @PrintAnnotation이 적용되었는지 확인 후에 해당 애노테이션을 method.getAnnotation() 메소드를 통해 가져온다.
  • 애노테이션 적용을 확인 후에 애노테이션의 메타데이터를 이용한 출력을 한다.
  • 이후 method.invoke() 메소드를 이용해 해당 메소드를 호출한다.
728x90

댓글