안녕! 오늘 우리는 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이 "안녕하세요!"라고 말하길 원한다는 것이죠. (무례한 사람을 좋아하는 사람은 아무도 없습니다) .
이 상황에서 우리가 해야 할 것은 무엇일까요?
우리는 몇 가지가 필요 할 것입니다:
- 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
인터페이스를 갖는 것은 절대적인 요구 사항입니다. 프록시는 인터페이스와 함께 동작합니다.
자, 이를 이용해서 이제 몇 가지 과제를 해결하면 좋을 것 같네요! :) 다음 시간까지!
이상.
'프로그래밍' 카테고리의 다른 글
네이티브 이미지의 동적 프록시 (0) | 2023.04.18 |
---|---|
언어별 - 프록시 디자인 패턴 (0) | 2023.04.16 |
[자바] 이미지 프로세싱 - 1 (0) | 2023.04.13 |
네이티브 이미지의 동적 프록시 (0) | 2023.04.10 |
초보자를 위한 클라우트 컴퓨팅 자습서 (1) | 2023.04.09 |