개발자는 기록이 답이다

🐙 Reflection을 활용한 Annotation 접근(with Proxy) 본문

언어/Java

🐙 Reflection을 활용한 Annotation 접근(with Proxy)

slow-walker 2023. 12. 24. 01:40

Reflection을 통한 메타데이터 접근


어노테이션은 아무런 동작을 가지지 않은 설정 정보일 뿐이다.

이 설정 정보를 이용해서 어떻게 처리할 것인지는 애플리케이션의 몫인데,

리플렉션을 이용해서 적용 대상으로부터 어노테이션의 정보를 얻을 수 있다.

 

  • 런타임 시 동적으로 프로그램(클래스의 정보)을 분석하거나 변경해야 할때 발생한다.
  • 메타데이터에는 소스 코드에 대한 정보나 추가적인 설정 정보가 포함되어 있을 수 있으며, 프로그램이 실행되는 동안 동적으로 이용하고자 할때 리플랙션을 사용한다.

클래스에 적용된 어노테이션 정보를 읽을땐 java.lang.Class를 이용하면 되지만, 필드, 메소드, 생성자와 같은 메타 정보는 java.lang.reflect패키지를 사용해야 한다. 관련 메소드는 여기에서 확인 할 수 있다.

리턴타입 메소드명(매개변수) 설명
boolean isAnnotationPresent(AnnotationName.class) 지정한 어노테이션이 적용되었는지 여부
Annotation getAnnotation(AnnotationName.class) 지정한 어노테이션이 적용되어 있으면 어노테이션을 리턴하고, 그렇지 않다면 null 리턴
Annotation[] getAnnotations 적용된 모든 어노테이션을 리턴
(public까지)
적용된 어노테이션이 없으면
길이가 0인 배열 리턴
Annotation[] getDeclaredAnnotations() 적용된 모든 어노테이션을 리턴
(private까지)
package org.example.annotation;

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

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomAnnotation {
    String value() default "기본값";
    int number();
}
package org.example.annotation;

import java.lang.reflect.Method;

public class AnnotationExample {

    @MyCustomAnnotation(number = 42)
    public void annotatedMethod() {
        // 어노테이션이 적용된 메서드
    }

    public static void main(String[] args) throws NoSuchMethodException {
        AnnotationExample example = new AnnotationExample();
        Method method = example.getClass().getMethod("annotatedMethod");

        // 어노테이션 확인
        if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
            MyCustomAnnotation annotation = method.getAnnotation(MyCustomAnnotation.class);
            System.out.println("Value: " + annotation.value());
            System.out.println("Number: " + annotation.number());
        }
    }
}

 

리플렉션을 통해 어노테이션의 속성값에 접근하는 방법

 

1. Annotation 인터페이스의 속성 메서드 사용


어노테이션은 자동으로 Annotation 인터페이스를 구현하며, 어노테이션의 각 속성은 해당 메서드로 표현된다.

즉, MyCustomAnnotation에서 만들어놓은 속성을 메서드로 사용할 수 있다.

package org.example.annotation;

import java.lang.reflect.Method;

public class AnnotationExample {

    @MyCustomAnnotation(number = 42)
    public void annotatedMethod() {...}   // 어노테이션이 적용된 메서드

    public static void main(String[] args) throws NoSuchMethodException {
        AnnotationExample example = new AnnotationExample();
        Method[] methods = example.getClass().getMethods();
        for (Method method : methods) {
            // 해당 메서드에 MyCustomAnnotation이 적용되었는지 확인
            if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
                MyCustomAnnotation annotation = method.getAnnotation(MyCustomAnnotation.class);
                int number = annotation.number(); // 메서드로 사용
                System.out.println("Method: " + method.getName() + ", Number: " + number);
                String value = annotation.value(); // 메서드로 사용
                System.out.println("Method: " + method.getName() + ", Value: " + value);
            }
            //Object 클래스에서 상속받은 메서드
            System.out.println("Method: " + method.getName() + ", Value: " + method.getDefaultValue());
        }
    }
}

 

 

 

 

궁금한점 : 속성은 2개를 해놨는데, 왜 annotatedMethod는 3가 나오는 걸까? 중간에 경계선을 뒀더니 분기문 바깥에서 나오는 것 같은데 잘은 모르겠다.

 

속성 메소드를 사용하는 방법은 어노테이션의 속성이 변경되지 않는다고 가정하고, 컴파일러가 생성한 메서드를 직접 호출하는 방식다. 속성 이름이나 타입이 변경되면 컴파일러가 생성한 메서드도 업데이트되어야 한다.

 

2. AnnotationUtils를 사용하는 방법

 

Spring 프레임워크의 AnnotationUtils 클래스를 사용하여 어노테이션의 속성값에 접근한다.

AnnotationUtils는 런타임에 리플렉션을 사용하여 속성값에 접근하므로, 컴파일 시간에 타입이나 이름 변경에 영향을 받지 않는다.

import org.springframework.core.annotation.AnnotationUtils;

import java.lang.reflect.Method;

public class AnnotationExample {

    @MyCustomAnnotation(number = 42)
    public void annotatedMethod() {...}   // 어노테이션이 적용된 메서드

    public static void main(String[] args) throws NoSuchMethodException {
        AnnotationExample example = new AnnotationExample();
        Method[] methods = example.getClass().getMethods();
        for (Method method : methods) {
            // 해당 메서드에 MyCustomAnnotation이 적용되었는지 확인
            if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
                MyCustomAnnotation annotation = AnnotationUtils.findAnnotation(method, MyCustomAnnotation.class);
                String value = annotation.value();
                int number = annotation.number();
                System.out.println("Method: " + method.getName() + ", Value: " + value);
                System.out.println("Method: " + method.getName() + ", Value: " + number);
            }
        }
    }
}

 

3. 프록시를 사용한 방법

 

차이점 newProxyInstance() 메서드 별도의 InvocationHandler구현
구현 방식 익명 클래스로 직접 InvocationHandler를 정의하여 사용 별도의 클래스(혹은 인스턴스)가
InvocationHandler를 구현한 후 사용됨
코드 위치 익명 클래스는 코드 블록 내에 직접 작성되어 사용 InvocationHandler를 구현한 클래스는
별도의 파일에 위치
재사용성 해당 프록시에서만 사용되는 일회성 코드 여러 프록시에서 같은
InvocationHandler를 재사용할 수 있음
코드 가독성 코드가 프록시 생성 코드 내에 직접 포함되어 있어 가독성 저하 코드가 분리되어 있어 가독성 향상
특이 사항 익명 클래스는 코드 내부에 존재하기 때문에 클래스 파일이 추가로 생성되지 않음 클래스를 별도로 작성해야 하므로 클래스 파일이 추가로 생성됨

 

1) newProxyInstance 메서드를 이용하는 방법

 

newProxyInstance를 사용한 경우 익명 클래스를 통해 InvocationHandler를 정의한다.

익명 클래스를 사용하는 경우 재사용성이 낮아진다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface MyInterfaceA {
    @MyCustomAnnotation(number = 42)
    void myMethod();
}

class MyRealObject implements MyInterfaceA {
    public void myMethod() {
        System.out.println("Real object's method");
    }
}

public class ProxyExamplePrint {
    public static void main(String[] args) {
        MyInterfaceA realObject = new MyRealObject();

        // 방법 1: newProxyInstance 메서드 사용
        MyInterfaceA proxyObject = (MyInterfaceA) Proxy.newProxyInstance(
                MyInterfaceA.class.getClassLoader(),
                new Class[]{MyInterfaceA.class},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("Before method call");

                        // 메서드에 MyCustomAnnotation이 존재하는지 확인
                        if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
                            System.out.println(">>> MyCustomAnnotation이 메서드에 존재!");
                            MyCustomAnnotation methodAnnotation = method.getAnnotation(MyCustomAnnotation.class);

                            // 어노테이션의 속성값에 접근
                            String value = methodAnnotation.value();
                            int number = methodAnnotation.number();

                            System.out.println(">>> Annotation value: " + value);
                            System.out.println(">>> Annotation number: " + number);

                        }
                        Object result = method.invoke(realObject, args);
                        System.out.println("After method call");
                        return result;
                    }
                });
        proxyObject.myMethod();
    }
}

 

 

2) 별도의 InvocationHandler를 직접 구현하는 방법

 

newProxyInstance를 상요한 경우 익명 클래슬르 통해 InvocationHandler를 정의하는 반면,

InvocationHandler를 직접 별도의 클래스로 정의하는 방법이다.

 

이 방법이 클래스 분리나 가독성 측면에서 재사용성,확장성 측면에서 훨씬 더 유리하다.

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface MyInterface { // 인터페이스 생성
    @MyCustomAnnotation(number = 42)
    void myMethod();
}

class MyInterfaceImpl implements MyInterface { // 구현체 생성
    @Override
    public void myMethod() {
        System.out.println("Real object's method");
    }
}

class MyInvocationHandler implements InvocationHandler { // JDK 동적 프록시
    private final Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("MyInvocationHandler 실행");
        long startTime = System.currentTimeMillis();
        // 메서드에 MyCustomAnnotation이 존재하는지 확인
        if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
            System.out.println(">>> MyCustomAnnotation이 메서드에 존재!");
            MyCustomAnnotation methodAnnotation = method.getAnnotation(MyCustomAnnotation.class);

            // 어노테이션의 속성값에 접근
            String value = methodAnnotation.value();
            int number = methodAnnotation.number();

            System.out.println(">>> Annotation value: " + value);
            System.out.println(">>> Annotation number: " + number);

        }
        long endTime = System.currentTimeMillis();
        long resultTime = endTime-startTime;
        System.out.println("MyInvocationHandler 종료 resultTime= "+ resultTime);
        // 리플랙션을 사용해서 target 인스턴스 메서드(실제 메서드) 실행
        Object result = method.invoke(target, args);
        return result;
    }
}

public class AnnotationProxyHandler {
    public static void main(String[] args) {
        MyInterface realObject = new MyInterfaceImpl();

		// 방법 2: 따로 구현한 InvocationHandler를 사용
        MyInvocationHandler handler = new MyInvocationHandler(realObject);
        
        MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
                MyInterface.class.getClassLoader(),
                new Class[]{MyInterface.class},
                handler);
        // 여기에서 myMethod가 호출되면서 어노테이션의 속성값에 접근
        proxy.myMethod();
    }
}

 

 

 

 

프록시를 사용한 방법은 클래스, 필드, 생성자 등 다른 요소에 설정한 어노테이션은 안되고, 메서드에 적용한 어노테이션만 접근할 수 있는 것 같다. 다른 요소에 대한 접근은 Class 객체를 획득하고 isAnnotationPresent메서드를 사용해야 한다. 메서드에 적용한 어노테이션만 접근할 수 있는 이유는 Invoke함수내에서는 Method method를 통해서만 정보를 직접 얻을 수 있기 때문이다.

 

또한, 필드값을 리플랙션을 통해 변경하는 기능이 있는것 처럼, 어노테이션도 리플랙션을 통해 속성값을 변경할 수 있을까 싶어서 아래처럼 만들어봤지만,  어노테이션 속성값은 런타임에서 변경할 수 없는 것 같다. 왜냐하면 어노테이션이라는게 주로 코드에서 컴파일러에게 특정 정보를 전달하기 위해 사용되는 것이기에 런타임 중에 변경되지 않는다.

/// 그냥 테스트해본거임!! 프록시가 제대로 된건지는 애매함

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

interface MyInterfaceB { // 인터페이스 생성

    @MyCustomAnnotation(number = 42)
    void myMethod();
}

class InterfaceImplB implements MyInterfaceB { // 구현체 생성 : 근데 사용안되고 있음
    @Override
    public void myMethod() {
        System.out.println("Real object's method");
    }
}

class MyAnnotationInvocationHandler implements InvocationHandler {
    private final String value;
    private final int number;

    MyAnnotationInvocationHandler(String value, int number) {
        this.value = value;
        this.number = number;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        System.out.println(">>> InvocationHandler 시작");
        if (method.isAnnotationPresent(MyCustomAnnotation.class)) {
            System.out.println(">>> MyCustomAnnotation이 메서드에 존재!");
            MyCustomAnnotation methodAnnotation = method.getAnnotation(MyCustomAnnotation.class);

            // 어노테이션의 속성값에 접근
            String newValue = value;
            int newNumber = number;
            // .. 변경할수있는 메소드가 없음
            System.out.println(">>> 변경 전 Annotation value: " + methodAnnotation.value());
            System.out.println(">>> 변경 후 Annotation value: " + newValue);
            System.out.println(">>> 변경 전 Annotation number: " + methodAnnotation.number());
            System.out.println(">>> 변경 후 Annotation number: " + newNumber);
        }
        return null;
    }
}

public class AnnotationInvocationHandlerWithName {
    public static void main(String[] args) {
        MyInterfaceB proxyB = (MyInterfaceB) Proxy.newProxyInstance(
                MyInterfaceB.class.getClassLoader(),
                new Class[]{MyInterfaceB.class},
                // 원래 여기에 구현체인 InterfaceImplB를 넣어야 하는데, 어노테이션 변경하기 위한 값을 지정해서..
                new MyAnnotationInvocationHandler("Hello from Proxy", 1000));

        proxyB.myMethod();//
    }
}



궁금한 점 : JDK 동적 프록시는 메서드만 대리하기 위한 것인가?



🤔 
느낀점

 

리플랙션을 이용해서 어노테이션에 왜 접근해야 하는가 계속 궁금했었는데, AOP관점에서 활용이 된다고 한다.

 

프록시 패턴과 JDK동적 프록시를 학습하면서 프록시 객체를 생성하는 2가지 방법에 대해 알아보았다.

처음에는 생소한 용어로 어려움을 느꼈지만, 코드를 작성하면서 하니까 3시간만에 개념을 이해할 수 있었다.

 

또한, newProxyInstance메서드를 사용하는 방법과 InvocationHandler를 직접 구현하는 방식이 뭐가 다른거지? 하고 의문이 있었는데, 익명 클래스 vs 별도 클래스 차이로 간단하게 생각하면 된다.

 

사실 새로운 용어들이나 Proxy와 같은 디자인 패턴을 접하게 되면 낯설게 느껴질 수 있지만,

사실 그냥 이름만 새롭게 정의해놓은거지 코드를 잘 뜯어보면 얼른 이해할 수 있는 것 같다.

 

Reflection과 Annotation을 사용하는 근본적인 이유(AOP)

강남언니 공식 블로그 - Annotation과 Reflection을 이용해서 Entity의 여러 필드 한번에 수정하기

Spring AnnotatedElementUtils

Proxy란

Reflection과 Annotation으로 프록시 만들어보기

동적 프록시(proxy) 기술( JDK 동적 프록시)

누구나 쉽게 배우는 Dynamic Proxy

자바 어노테이션의 모든것 - (3) : 자바 리플랙션으로 어노테이션 다루기