HTTP 200 OK

Memento mori & Carpe diem

Spring

Annotation && AOP란?

sjoongh 2022. 5. 11. 20:31

Annotation이란?

  • Annotation은 JDK 1.5부터 도입된 것으로 자바 소스 코드에 추가하여 사용할 수 있는 메타데이터의 일종입니다.
  • 대부분이 많이 봤을 만한 @Override, @Deprecated 가 Annotation 예로 Annotation의 구현된 정보에 따라 연결되는 방향이 결정됩니다.
  • 즉, 전체적인 소스코드의 로직을 바꾸진 않지만 Annotation의 타겟을 연결 방법이나 소스코드의 구조를 변경할 수 있습니다. Annotation의 기능을 잘 활용한다면 비즈니스 로직과 별도의 시스템 설정은 Annotation에게 위임함으로써 개발자들은 로직 구현에만 집중할 수 있습니다. 따라서 Annotation을 통해 AOP(Aspect Object Programming)을 구성할 수 있습니다.
  • 참조(https://www.nextree.co.kr/p5864/)

AOP란?

AOP Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.

 

예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.

 

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다. 

 

위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

AOP 주요 개념

 

  • Aspect(Advice + PointCut) : 위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.
  • Target : Aspect를 적용하는 곳 (클래스, 메서드 .. )
  • Advice : 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
  • JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
  • PointCut : JointPoint의 상세한 스펙을 정의한 것. 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음

AOP 특징

 

  • 프록시 패턴 기반의 AOP 구현체, 프록시 객체를 쓰는 이유는 접근 제어 및 부가기능을 추가하기 위해서임
  • 스프링 빈에만 AOP를 적용 가능
  • 모든 AOP 기능을 제공하는 것이 아닌 스프링 IoC와 연동하여 엔터프라이즈 애플리케이션에서 가장 흔한 문제(중복코드, 프록시 클래스 작성의 번거로움, 객체들 간 관계 복잡도 증가 ...)에 대한 해결책을 지원하는 것이 목적
  • AOP를 활용하여 중심적인 로직에서 부가적인 로직을 따로 분리하여 ex) : REST API 사용 시 get,post에서 어느 부분이 걸리는지 로직을 찍어보고 싶을 때, 매개 변수와 리턴되는 결과를 확인 할 때, 또는 값을 변경하는 경우에도 사용

aop에 대한 설명 참조 : https://shlee0882.tistory.com/206

aop 사용 예제 참조 : https://programforlife.tistory.com/107

 

 

Annotation의 특징

  • 자바 Annotation는 메타데이터의 일종으로 메타데이터란 어플리케이션이 처리해야 할 데이터가 아니라, 컴파일 타임과 런타임에서 코드를 어떻게 컴파일하고 처리할 것인지 알려주는 정보입니다.
  • 컴파일러에게 코드 문법 에러를 체크하도록 정보를 제공한다거나, 런타임 때 특정 기능을 실행하도록 정보를 제공할 수 있습니다.

 

Built-in Annotation

  • @Override : 오버라이딩 메소드임을 나타내고 상속된 메소드를 사용하거나, 메소드의 행동을 대체할 수 있습니다. 만약 부모 클래스 또는 인터페이스에서 해당 메소드가 없다면 컴파일 오류를 납니다.
  • @SuppressWarnings : 다음 어노테이션이 있는 코드에서는 컴파일 경고를 무시하도록 합니다.
  • @Deprecated : 더 이상 사용하지 않는 메소드를 뜻합니다. 그래도 메소드를 사용할 경우 컴파일 경로를 발생시킵니다.
  • @SafeVarargs : 가변인자 매개변수를 사용할 때 경고를 무시합니다.
  • @FunctionalInterface : 메소드가 하나만 존재하는 인터페이스로 함수형 프로그래밍의 람다 함수를 위한 인터페이스를 나타냅니다. 메소드가 하나가 아닐 경우에는 컴파일 오류가 납니다.
  • @Native : 네이티브 코드에서 참조할 수 있는 상수를 나타냅니다.

 

Meta Annotation

Annotation을 직접 커스터마이징을 할 수 있는데 이를 가능하게 하기 위해서는 Meta Annotation을 알아야 합니다.

@Target
Annotaion의 적용 범위는 Target에 따라 달라지게 됩니다. 오직 메소드에서 사용될 수도 있고 생성자 및 필드 선언에 사용될 수 있습니다. Target annotation 종류는 8개 있습니다.

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
public @interface SafeVarargs {
}

@Retention

Retention annotation을 사용하여 프로그램 lifecycle에서 annotation이 적용되는 위치를 지정합니다.
3가지 정책이 존재하고 각 구성에 대한 이해가 필요합니다.

  • RetentionPolicy.SOURCE : 컴파일 이후 컴파일러에 의해 삭제됩니다.
  • RetentionPolicy.CLASS : 컴파일러에 의해 클래스 파일에 기록되지만 런타임까지 유지되지 않기 때문에 리플렉션을 이용하여 어노테이션의 정보를 얻을 수 없습니다.
  • RetentionPolicy.Runtime : 클래스 파일에도 어노테이션 정보가 기록되고 런타임 시에 리플렉션을 이용해서 어노테이션 정보를 얻을 수 있습니다.
  • 참조(https://nesoy.github.io/articles/2018-04/Java-Annotation)

@Documented

문서에 어노테이션 정보가 표기됩니다.

@Inherited

자식 클래스가 어노테이션을 상속받도록 합니다.

@Repeatable
java8부터 지원하며, 동일한 선언에 annotation을 두번 이상 적용할 수 있도록 합니다.

@Inherited // 상속
@Documented // 문서화
@Retention(RetentionPolicy.RUNTIME)     // 런타임까지 유지
@Target({
        ElementType.PACKAGE,        // 패키지 선언 시
        ElementType.TYPE,       // 타입 선언 시
        ElementType.CONSTRUCTOR,        // 생성자 선언 시
        ElementType.FIELD,      // 클래스 멤버변수 선언 시
        ElementType.METHOD,     // 메소드 선언 시
        ElementType.ANNOTATION_TYPE,        // 어노테이션 타입 선언 시
        ElementType.LOCAL_VARIABLE,     // 지역변수 선언 시
        ElementType.PARAMETER,      // 매개변수 선언 시
        ElementType.TYPE_PARAMETER,     // 매개변수 타입 선언 시
        ElementType.TYPE_USE        // 타입 사용 시
})
public @interface TodayAnnotation {
    public enum DAY{
        MON, TUE, WED,TUR,FRI,SAT,SUN
    }

    String today() default "SUN";
    int count() default 7;
    DAY getday() default DAY.SUN;
}

 

Custom Annotation

@Inherited // 상속
@Documented // 문서화
@Retention(RetentionPolicy.RUNTIME)     // 런타임까지 유지
@Target({ 
        ElementType.FIELD     // 메소드 선언 시
})
public @interface TodayAnnotation {
    public enum DAY{
        MON, TUE, WED,TUR,FRI,SAT,SUN
    }

    String today() default "SUN";
    int count() default 7;
    DAY getday() default DAY.SUN;
}

다음과 같이 Meta Annotation을 정의하였습니다. 현재 메타 어노테이션은 클래스 파일과 더불어 런타임까지 유지되며, 메소드 선언 시에만 어노테이션이 적용됩니다.

public class AnnoExample {
    @TodayAnnotation(today = "MON")
    private String strToday;

    @TodayAnnotation(count = 6)
    private int count;

    @TodayAnnotation(getday = TodayAnnotation.DAY.FRI)
    private Enum enumToday;

    void printAnnotation(){
        System.out.println("TodayAnnotation");
    }

    public String getStrToday() {
        return strToday;
    }

    public int getCount() {
        return count;
    }

    public Enum getEnumToday() {
        return enumToday;
    }
}

어노테이션을 Target에 맞게 지정하시면 됩니다. Annotation Target을 Field로 정의했기 때문에 멤버 변수에만 어노테이션이 지정되는 것을 확인할 수 있습니다.

public class InvokeAnnotation {
    public InvokeAnnotation() {
    }

    private  <T> T invokeAnnotation(T instance) throws IllegalAccessException{
        Field[] fields = instance.getClass().getDeclaredFields();

        for(Field field : fields){
           TodayAnnotation anno = field.getAnnotation(TodayAnnotation.class);
           if(anno != null){
               System.out.println(anno.today());
               System.out.println(anno.count());
               System.out.println(anno.getday());
           }
            System.out.println();
        }
        return instance;
    }

    public <T> T get(Class clazz) throws IllegalAccessException, InstantiationException{
        T instance = (T) clazz.newInstance();
        instance = invokeAnnotation(instance);
        return instance;
    }
}
public class Main {
    public static void main(String[] args) throws InstantiationException,IllegalAccessException{
        InvokeAnnotation demo = new InvokeAnnotation();
        AnnoExample anno = demo.get(AnnoExample.class);
        anno.printAnnotation();
    }
}

자바에서는 리플렉션이라는 유용한 기능이 있습니다. 리플렉션이란 구체적인 클래스 타입을 알기 못하더라도, 클래스의 메소드, 타입, 변수들에 접근할 수 있도록 해주는 자바 API 입니다. 위에 있는 코드는 Custom Annotation을 사용한 클래스(AnnoExample)를 호출하여 확인하는 클래스입니다. invokeAnnotation 메소드에서는 제너릭으로 코드가 구현되어 있어 구체적인 클래스 타입을 알지 못하지만 리플렉션을 이용하여 멤버 변수를 호출하는 메소드를 실행하였습니다.

여기서 Retentation으로 인해 결과값이 달라질 수 있습니다. TodayAnnotation을 얻기 위해 getAnnotation 함수를 호출하였습니다. 현재 TodayAnnotation의 Retentation은 RUNTIME으로 지정되어 있기 때문에 리플렉션을 이용하여 어노테이션 정보를 얻을 수 있습니다. 하지만 그 밖의 CLASS, SOURCE로 지정 시 런타임 시에는 어노테이션 정보를 얻을 수 없습니다.

 

Annotation Processor

Annotation Process는 자바 컴파일러 플러그인의 일종으로, 어노테이션에 대한 코드베이스를 검사, 수정, 생성하는 역할을 합니다.
어노테이션 처리 동작은 여러 과정을 거쳐서 수행됩니다.

  • 자바 컴파일러는 어노테이션 프로세서에 대해 알고 있는 상태에서 컴파일을 수행합니다.
  • 실행되지 않은 어노테이션 프로세서들을 수행합니다.
  • 프로세서 내부에서 어노테이션을 붙인 클래스, 메소드, 멤버변수들에 대한 처리를 합니다.
  • 컴파일러가 모든 어노테이션 프로세스가 실행되었는지 확인하고, 그렇지 않으면 반복해서 작업을 수행합니다.
  • 참조 (https://www.charlezz.com/?p=1167, https://mysend12.medium.com/java-annotation-processor-1-7f95693748ef)