개발/Java, Kotlin

[Whiteship][12주차] Annotation

Kangjieun11 2022. 11. 20. 08:26
728x90

 

 

 

 

 

WhiteShip Java Live Study 의 커리큘럼을 따라 개인적으로 공부한 내용입니다.

 

 

 

 


Codestates BE 42 : Study, Commercise 01 Java

 

Thanks to

" Codestates_SEB_BE_42 : Commercise "

강지은, 김례화, 전진우

22.11.15

 

 

 

 

 

 

스터디 진행 후 추가적으로 재구성한 부분이 있습니다.

 


 

 

12주차 :  Annotation

 

 

목차

  • Annotation이란?
    • 등장배경
    • 주요 역할
    • 종류
  • @target
  • @retention
  • @documented
  • Annotation 정의하는 방법
    • 왜 Custom Annotation을 사용할까?
    • 애너테이션 정의하는 방법
    • 애너테이션 요소
    • 애너테이션 조상
    • 애너테이션의 배치 및 사용예시
      • 예제 소스코드에서 사용되는,  미리 대충 알고가는 개념  :  Reflection 
      • Custom Annotation 예제 소스코드
  • Annotation Processor
    • Annotation Processor 동작방식
    • AST (Abstract Syntax Tree) 추상 구문 트리
    • example :  Lombok Library

 

 


 

 

 

✅ Annotation

 주석과 같이 프로그래밍 언어에 영향을 미치지 않으며, 유용한 정보를 제공.

프로그램의 소스코드 안에 다른 프로그램을 위한 정보를 미리 약속된 형식으로 포함시킨 것.

 

"주석"

Annotation vs comment

차이는 정보를 전달하는 대상이 누구(무엇)인가에 있다. 

 

⏺ 등장 배경

주석

과거에는 소스코드에 대한 문서를 따로 저장했기 때문에 소스코드를 변경할 때마다 문서를 같이 변경해야 했다. 그러다보니 소스코드 변경 후 문서를 변경하는 것을 놓치고 넘어가는 경우가 종종 있었다. -> 관리 어려움 => 주석의 등장 소스코드와 개발문서 버전 불일치 발생 많았음 -> 주석의 적극 도입으로 해결 주석의 등장으로 소스코드와 개발 문서를 하나의 파일로 관리할 수 있게 되었다. /** ~ */ : javadoc.exe 주석 (소스코드의 주석으로부터 HTML 문서를 생성해내는 프로그램)

애너테이션

소스코드와 설정 파일(.xml) 따로 존재했었다. -> 주석의 경우와 마찬가지로 어려움이 있음 => 애너테이션의 등장 애너테이션의 등장으로 소스코드와 설정에 대한 정보를 하나의 파일로 관리할 수 있게 되었다.

 

  주요 역할

  1. 프로그램 빌드 시 코드를 자동으로 생성할 수 있도록 정보 제공
  2. 런타임에 특정 기능 실행할 수 있도록 정보 제공

해당 애너테이션을 수행하는 프로그램 외에 다른 프로그램들에게 아무런 영향을 주지 않는다.
→  명확한 타겟이 존재, 타겟 외 대상에게는 없는 존재임

 

 

⏺  애너테이션 종류

  • 표준 애너테이션
  • 메타 애너테이션
  • 사용자 정의 애너테이션 : 사용자 직접 정의

 

✔️ 표준 애너테이션

주로 컴파일러를 위한 것

 

✔️ 메타 애너테이션

'애너테이션의 애너테이션' => 애너테이션 정의, 애너테이션 만들 때 사용

 java.lang.annotation 패키지에 포함

 

✔️ 사용자 정의 애너테이션

사용자가 직접 정의하는 애너테이션

 

 

 


 

 ✅  @Target

 

애너테이션이 적용 가능한 대상을 지정하는 데에 사용.
애너테이션을 적용할 수 있는 대상을 @Target 으로 지정.
여러 개의 값을 지정할 때는 배열처럼 중괄호 {} 를 사용.

 

@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR})

 

@Target으로 지정할 수 있는 애너테이션 적용 대상

TYPE : 타입 선언할 때, 애너테이션을 붙일 수 있음.
TYPE_USE : 해당 타입의 변수를 선언할 때, 붙일 수 있음.
FIELD : 기본형 타입에 사용.
TYPE_USE : 참조형 타입에 사용.

 

 ✅ @Retention

애너테이션이 유지(retention)되는 기간을 지정하는 데에 사용.

애너테이션 유지 정책 (retention policy) 

 

✔️  SOURCE

컴파일러에 의해 사용되는 애너테이션 컴파일러를 직접 작성할 것이 아니면, 이 유지정책은 사용할 일이 없다.

 

✔️  RUNTIME

유지정책을 RUNTIME으로 하면 실행 시에 reflection을 통해 클래스 파일에 저장된 애너테이션의 정보를 읽어서 처리할 수 있다.

cf)
@FunctionalInterface
컴파일러가 체크해주는 애너테이션이지만, 실행 시에도 사용되기 때문에 유지 정책이 RUNTIME으로 설정되어있다.

 

✔️  CLASS

컴파일러가 애너테이션의 정보를 클래스 파일에 저장할 수 있게 함.
그러나 클래스 파일이 JVM에 로딩될 때 애너테이션의 정보가 무시되기 때문에 실행 시에 애너테이션에 대한 정보를 얻을 수 없다.
---   그래서 CLASS가 유지 정책의 기본값임에도 불구하고 잘 사용되지 않는다.

cf)
지역 변수에 붙은 애너테이션은 컴파일러만 인식할 수 있기 때문에,
유지 정책이 RUNTIME인 애너테이션을 지역변수에 붙여도 실행 시에 인식되지 않는다.

 

 

 ✅  @Documented

애너테이션에 대한 정보를 javadoc으로 작성한 문서에 포함시킴.

기본 애너테이션 중 @Override 와 @SuppressWarnings 를 제외하고는 @Documented가 붙어있다.

@Documented
@Retention (RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @Interface FunctionInterface {}

 

 


 

✅ Annotation 정의하는 방법

 

애너테이션도 직접 만들어서 사용할 수 있다.

 

 

⏺  왜 Custom Annotation을 사용할까?

 

  • 코드 작성 노력을 줄이기 위해(컴파일 시 주석 프로세서가 코드를 생성합니다).
  • 추가 정확성 보장을 제공하기 위해(컴파일 시 주석 프로세서가 오류에 대해 경고합니다). 이를 위한 좋은 도구 중 하나 는 null 포인터 역참조, 동시성 오류 등을 방지 하는 Checker Framework 입니다.
  • 동작을 사용자 지정하려면(런타임에 코드가 리플렉션을 사용하여 주석을 확인하고 주석이 있는지 여부에 따라 다르게 동작합니다). HibernateOracle 기사

 

  • 어노테이션을 써서 런타임 또는 컴파일 시간에 사용할 수 있는 클래스 파일에 메타 정보를 직접 추가 함.
  • → 컴파일 시 annotation processor가 추가적인 정확성을 보장하기 위해 오류에 대해 경고를 날림
  • 웹기술 프레임워크에서 XML을 피하기 위해 주석(애노테이션)을 사용한다.
  • → web.xml : DD (Deployment Descriptor : 배포 설명자)라고 불리며, Web Application의 설정파일이다. DD는 Web Application 실행 시 메모리에 로드된다.

 

✔️  Spring에서 주석과 xml 기반 프로그래밍의 차이점은?

 

Difference between annotation and xml based programming in Spring? Which one is better?

  1. annotation이 없었을 땐 xml기반 구성을 했으며, 각 클래스가 무슨 역할을 하는지 보기 위해서 xml파일로 이동해 확인함.
  2. annotation은 클래스 바로 위에 어떤 역할을 하는지 명시하기 때문에 이동할 필요가 없고, 간결 및 가독성이 뛰어나다.

 

<어떤 사람이 말한거 가져옴>

🧑‍💻 XML

→ 변경가능성이 낮은, 수십개의 소스파일에서 configurators, factories 등을 크롤링할 필요가 없는 경우에 주로 사용

→ XML을 통해 모든 서비스, 개체 및 엔터티를 정의하는 것을 고통스러움.

 

🧑‍💻 Annotation

→ 서비스, 개체 및 엔터티에 추가하는것이 좋다.

→ 조인테이블을 사용해 다대다 관계로 두 객체 조인할 때 XML은 악몽이지만 주석은 매우 간단했음

 

 

 

⏺  Annotation 정의하는 방법

  • 인터페이스 정의하는 것 앞에 @ 기호를 붙히면 됨.
  • 요소를 선언해서 사용할 수 있다.
public @interface Make {
	타입 요소이름(); //Annotation의 요소 선언
}

 

⏺ Annotation 요소

 

반환값이 있고 매개변수 없는 추상메서드의 형태를 가짐, 상속을 통해 구현하지 않아도 됨.

  • 애너테이션 적용시 요소들의 값을 빠짐없이 지정해주어야 함
  • 요소의 이름도 같이 적어주기 때문에 순서는 상관 없음
  • 요소는 기본 값을 가질 수 있다 (애너테이션 적용시 값을 지정하지 않으면 기본값이 사용 됨)
    • default 3; // 값 지정
    • default {”aaa”, ”bbb”}; // 기본 값이 여러개인 경우 괄호 {} 사용
    • default “ccc”; // 기본값이 하나인 경우 괄호 생략 가능

 

enum TestType {FIRST, FINAL}

@interface TestInfo {
	int count() default 1;
	String testedBy();
	String[] testTools() default "aaa";
	TestType testType();
	DateTime testDate(); // 자신이 아닌 다른 애너테이션을 포함할 수 있음.
}

@interface DateTime{
	String yymmdd();
	String hhmmss();
}

--------------------------

@TestInfo(
	testedBy = "Kang",
	testTools= {"Junit","AutoTester"},
	testType = TestType.FIRST,
	testDate = @DateTime(yymmdd="221113", hhmmss="235959"),

)
public class myClass {  .....   }

 

애너테이션 요소 선언시 규칙

  • 요소의 타입은 기본형, String, enum, 어노테이션, Class만 허용
  • ()안에 매개변수는 선언할 수 없다.
  • 예외를 선언할 수는 없다.(??)
  • 요소를 타입 매개변수로 정의 할 수 없다.

 

 

⏺ 애너테이션 조상

java.lang.annotation.Annotation → 인터페이스로 구성되어있음

 

즉 구현하기 위해 implements or 익명클래스로 만들어야한다.

 

Annotation annotation = new Annotation() {
     @Override
     public Class<? extends Annotation> annotationType() {
        return null;
};

 

 

 

⏺  애너테이션의 배치 및 사용 예시 

 

✔️  예제코드에서 사용되는, 미리 대충 알고가는 개념  :   Reflection 

Reflection은 클래스의 구조를 분석하는 기능 api

Reflection를 통해서 클래스의 구조를 분석해서 메소드를 검색하고 실행할 수 있다.

  • JVM이 클래스 정보는 대부분 .class에 적힌 바이트 코드대로 메모리에 올리게 된다.
  • .class 파일에 적힌 자바 바이트코드대로 메모리에 올라가 있지만, 리플렉션 코드에 의해 세세한 부분까지도 접근이 가능하게 한다.
    • 구체적인 클래스 타입을 알지 못해도, 그 클래스의 메소드, 타입 변수들에 접근할 수 있도록 해주는 자바 API
    • Class / Constructor / Method /Field 와 같은 정보를 찾을 수 있다.
    • 객체 생성/ 변수 변경 / 메서드 호출 등을 할 수 있다.

 

-- 대충 리플렉션 예제

  • 클래스.class 로 가져오기
  • 인스턴스.getClass() 로 가져오기
  • Class.forName("클래스명") 으로 가져오기
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

// 테스트할 클래스  
class Node {

  // 생성자
  public Node() { }

  // 함수 생성
  public void print() {
    System.out.println("Hello world");
  }

}

public class Example {
  // 실행 함수
  public static void main(String... args) {
    try {

      // Node 클래스의 타입을 찾는다.
      Class<?> cls = Class.forName("Node");

      // Node 클래스의 생성자를 취득한다.
      Constructor<?> constructor = cls.getConstructor();

      // 생성자를 통해 newInstance 함수를 호출하여 Node 인스턴스를 생성한다.
      Object node = constructor.newInstance();

      // Node 클래스의 print함수를 취득한다.
      Method method = cls.getMethod("print");

      // 취득한 함수에 생성한 인스턴스를 넣고 실행시킨다.
      method.invoke(node);

    } catch (Throwable e) {
      e.printStackTrace();
    }
  }
}

 

 

⏺    Custom Annotation  예제 소스코드  💻 

  1. Tester.java (어노테이션)
  2. Service.java (사용되는 클래스)
  3. Main.java (Service를 사용하는 클래스)  

 

  • Main 클래스가 Service 클래스의 메서드에  Tester 어노테이션 설정이 있는지 확인
  • 각 메서드에 설정된 어노테이션에 삽입된 값에 따라 결과를 출력하는 예제임


  [Tester.java]  

  • element로 테스터의 이름(name)과 테스트횟수(testCount) , 테스트도구 배열(testTools) 를 주었다.
  • 테스터가 사람이 아닐경우, default auto machine 지정
  • 테스트횟수의 기본값은 1
  • 테스트도구 미지정시 Junit5를 사용한다.
package Annotation_code;

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

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Tester {
    String name() default "auto machine";
    int testCount() default 1;
    String[] testTools() default "Junit5";
}

 

  [Service.java]  

  • 테스터 메서드를 3개 생성
  • 각 메서드 위에 @Tester 를 이용해 name과 테스트횟수, 테스트 도구를 각각 지정해줌
package Annotation_code;

public class Service {

    @Tester(name = "강지은", testCount = 3, testTools = {"Junit5", "JSUnit","Qunit"})
    public void tester01(){
        System.out.println("<tester01> 테스트를 진행합니다.");
    }
    @Tester(name = "김례화", testTools = "JSUnit")
    public void tester02(){
        System.out.println("<tester02> 테스트를 진행합니다.");
    }

    @Tester(name = "전진우", testCount = 2, testTools = {"Junit5", "JSUnit"} )
    public void tester03(){
        System.out.println("<tester03> 테스트를 진행합니다.");
    }
}

 

 

  [Main.java]  

  • Service.class.getMethods()
package Annotation_code;
import java.lang.reflect.Method;
public class Main {

    public static void main(String[] args) {
        Method[] methodList = Service.class.getMethods();

        for (Method m : methodList) {

            if (m.isAnnotationPresent(Tester.class)) {
                System.out.println(m.getName());
                Tester tester = m.getDeclaredAnnotation(Tester.class);

                String name = tester.name();
                String[] tools = tester.testTools();
                int count = tester.testCount();
                for (int i = 0; i < count; i++) {
                    System.out.print(name + "님이 " + tools[i] + "를 이용해 테스트를 완료했습니다.\n");
                }
                System.out.println();
            }
        }
    }
}

 

 

결과

  • 필드, 생성자, 메소드에 적용된 어노테이션 정보는 위에서 처럼 .class로부터 얻을 수 있다.

 

 

+  리플렉션  

[클래스.class.메소드]

  • 다음 메서드를 이용해 java.lang.reflect 패키지의 Field, Constructor, Method 클래스의 배열을 얻어냄
리턴타입 메소드명(매개변수) 설명
Field[] getFields() 필드 정보를 Field 배열로 리턴
Constructor[] getConstructors() 생성자 정보를 Constructor 배열로 리턴
Method[] getDelclaredMethods() 메소드 정보를 Method 배열로 리턴

 

 

+ [어노테이션 정보를 얻기 위한 메소드]

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

 

 

 


 

✅  Annotation Processor

  • 컴파일 단계에서 Annotation에 정의된 일렬의 프로세스를 동작하게 하는것.
  • java 컴파일러 플러그인의 일종,  컴파일 단계에서 애노테이션들을 스캐닝하고 프로세싱하는 javac 에 속한 빌드 툴
    특정한 어노테이션이 붙은 소스코드를 컴파일 환경에서 참조해 새로룽 소스를 만들 수 있는 기능을 제공함
  • 특정 애노테이션이 동작하기 위해 애노테이션 프로세서를 만들어 등록할 수 있다.
  • 모든 애노테이션 프로세서들은 AbstractProcessor 를 상속받아야한다.

 

  • 소스 또는 리소스 파일 세트 생성
  • 기존 소스 코드를 변경
  • 기존 소스 코드 분석 및 진단 메세지 생성

 

 

 

⏺  Annotation Processor 동작 방식

여러 round에 걸쳐 수행된다. 

  1. 자바 컴파일러가 컴파일을 수행 (자바 컴파일러는 애노테이션 프로세서에 대해 미리 알고 있어야 함)
  2. 실행되지 않은 Annotation processor들을 수행 (각각의 프로세서는 모두 각자에 역할에 맞는 구현이 되어있어야 함)
  3. 프로세서 내부에서 Annotation이 달려있는 요소(변수, 메소드, 클래스 등)들에 대한 처리를 함.
    (일반적으로 이곳에서 자바 클래스를 생성) -- 새 파일을 만들고, 생성된 문자열을 클래스로 작성함
  4. 컴파일러가 모든 애노테이션 프로세서가 실행되었는지 확인 후, 실행되지 않은 annotation이 있을 경우  반복해서 작업을 수행

 

 

 

 

✔️ AST (Abstract Syntax Tree) 추상 구문 트리

  • AST 가 컴파일 단계중 구문 분석 단계의 결과물
  • AST 는 프로그래밍 언어로 작성된 소스코드의 추상 구문 구조의 트리→ 소스코드를 문법에 맞게 노드들로 쪼개서 만든 트리
  • 이 트리의 각 노드는 소스코드에서 발생되는 구조를 나타낸다.

AST (Abstract syntax tree) 추상구문트리

 

⬛️ example)      Lombok 롬복 라이브러리  

@Getter, @Setter, @Builder 등의 Annotation 제공,

표준적으로 작성해야할 코드를 개발자 대신 생성해주는 라이브러리

 

 

 

0) @Setter 어노테이션 사용 - 컴파일러의 parsing

1) 컴파일시점에 어노테이션 프로세서를 사용해 AST(abstract syntax tree) 조작

--- setter method AST 만들고, 해당 클래스 트리 AST에 연결 시킴

--- 모든 어노테이션 프로세서가 실행되지 않은 경우 round 반복

 

2) 컴파일러가 수정된 AST를 바이트코드(.class)로 변환 

 

 

 

* 결국 컴파일 후 setter 메서드를 사용할 수 있게 됨.

 

 

 

 


 

 

references

더보기