programming language/Java

리플렉션(Reflection)과 jdk 동적 프록시(dynamic proxy)

공대키메라 2022. 3. 27. 20:30

1. 프록시란? 

프록시(Proxy)는 '대리'라는 의미다. 다른 개발자분들 께서 이미 잘 정리를 해주셨다. (참고하려면 여기 클릭)

 

그리고 이 프록시를 사용하는 프록시 패턴에 대해서 전에 학습했다. 뒤에 올 내용도 이것을 바탕으로 추가를 할 예정이다. (프록시 패턴 바로가기)

2. 프록시의 단점?

전에 프록시 패턴의 구조를 한 번 다시 보고 가겠다.

 

 

현재 특정 객체에 대한 접근을 제어하거나 기능을 추가하고 있다. 

 

별 거 없어 보이지만 RealSubject가 Subject를 구현하는데 이것을 FirstProxyCache에서 캐싱 처리를 하고 있는 것이다. 

 

만약 FirstProxyCache의 기능이 맘에 안들어서 다른 class를 통해 proxy기능을 구현하려고 한다면 어떻게 해야 할 까?

 

그러면 새롭게 class를 별도로 생성해 줘야 한다. 

 

 

이런식으로 기능을 추가하게 되는데....

 

Client.java

public class Client {
    public static void main(String[] args) {
        Subject subject = new RealSubject();
        FirstProxyCache proxyCache = new FirstProxyCache(subject);
        ProxyClient client = new ProxyClient(proxyCache);
        client.execute();
        client.execute();
        client.execute();

        SecondProxyCache secondProxyCache = new SecondProxyCache(subject);
        ProxyClient client1 = new ProxyClient(secondProxyCache);
        client1.execute();
        client1.execute();
        client1.execute();

        ThirdProxyCache thirdProxyCache = new ThirdProxyCache(subject);
        ProxyClient client2 = new ProxyClient(thirdProxyCache);
        client2.execute();
        client2.execute();
        client2.execute();
    }
}

 

결과는 다음과 같은데.... 

 

 

아니 그럼... 프록시가 필요할 때 마다 새롭게 만들어야 한다는 말이자나? Yes...

 

여태 사용한 proxy들은 거의 같은 모습을 하고 있다. 

 

그래서 이것을 dynamic proxy, CGLIB를 이용해서 클래스를 별도로 생성하지 않고도 사용이 가능하다. 

 

이를 위해서는 Reflection이란 무엇인가에 대해 이해를 해야 한다.

3. 리플렉션(Reflection)이란?

리플렉션은 자바 프로그래밍 언어의 한 특징이다. 자바 프로그램을 조사 또는 내부를 스스로 들여다보도록 해주며 프로그램의 내부 성분을 조작할 수 있게 해준다. 예를 들어, 자바 클래스가 그 모든 멤버들의 이름을 가져와서 보여주는것이 가능하다.

 

필자가 다른 사이트를 참조하며 연습할 겸 코드를 작성해봤다.

 

궁금한 분은 클릭!

더보기
package reflection;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class TestMain {

    public static void main(String[] args) {
        try {
            //클래스 이름 출력
            System.out.println("클래스 이름 출력\n");
            Class clazz = SecondTarget.class;
            System.out.println("클래스 이름 출력 : " + clazz.getName());

            //이름으로 찾기
            System.out.println("이름으로 찾기\n");
            Class<?> clazz2 = Class.forName("reflection.SecondTarget");
            System.out.println("이름만 알고 있다 : " + clazz2.getName());

            //생성자 찾기
            System.out.println("생성자 찾기n\n");
            Class<?> clazz3 = Class.forName("reflection.FirstTarget");
            Constructor<?> dc = clazz3.getDeclaredConstructor();
            System.out.println("생성자 찾기 : " + dc.getName());

            //생성자 찾기 - 여러개
            System.out.println("생성자 찾기 - 여러개\n");
            Class<?> clazz4 = Class.forName("reflection.FirstTarget");
            Constructor<?>[] dc1 = clazz3.getDeclaredConstructors();
            for (Constructor<?> constructor : dc1) {
                System.out.println("생성자 찾기 - 여러개 : " + constructor);
            }

            //메소드 찾기 - 인자 있는 경우
            System.out.println("메소드 찾기 - 인자 있는 경우\n");
            Class clazz5 = Class.forName("reflection.SecondTarget");
            Method method1 = clazz5.getDeclaredMethod("method4", int.class);
            System.out.println("Find out method4 method in SecondTarget: " + method1);

            //메소드 찾기 - 인자 없는 경우
            System.out.println("메소드 찾기 - 인자 없는 경우\n");
            Class clazz6 = Class.forName("reflection.FirstTarget");
            Method method2 = clazz6.getDeclaredMethod("method1", null);
            System.out.println("Find out method1 method in FirstTarget: " + method2);

            //메소드 찾기 - 인자 여러개
            System.out.println("메소드 찾기 - 인자 여러개\n");
            Class clazz7 = Class.forName("reflection.SecondTarget");
            Class partypes[] = new Class[1];
            partypes[0]=int.class;
            Method method3 = clazz7.getDeclaredMethod("method4", partypes);
            System.out.println("Find out method1 method in SecondTarget: " + method3);

            //모든 메소드 찾기 - 상속받은 것은 안찾음
            System.out.println("모든 메소드 찾기 - 상속받은 것은 안찾음\n");
            Class clazz8 = Class.forName("reflection.SecondTarget");
            Method methods2[] = clazz8.getDeclaredMethods();
            for (Method method : methods2) {
                System.out.println("method = " + method);
            }

            //모든 메소드 찾기 - 상속받은 것도 찾음
            System.out.println("모든 메소드 찾기 - 상속받은 것도 찾음\n");
            Class clazz9 = Class.forName("reflection.SecondTarget");
            Method methods3[] = clazz9.getMethods();
            for (Method method : methods3) {
                System.out.println("method = " + method);
            }

            //필드 변수로 찾기
            System.out.println("필드 변수 찾기\n");
            Class clazz10 = Class.forName("reflection.FirstTarget");
            Field field = clazz10.getDeclaredField("str1");
            System.out.println(field);

            //모든 필드 변수로 찾기
            System.out.println("모든 필드 변수 찾기\n");
            Class clazz11 = Class.forName("reflection.FirstTarget");
            Field[] fields = clazz10.getDeclaredFields();
            for (Field field1 : fields) {
                System.out.println("field1 = " + field1);
            }

            //메소드 호출하기
            System.out.println("메소드 호출하기\n");
            SecondTarget secondTarget = new SecondTarget();
            Class<?> clazz12 = Class.forName("reflection.SecondTarget");
            Method method = clazz12.getDeclaredMethod("method4", int.class);
            int returnValue = (int) method.invoke(secondTarget, 10);
            System.out.println("return value of SecondTarget method4 : " + returnValue);

            //private 한 메소드 접근하기
            System.out.println("private 한 메소드 접근하기\n");
            SecondTarget secondTarget1 = new SecondTarget();
            Class<?> clazz13 = Class.forName("reflection.FirstTarget");
            Method method11 = clazz13.getDeclaredMethod("method1");
            method11.setAccessible(true);
            method11.invoke(secondTarget1);

            //Field 출력
            System.out.println("Field 출력\n");
            SecondTarget secondTarget2 = new SecondTarget();
            Class<?> clazz14 = Class.forName("reflection.SecondTarget");
            Field fld = clazz14.getField("target1");
            //Returns the value of the field represented by this Field,
            // on the specified object.
            // The value is automatically wrapped in an object
            // if it has a primitive type.
            System.out.println(fld.get(secondTarget2));

            fld.set(secondTarget2,"str2");
            System.out.println("fld = " + fld.get(secondTarget2));

            SecondTarget secondTarget3 = new SecondTarget();
            Class<?> clazz15 = Class.forName("reflection.SecondTarget");
            Field fld2 = clazz15.getDeclaredField("target2");
            fld2.setAccessible(true);
            fld2.set(secondTarget3, "123456");
            System.out.println(fld2.get(secondTarget3));

            //static 메소드 호출과 필드 변경
            System.out.println("static 메소드 호출\n");
            Class<?> clazz16 = Class.forName("reflection.StaticEx");
            Method method4 = clazz16.getDeclaredMethod("getSquare", int.class);
            method4.invoke(null, 10);

            System.out.println("static 필드 호출\n");
            Class<?> clazz17 = Class.forName("reflection.StaticEx");
            Field fld3 = clazz17.getDeclaredField("EXAMPLE");
            fld3.set(null, "Hello, world!");
            System.out.println("StaticExample.EXAMPLE : " + fld3.get(null));

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

    }

}

 

결과 

클래스 이름 출력

클래스 이름 출력 : reflection.SecondTarget
이름으로 찾기

이름만 알고 있다 : reflection.SecondTarget
생성자 찾기n

생성자 찾기 : reflection.FirstTarget
생성자 찾기 - 여러개

생성자 찾기 - 여러개 : public reflection.FirstTarget()
메소드 찾기 - 인자 있는 경우

Find out method4 method in SecondTarget: public int reflection.SecondTarget.method4(int)
메소드 찾기 - 인자 없는 경우

Find out method1 method in FirstTarget: private void reflection.FirstTarget.method1()
메소드 찾기 - 인자 여러개

Find out method1 method in SecondTarget: public int reflection.SecondTarget.method4(int)
모든 메소드 찾기 - 상속받은 것은 안찾음

method = private int reflection.SecondTarget.method5(int)
method = public int reflection.SecondTarget.method4(int)
모든 메소드 찾기 - 상속받은 것도 찾음

method = public int reflection.SecondTarget.method4(int)
method = public void reflection.FirstTarget.method2(int)
method = public final native void java.lang.Object.wait(long) throws java.lang.InterruptedException
method = public final void java.lang.Object.wait(long,int) throws java.lang.InterruptedException
method = public final void java.lang.Object.wait() throws java.lang.InterruptedException
method = public boolean java.lang.Object.equals(java.lang.Object)
method = public java.lang.String java.lang.Object.toString()
method = public native int java.lang.Object.hashCode()
method = public final native java.lang.Class java.lang.Object.getClass()
method = public final native void java.lang.Object.notify()
method = public final native void java.lang.Object.notifyAll()
필드 변수 찾기

private java.lang.String reflection.FirstTarget.str1
모든 필드 변수 찾기

field1 = private java.lang.String reflection.FirstTarget.str1
field1 = public java.lang.String reflection.FirstTarget.str2
메소드 호출하기

method4: 10
return value of SecondTarget method4 : 10
private 한 메소드 접근하기

method1
Field 출력

1
fld = str2
123456
static 메소드 호출

Get square : 100
static 필드 호출

StaticExample.EXAMPLE : Hello, world!

Process finished with exit code 0

 

그냥 직접 클래스를 인스턴스화 하지 않고도 해당 경로에 접근해서 클래스 정보를 가져오는 것이 리플렉션인 것이다. 

4. 동적 프록시(dynamic proxy)이란?

그래서, 동적 프록시가 무엇인가? 말 그래도 동적으로 프록시를 만들어 주는 것이다. 

 

이것의 목적은 아까 2번 주제에서 좀 느꼇을 것이다.

 

프록시! 그래... 좋은데 프록시를 우리가 필요할 때 마다 클래스를 새롭게 만들어 줄 수는 없는 노릇 아닌가?

 

이러한 걱정을 해결하는 것이 동적 프록시이다.

 

이 동적 프록시 클래스에 대해 oracle에서 다음과 같이 설명한다.

 

동적 프록시 클래스는 런 타임시에 특정 인터페이스의 리스트를 구현하는 클래스이다.

출처 : https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html

 

우리가 직접 프록시를 위한 클래스를 만들지 않고 런타임에 생성해준다.

 

동적 프록시의 기능을 다른 사이트에서 참고했다. (내가 사랑하는 daeldung)

 

다이나믹 프록시는 하나의 메소드를 가진 한 클래스가 임의의 수의 메소드를 가진 추상 클래스를 부르는 여러개의 메소드을 지원하도록 한다. 

출처 : https://www.baeldung.com/java-dynamic-proxies

 

동적 프록시는 만드는 방법은 JDK Dynamic Proxy나 CGLib를 이용해서 생성이 가능하다. 

 

두개의 차이점은 인터페이스가 있냐 없느냐이다.

5. 동적 proxy 생성 구현하기 

AInterface.java

package hello.jdkdynamic.code;

public interface AInterface {
    String call();
}

AImpl.java

package hello.jdkdynamic.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class AImpl implements AInterface{
    @Override
    public String call() {
        log.info("A 호출");
        return "A";
    }
}

ReflectTestHandler.java

package hello.jdkdynamic.code;

import lombok.extern.slf4j.Slf4j;

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

@Slf4j
public class ReflectionTestHandler implements InvocationHandler {

    private final Object target;

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


    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        log.info("ReflectionTestHandler's invoke method start");

        Object result = method.invoke(target, args);

        log.info("ReflectionTestHandler's invoke method end");
        return result;
    }
}


 

InvocationHandler는 reflection에서 제공하는 interface고 이것을 상속해서 ReflectionTestHandler를 만들었다. 

 

여기서 이제 invoke를 하게 되면 우리가 넘긴 target 관련 메서드를 작동시킨다. 

JdkDynamicProxyTest.java

package hello.jdkdynamic;

import hello.jdkdynamic.code.*;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.lang.reflect.Proxy;

@Slf4j
public class JdkDynamicProxyTest {

    @Test
    void dynamicA(){
        AInterface target = new AImpl();
        ReflectionTestHandler handler = new ReflectionTestHandler(target);

        AInterface proxy = (AInterface) Proxy.newProxyInstance(AInterface.class.getClassLoader(), new Class[]{AInterface.class}, handler);

        proxy.call();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());

        /*
        결과
        20:12:55.573 [main] INFO hello.jdkdynamic.code.ReflectionTestHandler - ReflectionTestHandler's invoke method start
        20:12:55.574 [main] INFO hello.jdkdynamic.code.AImpl - A 호출
        20:12:55.574 [main] INFO hello.jdkdynamic.code.ReflectionTestHandler - ReflectionTestHandler's invoke method end
        20:12:55.574 [main] INFO hello.jdkdynamic.JdkDynamicProxyTest - targetClass=class hello.jdkdynamic.code.AImpl
        20:12:55.575 [main] INFO hello.jdkdynamic.JdkDynamicProxyTest - proxyClass=class com.sun.proxy.$Proxy9
        */
    }

}


현재 AInterface를 target으로 선언했다. 

 

이 target을 ReflectionTestHandler를 생성하는데 넘긴다. 

 

그리고 Proxy를 생성한다. 

 

 

Proxy.newProxyInstance도 reflection 기능인데 우리가 사용할 정보를 잘 담아서 주면 된다. 

 

AInterface에 해당하는 Proxy가 생성이 되었고 이것을 ReflectionTestHandler내부에서 호출하게 된다. 

 

그러면 해당되는 기능을 그대로 상속해서 사용할 수 있는 proxy 완성!

 

 

동일한 방식으로 선언해서 사용해도 마찬가지다. 

 

여기서 중요한 것은 우리가 reflection을 이용하는 이유로 떠올릴 수 있다. 

 

 

AImpl, BImpl, CImpl을 위한 Proxy를 우리는 각자 하나씩 만들어 준것이 아니라 하나의 proxy 객체만으로 적용이 가능해졌다는 것이다!

 

현재는 인터페이스로 프록시를 구성했지만 CGLIB를 이용하면 클래스를 이용해서 프록시를 구현하기도 가능하다. 

 

4. Cglib를 이용한 동적 프록시 구현하기 

 

ConcreteService.java

package hello.common.service;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class ConcreteService {
    public void call(){
        log.info("ConcreteService 호출");
    }
}

 

CglibTestInterceptor.java

package hello.cblib.code;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

@Slf4j
public class CglibTestInterceptor implements MethodInterceptor {

    private final Object target;

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

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        log.info("CglibTestInterceptor 실행");

        Object result = methodProxy.invoke(target, args); // call

        log.info("CglibTestInterceptor 종료 ");
        return result;
    }

}

 

CglibTest.java

package hello.cblib;

import hello.cblib.code.TimeMethodInterceptor;
import hello.common.service.ConcreteService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.cglib.proxy.Enhancer;

@Slf4j
public class CglibTest {

    @Test
    void cglib(){
        ConcreteService target = new ConcreteService();

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(ConcreteService.class);
        enhancer.setCallback(new TimeMethodInterceptor(target));
        ConcreteService proxy = (ConcreteService) enhancer.create();
        log.info("targetClass={}", target.getClass());
        log.info("proxyClass={}", proxy.getClass());

        proxy.call();

    }
}

 

이러면 문제점이 Proxy를 MethodInterceptor를 통해서 구현하거나 InvocationHandler로 구현이 가능한것을 확인했는데, 이것도 상황에 따라서 어떤 것을 사용해야 할 지 선택해야 한다는 것이다. 

 

이것을 위해 스프링에서는 ProxyFactory 기능을 제공한다. 

 

출처:

https://docs.oracle.com/javase/tutorial/reflect/index.html

https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html

https://www.baeldung.com/java-dynamic-proxies

https://www.oracle.com/technical-resources/articles/java/javareflection.html

'programming language > Java' 카테고리의 다른 글

static / final / static final이란?  (2) 2022.04.03
어노테이션(annotation)이란?  (0) 2022.03.31
제네릭 이해하기  (2) 2022.03.26
interface vs abtract class  (4) 2022.02.06
BigInteger 사용하기  (0) 2022.01.18