자바의 다이나믹 프록시

2023. 4. 14. 19:36프로그래밍

728x90
 

Dynamic Proxies in Java

Today we will consider a rather important and interesting topic: the creation of dynamic proxy classes in Java. It's not very simple, so we'll try to figure

codegym.cc

안녕! 오늘 우리는 Java에서 동적 프록시 클래스 생성이라는 다소 중요하고 흥미로운 주제를 생각해 볼 예정입니다. 이 주제는 그리 간단하지 않으니 예제를 통해 알아보도록 하겠습니다 :)

 

그러면, 가장 중요한 질문: 동적 프록시가 무엇이며 어떻게 쓰여 질까요?

 

프록시 클래스는 원본 클래스 위에 추가되는 일종의 "추가 기능(add-on)"으로, 필요에 따라 원본 클래스의 동작을 변경할 수 있는 클래스입니다.

 

"동작 변경하다"란 무엇을 의미하며 어떻게 동작하는 것을 말할까요?

 

간단한 예를 하나를 생각해 봅시다. 우리는 Person 인터페이스와 이 인터페이스를 구현하는 간단한 Man 클래스가 있다고 가정해 봅시다.

public interface Person {

   public void introduce(String name);

   public void sayAge(int age);

   public void sayWhereFrom(String city, String country);
}

public class Man implements Person {

   private String name;
   private int age;
   private String city;
   private String country;

   public Man(String name, int age, String city, String country) {
       this.name = name;
       this.age = age;
       this.city = city;
       this.country = country;
   }

   @Override
   public void introduce(String name) {

       System.out.println("My name is " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("I am " + this.age + " years old");
   }

   @Override
   public void sayWhereFrom(String city, String country) {

       System.out.println("I'm from " + this.city + ", " + this.country);
   }

   // ...getters, setters, etc.
}
 

우리 Man 클래스에는 3가지 메서드가 있습니다: introduce, sayAge 및 sayWhereFrom이 그것입니다 .

 

기존의 JAR 라이브러리의 일부로 이 클래스를 얻었고 우리는 코드를 단순히 다시 작성할 수 없다고 생각해 봅시다.

 

그렇지만 우리는 또한 이 클래스의 행동을 바꿀 필요가 있습니다. 예를 들어 객체에서 어떤 메서드가 호출될지 모르지만, 메서드가 호출될 때 우리의 person이 "안녕하세요!"라고 말하길 원한다는 것이죠. (무례한 사람을 좋아하는 사람은 아무도 없습니다) .

 

이 상황에서 우리가 해야 할 것은 무엇일까요?

 

우리는 몇 가지가 필요 할 것입니다:

 

  1. InvocationHandler

 

이건 또 뭘까요?

 

InvocationHandler는 우리의 객체에 대한 모든 메서드 호출을 가로채서 필요한 추가 동작을 첨가할 수 있는 특수 인터페이스를 말합니다.

 

우리는 우리만의 인터셉터를 만들어야 합니다. 이것은, 이 인터페이스를 구현하는 클래스를 만들어야 합니다.

 

매우 간단하게 만들 수 있습니다:

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

public class PersonInvocationHandler implements InvocationHandler {

private Person person;

public PersonInvocationHandler(Person person) {
   this.person = person;
}

 @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

       System.out.println("Hi!");
       return null;
   }
}
 

우리는 오직 하나의 인터페이스 메서드인 invoke()만 구현하면 됩니다. 그건 그렇고, 메서드는 우리가 필요로 하는 것을 수행합니다: 우리의 객체에 대한 모든 메서드 호출을 가로채고 필요한 동작을 추가합니다(invoke() 메서드 내에서 콘솔에 "Hi!"를 출력하는 것이죠).

 

2. 원본 객체와 해당 프록시.

 

우리는 원본 Man 객체와 이를 위한 "애드온"(프록시)을 생성합니다:

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());

   }
}
 

 

이것은 매우 간단해 보이지 않는군요!

 

각 코드 줄에 대해 구체적으로 주석을 추가했습니다. 무슨 일이 일어나고 있는지 자세히 살펴 봅시다.

 

첫 번째 줄에서는 프록시를 만들 원본 개체를 만드는 것입니다.

 

다음 두 줄로 인해 어려워 질 수 있을 것입니다:

// Get the class loader from the original object
ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();
 

사실 여기에서 특별한 일이 일어나는 것은 아닙니다. :)

 

네 번째 줄에서는 우리는 특수 Proxy 클래스와 정적 newProxyInstance() 메서드를 사용합니다:

// Create a proxy for our arnold object
Person proxyArnold 
  = (Person) Proxy.newProxyInstance
    (arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
 

이 메서드는 단순히 우리의 프록시 객체를 생성하는 것입니다.

 

마지막 단계에서 받은 원래 클래스에 대한 정보(ClassLoader 및 해당 인터페이스 목록)와 이전에 만든 InvocationHandler 객체를 메서드에 전달합니다.

 

가장 중요한 것은 원래 arnold 객체를 invocation handler에 전달하는 것을 잊지 않는 것입니다. 그렇지 않으면 "handle" 할 것이 없습니다 :)

 

우리는 무슨일을 했을까?

 

우리는 이제 프록시 객체인 proxyArnold를 가지게 되었습니다. Person 인터페이스의 모든 메서드를 호출할 수 있습니다. 왜 일까요?

 

여기에 모든 인터페이스 목록을 제공했기 때문입니다:

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();

// Create a proxy for our arnold object
Person proxyArnold = 
(Person) Proxy.newProxyInstance(
   arnoldClassLoader, 
   interfaces, 
   new PersonInvocationHandler(arnold));
 

이제 Person 인터페이스의 모든 메서드에 대해 알게 되었습니다.

 

게다가, arnold 객체와 함께 작동하도록 구성된 PersonInvocationHandler 객체를 프록시에 전달했습니다:

// Create a proxy for our arnold object
Person proxyArnold 
   = (Person) Proxy.newProxyInstance
      (arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
 

이제 프록시 객체에서 Person 인터페이스의 어떤 메서드를 호출하면 핸들러가 해당 호출을 가로채 자신의 invoke() 메서드를 대신 실행합니다.

 

main() 메서드를 실행해 봅시다!

 

콘솔 출력:

Hi!
 

훌륭하네요! 원래 Person.introduce() 메서드 대신 PersonInvocationHandler()의 invoke() 메서드가 호출되는 것을 볼 수 있습니다.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hi!");
   return null;
}
 

"Hi!" 메시지가 콘솔에 표시되지만 이는 정확히 우리가 원하는 동작은 아닙니다.

 

우리가 달성하려고 했던 것은 먼저 "Hi!"를 표시하고, 원본 메서드 자체를 호출하는 것입니다.

 

다시 말해서, 메서드 호출은

proxyArnold.introduce(arnold.getName());
 

단순히 "Hi!"가 아니라 "Hi! My name is Arnold"를 표시해야만 합니다.

 

이렇게 하려면 어떻게 해야 할까요? 방법은 그리 복잡하지 않습니다. 핸들러와 invoke() 메서드를 사용하여 약간의 자유를 얻으면 됩니다. :)

 

이 메서드에 어떤 인수가 전달되는지 주의 해야 합니다:

public Object invoke(Object proxy, Method method, Object[] args)
 

invoke() 메서드는 원래 호출된 메서드와 모든 인수(Method 메서드, Object[] args)에 액세스할 수 있습니다.

 

다른 말로, proxyArnold.introduce(arnold.getName()) 메서드를 호출하여 introduce() 메서드 대신에 invoke() 메서드를 호출하면 이 메서드 내에서 우리는 원래의 introduce() 메서드와 메서드의 인수 에 액세스할 수 있습니다.

 

결과적으로 다음과 같이 할 수 있습니다:

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

public class PersonInvocationHandler implements InvocationHandler {

   private Person person;

   public PersonInvocationHandler(Person person) {

       this.person = person;
   }

   @Override
   public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
       System.out.println("Hi!");
       return method.invoke(person, args);
   }
}
 

이제 우리는 invoke() 메서드에서 원래 메서드에 대한 호출을 추가했습니다.

 

이제 이전 예제의 코드를 실행하려고 하면 다음과 같습니다.

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold 
          = (Person) Proxy.newProxyInstance
               (arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());
   }
}
 

그러면 이제 모든 것이 제대로 작동하는지 확인할 수 있을 것입니다. :)

 

콘솔 출력:

Hi! My name is Arnold
 

언제 이런 방법이 필요 할까요? 사실, 꽤 자주 필요 합니다.

 

"동적 프록시" 디자인 패턴은 인기 있는 기술에서 활발히 사용됩니다... 아, 그런데 동적 프록시가 디자인 패턴이라는 것을 언급하는 것을 잊었군요!

 

축하합니다. 하나 더 배웠네요! :)

 

예를 들어, 보안과 관련된 대중적인 기술 및 프레임워크에서 활발히 사용됩니다.

 

프로그램에 로그인한 사용자만 실행해야 하는 20개의 메서드가 있다고 상상해 보십시오. 배운 기술을 사용하여 이러한 20가지 메서드에 각 메서드에서 확인 코드를 복제하지 않고 사용자가 유효한 자격 증명을 입력했는지 확인하는 검사를 쉽게 추가할 수 있습니다.

 

혹은 모든 사용자 작업이 기록되는 로그를 생성한다고 가정합니다. 이것은 또한 프록시를 사용하여 쉽게 수행할 수 있습니다.

 

지금도 위의 예제에 코드를 추가하면 invoke()를 호출할 때 메서드 이름이 표시되고 프로그램의 매우 간단한 로그가 생성됩니다. :)

 

결론적으로 한 가지 중요한 제한 사항에 주의하십시오.

 

프록시 개체는 클래스가 아닌 인터페이스와 함께 작동합니다. 하나의 인터페이스에 대한 하나의 프록시가 생성됩니다.

이 코드를 살펴보십시오:

// Create a proxy for our arnold object
Person proxyArnold 
   = (Person) Proxy.newProxyInstance
        (arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
 

여기에서 우리는 특히 Person 인터페이스를 위한 프록시를 생성한 것입니다.

 

만약 클래스에 대한 프록시를 만들려고 한다면, 즉 참조 유형을 변경해서 Man 클래스로 캐스트하려고 하면 동작하지 않을 것입니다.

Person proxyArnold = (Person) Proxy.newProxyInstance
   (arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

proxyArnold.introduce(arnold.getName());
 
Exception in thread "main" java.lang.ClassCastException: com.sun.proxy.$Proxy0 cannot be cast to Man 
 

인터페이스를 갖는 것은 절대적인 요구 사항입니다. 프록시는 인터페이스와 함께 동작합니다.

 

자, 이를 이용해서 이제 몇 가지 과제를 해결하면 좋을 것 같네요! :) 다음 시간까지!

 

이상.

 

728x90