코딩하는 털보

STUDY HALLE - 12주차 본문

Diary/Study Halle

STUDY HALLE - 12주차

이정인 2021. 2. 5. 04:42

목표

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

학습할 것

  • Java Ennotation
  • 애노테이션을 정의하는 방법
  • Java 언어에서 사용되는 애노테이션
  • 다른 애노테이션에 적용되는 애노테이션
  • 애노테이션 프로세서

Java Ennotation

애노테이션은 프로그램의 일부가 아닌 프로그램에 대한 데이터(메타 데이터)를 제공한다. 애노테이션은 애노테이션이 추가 된 코드의 작동에 직접적인 영향을 주지 않는다.

애노테이션의 용도

  1. 컴파일러를 위한 정보

    컴파일러는 애노테이션을 사용하여 오류를 감지하거나 경고를 숨길 수 있다.

  2. Compile-timedeployment-time 처리 (애노테이션 프로세서)

    소프트웨어 도구는 애노테이션 정보를 처리하여 코드, XML 파일 등을 생성할 수 있다.

  3. 런타임 처리

    일부 애노테이션은 런타임중에 검사되도록 할 수 있다.

애노테이션 사용하기

애노테이션을 사용하는 가장 일반적인 형태. (@애노테이션 이름)

// '@' 다음에 오는 것이 애노테이션임을 컴파일러에 표시한다.
@Entity
public class Account { ... }

애노테이션에는 요소가 포함될 수 있으며(named or unnamed) 해당 요소는 값을 가지고 있다.

@Author(
   name = "Benjamin Franklin",
   date = "3/27/2003"
)
class MyClass { ... }

만약 애노테이션의 요소가 단 하나 라면, 해당 요소의 이름을 생략할 수 있다.

또는 애노테이션에 요소가 없으면, 괄호를 생략 할 수 있다.

@SuppressWarnings("unchecked")
void myMethod() { ... }
@Entity
public class Account { ... }

동일한 선언에 여러 애노테이션을 사용할 수도 있다.

@Author(name = "Jane Doe")
@EBook
class MyClass { ... }

유형이 동일한 애노테이션을 같이 선언한 경우를 반복 애노테이션이라고 한다. (java 8 부터 지원한다.)

@Author(name = "Jane Doe")
@Author(name = "John Smith")
class MyClass { ... }

애노테이션은 클래스, 필드, 메소드 및 기타 프로그램 요소의 선언에 적용할 수 있다.

@Entity
public class Account { ... }
@id
private accountId int;
@Override
void mySuperMethod() { ... }

Java SE 8 릴리스부터는 타입에 애노테이션을 적용 할 수도 있다. (유형 애노테이션)

//인스턴스 생성 시
    new @Interned MyObject();

//타입 캐스팅
    myString = (@NonNull String) str;

//implements 절
    class UnmodifiableList<T> implements
        @Readonly List<@Readonly T> { ... }

//예외 선언부
    void monitorTemperature() throws
        @Critical TemperatureException { ... }

참고 : https://docs.oracle.com/javase/tutorial/java/annotations/index.html

,https://docs.oracle.com/javase/tutorial/java/annotations/basics.html


애노테이션을 정의하는 방법

애노테이션 정의은 일반 인터페이스 선언과 유사하다.

특징으로 @ 기호가 interface 키워드 앞에 온다.

public @interface RequestForEnhancement {
    int    id();
    String synopsis();
    String engineer() default "[unassigned]"; 
    String date()    default "[unimplemented]"; 
}

만약 아래와 같이 모든 클래스가 애노테이션으로 중요한 정보를 제공하는 사내 관례가 있다고 가정했을 때,

public class Generation3List extends Generation2List {

   // Author: John Doe
   // Date: 3/17/2002
   // Current revision: 6
   // Last modified: 4/12/2004
   // By: Jane Doe
   // Reviewers: Alice, Bill, Cindy
    ...
}

애노테이션을 사용하여 이와 동일한 메타 데이터를 추가하려면 애노테이션을 정의해야한다. 이를 위한 구문은 아래와 같다.

@interface ClassPreamble {
   String author();
   String date();
   int currentRevision() default 1;
   String lastModified() default "N/A";
   String lastModifiedBy() default "N/A";
   String[] reviewers();
}

애노테이션 정의의 본문에는 메소드와 매우 비슷한 annotation type element 선언이 포함되어 있다. annotation type element 선언은 선택적으로 기본값을 정의 할 수도 있다.

또는 value() 키워드로 특별한 요소를 선언할 수 도 있다.

public @interface ShowME {
    String value();
}

value()로 선언된 요소는 애노테이션을 사용할때 굳이 요소이름을 적어줄 필요가 없다. (unnamed)

@ShowME("the money")
public interface Bank {
    int Withdraw();
}

하지만 value()또한 다른 요소가 있다면 이름을 입력해야한다.

public @interface ShowME {
    String value();
    int dollar();
}
@ShowME(value = "the money", dollar = 1000)
public interface Bank {
    int Withdraw();
}

애노테이션 메소드의 특징

  • 각 메소드의 선언은 _annotation type element_를 정의한다.

  • 메소드 선언에는 파라미터 나 throws 절이 없어야 한다.

  • 반환 타입은 primitives, String, Class, enum, 애노테이션 및 이런 타입들의 배열로 제한된다.

  • 메소드는 기본값을 가질 수 있다.

애노테이션이 정의 된 후 다음과 같이 요소의 값을 채워서 해당 유형의 애노테이션을 사용할 수 있다.

@ClassPreamble (
   author = "John Doe",
   date = "3/17/2002",
   currentRevision = 6,
   lastModified = "4/12/2004",
   lastModifiedBy = "Jane Doe",
   // Note array notation
   reviewers = {"Alice", "Bob", "Cindy"}
)
public class Generation3List extends Generation2List {
    ...
}

참고 : https://docs.oracle.com/javase/tutorial/java/annotations/declaring.html


Java 언어에서 사용되는 애노테이션

java.lang에 사전 정의 된 애노테이션 유형은 @Deprecated, @Override 및 @SuppressWarnings 등이 있다.

@Deprecated

@Deprecated 애노테이션은 표시된 요소가 더 이상 사용되지 않으며 더 이상 사용되지 않아야 함을 나타낸다. 컴파일러는 프로그램이 @Deprecated 애노테이션이있는 메소드, 클래스 또는 필드를 사용할 때 경고를 생성한다.

요소가 더 이상 사용되지 않는 경우 다음 예제와 같이 Javadoc @deprecated 태그를 사용하여 문서화해야한다.

    /**
     * @deprecated
     * deprecated 된 건에 대한 설명
     */
    @Deprecated
    static void deprecatedMethod() { }
}

@Override

@Override 애노테이션은 해당 요소가 상위 클래스에 선언 된 요소를 재정의한다는 것을 컴파일러에 알려준다.

   @Override 
   int overriddenMethod() { }

메소드를 재정의 할 때 이 애노테이션을 사용할 필요는 없지만 오류를 방지하는 데 도움이 될 수 있다.

@Override로 표시된 메소드가 상위 클래스 중 하나의 메소드를 올바르게 재정의하지 못하면 컴파일러에 의해 컴파일 단계에서 오류가 발생한다.

@SuppressWarnings

@SuppressWarnings 애노테이션은 컴파일러가 생성 할 특정 경고(이 애노테이션이 없으면 발생할)를 억제하도록 지시한다.

다음은 @Deprecated 처리 되어있는 메소드가 사용되었으며 일반적으로 컴파일러가 경고를 생성하게 되지만, 이 경우 @SuppressWarnings 애노테이션으로 인해 경고가 표시되지 않는다.

   @SuppressWarnings("deprecation")
    void useDeprecatedMethod() {
        // deprecation warning 이 억제된다.
        objectOne.deprecatedMethod();
    }

@SafeVarargs

@SafeVarargs 애노테이션은 메소드 또는 생성자에 적용될 때 varargs 매개 변수 사용과 관련된 확인되지 않은 경고를 억제하도록 지시한다.

Java Varargs(가변인수)?

과거 자바 버전에서는 임의의 값을 사용하는 메소드를 사용하려면 메소드를 호출하기 전에 배열을 만들고 값을 배열에 넣어야했다.

Object[] arguments = {
    new Integer(7),
    new Date(),
    "a disturbance in the Force"
};

String result = MessageFormat.format(
    "At {1,time} on {1,date}, there was {2} on planet "
     + "{0,number,integer}.", arguments);

Java 5부터 사용가능한 Varargs는여러 인수가 배열로 전달되는 것을 자동화하고 숨긴다.

    public static String format(String pattern,
                                Object... arguments);

매개 변수 뒤에있는 세 개의 마침표는 배열 또는 아규먼트 시퀀스로 전달 될 수 있음을 나타낸다. Varargs는 가장 마지막 아규먼트 위치에서만 사용할 수 있다.

format 메소드 호출이 이 전보다 더 직관적이고 간결하게 대체되었다.

String result = MessageFormat.format(
    "At {1,time} on {1,date}, there was {2} on planet "
    + "{0,number,integer}.",
    7, new Date(), "a disturbance in the Force");

그렇다면 varargs를 사용할 때 어떤 경고가 나오고 언제 @SafeVarargs를 사용해야 할까?

Java varargs 매개변수에 제네릭 타입이 포함되어 있는 경우 프로그램이 비정상적으로 실행될 수 있다.

 static void m(List<String>... stringLists) {
   Object[] array = stringLists;
   List<Integer> tmpList = Arrays.asList(42);
   array[0] = tmpList; // 논리적으로 유효하지 않지만 경고없이 컴파일됨.
   String s = stringLists[0].get(0); // ClassCastException 발생.
 }

=> 제네릭 타입 varargs 배열 매개변수에 값을 저장하는 것은 type-unsafe 하다.

이렇게 varargs를 사용하는 메소드가 잘 못 실행될 수 있다는 점을 컴파일러는 미리 경고하는 것이다.

@SafeVarargs는 그 경고를 없애는 것이며, @SafeVarargs 애노테이션을 적용한다는 것은 프로그래머가 이 메소드에 대해 문제 없다고 증명하는 것이다.

@SafeVarargs
@SuppressWarnings("varargs")
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

제네릭 타입이란

제네릭 타입은 매개 변수화 된 타입을 사용하는 제네릭 클래스 또는 인터페이스 타입이다.

우리가 많이 사용하는 ArrayList<> 또한 제네릭 타입의 한 종류이다.

//"<>"로 타입 매개변수를 표시한다.
class name<T1, T2, ..., Tn> { /* ... */ }
public class nonGenericBox {
    private Object object;

    public void set(Object object) { this.object = object; }
    public Object get() { return object; }
}

public class GenericBox<T> {
    // T 는 매개 변수화된 타입이며 클래스 내에서 사용할 수 있다.
    private T t;

    public void set(T t) { this.t = t; }
    public T get() { return t; }
}

간단히 말해서 제네릭은 클래스, 인터페이스 및 메서드를 정의 할 때 자료형 (클래스 및 인터페이스)이 매개 변수가 되도록 한다.

메서드 선언에서 사용되는 일반적인 매개 변수와 마찬가지로 타입 매개 변수는 코드의 재사용성을 제공한다.

차이점은 일반적인 매개 변수에 대한 입력은 값이고 타입 매개 변수에 대한 입력은 타입이라는 것!

제네릭을 사용할 때 장점

  • 컴파일 타임에 더 강력한 타입 체크

    Java 컴파일러는 강력한 타입 체크를 제네릭 코드에 적용하고 코드가 type-safety를 위반하면 오류를 발생시킨다.

    컴파일 타임 오류를 수정하는 것은 런타임 오류를 수정하는 것보다 쉽다.

  • 캐스팅 제거

    //제네릭이없는 다음 코드는 캐스팅이 필요하다.
    List list = new ArrayList();
    list.add("hello");
    String s = (String) list.get(0);
    //제네릭을 사용하면 캐스팅이 필요하지 않다.
    List<String> list = new ArrayList<String>();
    list.add("hello");
    String s = list.get(0);   // no cast
  • 프로그래머가 제네릭 알고리즘을 구현 가능

    프로그래머는 제네릭을 사용하여 다양한 타입의 컬렉션에서 작동하고 type-safe하고 가독성 좋은 제네릭 알고리즘을 구현할 수 있다.

@FunctionalInterface

@FunctionalInterface 애노테이션은 함수형 인터페이스임을 나타내는 데 사용되는 정보 애노테이션이다.

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

단순히 정보형 인터페이스이므로 컴파일러는 @FunctionalInterface 애노테이션이 인터페이스 선언에 있는지 여부에 관계 없이 함수형 인터페이스의 정의를 충족하는 모든 인터페이스를 함수형 인터페이스로 취급한다.

참고 : https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html

,https://docs.oracle.com/javase/7/docs/api/java/lang/SafeVarargs.html

,https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html

,https://docs.oracle.com/javase/tutorial/java/generics


다른 애노테이션에 적용되는 애노테이션

다른 애노테이션에 적용되는 애노테이션을 메타 애노테이션이라고 한다. java.lang.annotation에 몇 가지 메타 애노테이션 유형이 있다.

@Retention

@Retention 애노테이션은 적용되어있는 애노테이션이 보존되는 기간을 지정한다.

RetentionPolicy 요소를 선택하여 정해진 전략중 하나를 지정할 수 있다.

  • RetentionPolicy.SOURCE – 소스 코드까지만 유지, 컴파일하면 해당 애노테이션 정보는 사라짐.
  • RetentionPolicy.CLASS – 컴파일 한 .class 파일에도 유지, 런타임에서 클래스를 메모리로 읽어오면 해당 정보는 사라짐. (default)
  • RetentionPolicy.RUNTIME – 클래스를 메모리로 읽어왔을때까지 유지, 코드에서 이 정보를 바탕으로 특정 로직을 실행할 수 있다.

@Retention 애노테이션이 없는 애노테이션의 기본 보존 기간 전략은 RetentionPolicy.CLASS 이다.

@Target

@Target 애노테이션은 적용되는 애노테이션을 적용 할 수있는 Java 요소의 종류를 제한한다.

간단히 말해서 애노테이션을 어디에 사용할 수 있는지 결정한다.

  • ElementType.ANNOTATION_TYPE
  • ElementType.CONSTRUCTOR
  • ElementType.FIELD
  • ElementType.LOCAL_VARIABLE
  • ElementType.METHOD
  • ElementType.PACKAGE
  • ElementType.PARAMETER
  • ElementType.TYPE

@Documented

API의 Javadoc 문서에 @Documented 애노테이션이 적용되어있는 애노테이션에 대한 설명을 포함해야 함을 나타낸다.

참고 : https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html

,인프런 - 스프링 프레임워크 핵심 기술, 백기선


반복 애노테이션

엘리먼트에 동일한 애노테이션을 반복 적용하려는 경우가 있다. Java SE 8 릴리스부터 반복되는 애노테이션을 통해 이를 수행 할 수 있다.

호환성을 위해 반복 애노테이션은 Java 컴파일러에 의해 자동으로 생성되는 컨테이너 어노테이션에 저장되는데, 컴파일러가 이 작업을 수행하려면 코드에 두 개의 애노테이션 선언이 필요하다.

1 단계 : 반복 가능한 애노테이션 타입 선언

애노테이션은 @Repeatable 메타 애노테이션으로 표시되어야한다.

@Repeatable 애노테이션은 그 요소로 컨테이너 애노테이션 타입을 지정할 수 있다.

import java.lang.annotation.Repeatable;

//Schedule 애노테이션의 컨테이너 애노테이션은 Schedules
@Repeatable(Schedules.class)
public @interface Schedule {
  String dayOfMonth() default "first";
  String dayOfWeek() default "Mon";
  int hour() default 12;
}

2 단계 : 컨테이너 애노테이션 타입 선언

컨테이너 어노테이션 타입에는 요소로 애노테이션 타입을 가지는 배열이 value 키워드로 있어야한다.

public @interface Schedules {
    Schedule[] value();
}

위 두단계를 설정하면 반복해서 애노테이션을 적용시킬 수 있다.

@Schedule(dayOfMonth="last")
@Schedule(dayOfWeek="Fri", hour="23")
public void doPeriodicCleanup() { ... }

애노테이션 프로세서

애노테이션 프로세스는 컴파일하는 중간에 특정한 애노테이션이 붙어있는 소스코드를 참고해서 또다른 소스코드를 작성하는 일을 해낼 수 있다.

대표적으로 Lombok이 자바의 애노테이션 프로세서를 사용하여 동작하며 컴파일 타임에 소스코드의 AST를 조작하게 된다. 본래 프로세서가 제공하는 API를 통해서는 기존 소스코드는 참조만 할 수 있으나, 공개된 API가 아닌 컴파일러 내부 클래스를 사용하여 소스 코드를 조작할 수 있다. (때문에 어떤 프로그래머들은 롬복 사용을 반대하는 경우도 있지만, 엄청난 편리함 때문에 대안(AutoValue, Immutables)이 있더라도 계속 사용되고 있는 중이다. )

AST(abstract syntax tree)?

소스코드의 구조를 트리구조로 변환한 것.

img

이미지 출처 : https://javaparser.org/inspecting-an-ast/

애노테이션 프로세서 API (java 6)

Processor 인터페이스 : 여러 라운드에 거쳐서 소스 및 컴파일 된 코드를 처리할 수 있다.

애노테이션 프로세서로 구현체 만들기

애노테이션이 달려있는 인터페이스의 구현체를 자동으로 생성해주는 @Magic 애노테이션 및 애노테이션 프로세스를 만들어보자

@Magic 애노테이션 생성

package org.example;

@Retention(RetentionPolicy.SOURCE)
public @interface Magic {

}

전용 애노테이션 프로세스 생성

package org.example;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.Set;

//AbstractProcessor : 자바가 제공하는 기본적인 프로세서 추상 클래스
public class MagicProcessor extends AbstractProcessor {

    @Override
    //어떤 애노테이션에 대해 지원할 것인지
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Magic.class.getName());
    }

    @Override
    //어떤 소스 코드 버전을 지원할 것인지
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //true를 반환하면 애노테이션 처리 끝!, false인 경우 다른 애노테이션 프로세스로 전달할 수 있다.
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        //애노테이션이 달려있는 엘리먼트를 순회하면서 인터페이스에 달려있는지 확인하고 아니면 에러
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for (Element element : elements) {
            if ( element.getKind() != ElementKind.INTERFACE ) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "me.rockintuna.Magic annotation can't be used on "+element.getKind());
            } else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing "+element.getSimpleName());
            }
        }
        return true;
    }
}

프로세서 등록하기!

resources/META-INF/services 디렉토리에 Processor 클래스의 풀네임으로 파일 생성 후 그 안에 생성한 프로세서 풀네임을 기입한다.

javax.annotation.processing.Processor > org.example.MagicProcessor

이렇게 서비스로 등록이 되어야 컴파일할때 등록된 프로세서가 일하게 된다. 이런 동작은 ServiceLoader에 의해 이루어진다.

여기까지 애노테이션 프로세스가 애노테이션을 사용하는 곳을 찾고 검사하는 것까지 완료하였다.

이제부터는 애노테이션이 소스코드를 생성하도록 해보자.

Javapoet 라이브러리는 소스코드를 생성하도록 도와준다.

    <dependency>
      <groupId>com.squareup</groupId>
      <artifactId>javapoet</artifactId>
      <version>1.11.1</version>
    </dependency>

애노테이션 프로세스 수정

//AbstractProcessor : 자바가 제공하는 기본적인 프로세서 추상 클래스
public class MagicProcessor extends AbstractProcessor {

    @Override
    //어떤 애노테이션에 대해 지원할 것인지
    public Set<String> getSupportedAnnotationTypes() {
        return Set.of(Magic.class.getName());
    }

    @Override
    //어떤 소스 코드 버전을 지원할 것인지
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }

    //true를 반환하면 애노테이션 처리 끝!, false인 경우 다른 애노테이션 프로세스로 전달할 수 있다.
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

        //인터페이스에 달려있는지 확인하고 아니면 에러
        Set<? extends Element> elements = roundEnv.getElementsAnnotatedWith(Magic.class);
        for (Element element : elements) {
            if ( element.getKind() != ElementKind.INTERFACE ) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "me.rockintuna.Magic annotation can't be used on "+element.getKind());
            } else {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing "+element.getSimpleName());
            }

            //엘리먼트로부터 이름가져오기
            TypeElement typeElement = (TypeElement) element;
            ClassName className = ClassName.get(typeElement);

            //메소드 만들기
            MethodSpec pullOut = MethodSpec.methodBuilder("pullOut")
                    .addModifiers(Modifier.PUBLIC)
                    .returns(String.class)
                    .addStatement("return $S", "Rabit")
                    .build();

            //클래스 만들기
            TypeSpec magicMoja = TypeSpec.classBuilder("MagicMoja")
                    .addModifiers(Modifier.PUBLIC)
                    .addMethod(pullOut)
                    .addSuperinterface(className)
                    .build();

            //소스코드 쓰기
            //Filer 인터페이스 : 소스 코드, 클래스 코드 및 리소스를 생성할 수 있는 인터페이스
            Filer filer = processingEnv.getFiler();
            try {
                JavaFile.builder(className.packageName(), magicMoja)
                        .build()
                        .writeTo(filer);
            } catch (IOException e) {
                processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "FATAL ERROR: "+e);
            }

        }

        return true;
    }
}

Filer가 만드는 java 파일은 target/generated-sources/annotations 에 생성되므로, 이 디렉토리를 Sources 디렉토리로 설정해주어야 한다.

여기까지 왔으면 완성!

우리는 인터페이스에 애노테이션만 달아주었지만 애노테이션 프로세서가 인터페이스를 구현하는 MagicMoja 클래스를만들어 주기 때문에 아래 코드가 성공한다.

public class App {

    public static void main(String[] args) {
        Moja moja = new MagicMoja();
        System.out.println(moja.pullOut());
    }
}
Rabit

Process finished with exit code 0
Comments