본문 바로가기

Java

Dynamic Proxy Classes

Proxy[Proxy Pattern]

Intent

프록시는 원본 객체에 대한 대체물이나 플레이스홀더를 제공하는 구조적 디자인 패턴입니다. 프록시는 원본 객체에 대한 접근을 제어하여, 원본 객체에 어떤 요청이 전달되기 전이나 후에 어떤 작업을 수행할 수 있게 합니다.





Problem

원본 객체에 대한 접근을 제어하고 싶은 이유는 무엇일까요? 예를 들어, 시스템 자원을 대량으로 소비하는 거대한 객체(예를 들어, 데이터베이스 관련)가 있습니다. 이 객체는 때때로 필요하지만, 항상 필요한 것은 아닙니다.

데이터베이스 쿼리는 정말 느릴 수 있습니다.




이 문제를 해결하기 위해 지연 초기화를 구현할 수 있습니다: 이 객체를 실제로 필요할 때만 생성합니다. 그러므로 이 거대한 객체를 사용하는 모든 클라이언트는 지연된 초기화 코드를 실행해야 합니다. 불행히도, 이 방법은 많은 코드 중복을 야기할 수 있습니다.
이상적인 세계에서는 이 코드를 객체의 클래스에 직접 넣고 싶겠지만, 항상 가능한 것은 아닙니다. 예를 들어, 해당 클래스가 폐쇄된 타사 라이브러리의 일부일 수 있습니다.

Solution

해결책으로 프록시 패턴이 있습니다.
프록시 패턴은 원본 서비스 객체와 동일한 인터페이스를 가진 새로운 프록시 클래스를 생성하도록 제안합니다. 그런 다음 애플리케이션을 업데이트하여 모든 원본 객체의 클라이언트에게 프록시 객체를 전달하도록 합니다. 클라이언트로부터 요청을 받으면, 프록시는 실제 서비스 객체를 생성하고 모든 작업을 프록시 객체에 위임합니다.

프록시는 자신을 데이터베이스 객체로 가장합니다. 프록시는 클라이언트나 실제 데이터베이스 객체가 알지 못하는 상태에서 지연 초기화와 결과 캐싱을 처리할 수 있습니다.


그렇다면 프록시 사용의 이점은 무엇일까요? 클래스의 주요 로직 실행 이전이나 이후에 무언가를 실행해야 하는 경우, 프록시를 사용하면 해당 클래스를 변경하지 않고도 이를 수행할 수 있습니다. 프록시가 원래 클래스와 동일한 인터페이스를 구현하기 때문에, 실제 서비스 객체를 필요하는 모든 클라이언트에게도 전달될 수 있습니다.

Real-World Analogy

신용카드는 현금과 마찬가지로 결제에 사용될 수 있습니다.


신용카드는 은행 계좌의 프록시이며, 현금 다발의 프록시입니다.
둘 다 동일한 인터페이스를 구현합니다: 둘 다 결제에 사용될 수 있습니다. 소비자는 많은 현금을 가지고 다닐 필요가 없어서 기분이 좋고, 상점 주인도 거래로 인한 수입이 은행 계좌에 은행 전산 시스템으로 추가되므로 예금을 잃거나 은행으로 가는 도중에 강도를 만날 위험이 없어서 행복합니다.

Structure



1. ServiceInterface 는 서비스의 인터페이스를 선언합니다. 프록시는 서비스 객체로 위장할 수 있도록 이 인터페이스를 구현해야 합니다.
2. Service 는 유용한 비즈니스 로직을 제공하는 클래스입니다.
3. Proxy 클래스는 Service 객체를 가리키는 참조 필드를 가지고 있습니다. Proxy가 처리를 마친 후(예: 지연 초기화, 로깅, 접근 제어, 캐싱 등), 요청을 Service 객체에 전달합니다.
    보통, Proxy는 Service 객체의 전체 수명 주기를 관리합니다.
4. Client는 Service와 Proxy를 동일한 인터페이스를 통해 작업해야 합니다. 이렇게 하면 Service 객체를 필요로 하는 모든 코드에 프록시를 전달할 수 있습니다.

Java Dynamic Proxy

자바의 동적 프록시(Dynamic Proxy)는 런타임에 특정 인터페이스들을 구현하는 클래스의 인스턴스를 생성하는 기능을 제공합니다.
 
자바의 다이나믹 프록시를 사용 이점:
1. 유연성: 다이나믹 프록시를 사용하면 런타임에 인터페이스를 구현하는 객체를 동적으로 생성할 수 있습니다. 이는 개발자가 다양한 시나리오에 맞춰 빠르게 조정할 수 있는 유연성을 제공합니다.
2. 코드 중복 감소: 프록시를 통해 공통 기능(예: 로깅, 인증, 에러 처리 등)을 한 곳에서 관리할 수 있습니다. 이는 코드 중복을 줄이고 유지 보수성을 향상시킵니다.
3. 디버깅 및 모니터링: 프록시를 사용하면 메소드 호출을 가로채어 디버깅 정보를 기록하거나 성능 모니터링을 수행할 수 있습니다. 이는 시스템의 동작을 이해하고 최적화하는 데 도움이 됩니다.
4. 보안 향상: 민감한 메서드에 대한 접근을 제어하거나 검증 로직을 추가하여 애플리케이션의 보안을 강화할 수 있습니다.
5. 테스트 용이성: 프록시를 사용하면 실제 객체를 테스트 더블(예: 모의 객체)로 대체하기 쉬워집니다. 이는 테스트의 복잡성을 줄이고 의존성을 관리하기 쉽게 해 줍니다.
6. AOP(관점 지향 프로그래밍) 지원: 다이나믹 프록시는 AOP를 구현하는 데 중요한 역할을 합니다. 특정 관심사를 분리하고 애플리케이션의 다른 부분에 영향을 주지 않고 재사용할 수 있게 해 줍니다.
7. 인터페이스 기반 프로그래밍 강화: 다이나믹 프록시는 인터페이스를 기반으로 동작하기 때문에 인터페이스 기반 프로그래밍을 더욱 강화하고 장려합니다.

이러한 이점들 덕분에, 자바의 다이나믹 프록시는 다양한 애플리케이션과 프레임워크에서 널리 사용되고 있으며, 개발자들에게 유용한 도구로 인식되고 있습니다.

공통 관심사란?
공통 관심사(Common Concerns 또는 Cross-Cutting Concerns)는 소프트웨어 개발에서 애플리케이션의 여러 부분에 걸쳐 나타나는, 중복되는 기능이나 책임을 말합니다. 이러한 관심사는 주요 비즈니스 로직과는 직접적으로 관련이 없지만, 애플리케이션의 전체적인 기능과 품질에 중요한 영향을 미칩니다. 

공통 관심사의 몇 가지 예는 다음과 같습니다:
1. 로깅: 시스템의 동작을 추적하기 위해 로그를 기록하는 기능입니다. 이는 거의 모든 부분에서 필요하지만, 주요 비즈니스 로직과는 별개의 기능입니다.
2. 인증 및 권한 부여: 사용자의 신원을 확인하고 특정 작업에 대한 권한을 확인하는 과정입니다. 이는 많은 서비스에서 필요하지만, 각 서비스의 핵심 기능과는 분리되어 있습니다.
3. 트랜잭션 관리: 데이터베이스 작업을 트랜잭션으로 관리하는 기능입니다. 이는 데이터 무결성을 보장하는 데 중요하지만, 개별 비즈니스 로직과는 분리됩니다.
4. 에러 처리: 시스템 전체에서 발생할 수 있는 예외나 에러를 처리하는 방법입니다. 에러 처리 로직은 모든 부분에서 필요하지만, 각 기능의 핵심 로직과는 별개입니다.
5. 성능 모니터링: 애플리케이션의 성능을 모니터링하고 최적화하는 기능입니다. 이는 전체적인 시스템 품질에 기여하지만, 개별 기능의 구현과는 다릅니다.

이러한 공통 관심사를 주요 비즈니스 로직과 분리하여 관리하는 것은 코드의 재사용성을 높이고, 유지 보수를 용이하게 하며, 애플리케이션의 구조를 더욱 명확하게 만드는 데 도움이 됩니다. 관점 지향 프로그래밍(Aspect-Oriented Programming, AOP)은 이러한 공통 관심사를 효과적으로 관리하는 데 자주 사용되는 프로그래밍 패러다임입니다.

 

Java Proxy 생성

Java에서 동적 프록시를 사용하는 과정은 다음과 같습니다:

1. 인터페이스 정의: 먼저, 프록시가 구현할 한 개 이상의 인터페이스를 정의합니다.

public interface Interface1 {
    void method1();
}

public interface Interface2 {
    void method2();
}


2. InvocationHandler 구현: InvocationHandler 인터페이스를 구현하는 클래스를 만듭니다. 이 클래스는 프록시 인스턴스를 통해 호출된 모든 메서드를 처리합니다.

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

public class MyInvocationHandler implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Method " + method.getName() + " is called");
        return null;
    }
}


3. 프록시 인스턴스 생성: Proxy.getProxyClass 메서드를 사용하여 프록시 클래스를 생성하고, 그 클래스의 인스턴스를 만듭니다. 이 과정에서 클래스 로더와 인터페이스 배열을 매개변수로 제공합니다. 생성된 프록시 인스턴스는 지정된 인터페이스들을 구현합니다.

import java.lang.reflect.Proxy;

public class ProxyExample {
    public static void main(String[] args) {
        ClassLoader classLoader = ProxyExample.class.getClassLoader();
        Class<?>[] interfaces = new Class<?>[] { Interface1.class, Interface2.class };

        Object proxyInstance = Proxy.newProxyInstance(
            classLoader,
            interfaces,
            new MyInvocationHandler()
        );

        Interface1 if1 = (Interface1) proxyInstance;
        Interface2 if2 = (Interface2) proxyInstance;

        if1.method1();
        if2.method2();
    }
}


4. 제한 사항 고려: 프록시 클래스를 생성할 때, 인터페이스 배열에 있는 모든 클래스 객체들은 인터페이스를 나타내야 하며, 이 배열에는 동일한 `Class` 객체를 참조하는 요소가 없어야 합니다. 또한, 이들 인터페이스는 모두 지정된 클래스 로더를 통해 이름으로 볼 수 있어야 하며, 비공개 인터페이스의 경우 동일한 패키지 내에 있어야 합니다. 

5. 중복 메서드 처리: 같은 시그니처를 가진 여러 인터페이스의 메서드에 대해서는, 이들 중 하나가 다른 모든 메서드의 반환 타입에 할당 가능한 타입을 가져야 합니다. 원시 타입이나 void를 반환하는 경우, 모든 메서드가 같은 반환 타입을 가져야 합니다.

6. 프록시 클래스의 고유성: 인터페이스 배열의 순서가 프록시 클래스의 고유성에 영향을 줍니다. 같은 인터페이스 조합이라도 순서가 다르면 다른 프록시 클래스가 생성됩니다.

위 코드는 Java의 리플렉션 API와 동적 프록시를 사용하여 인터페이스 기반의 프록시 객체를 생성하고 사용하는 간단한 예시를 보여줍니다. 이러한 기법은 객체 지향 프로그래밍에서 다양한 디자인 패턴, 특히 동적 인터페이스 구현, 메서드 호출 로깅, 가상 프록시, 보안 프록시 등에 유용하게 쓰입니다. 


타겟 객체를 포함한 프록시

위에서 제공된 예제에는 프록시의 타겟 클래스 객체가 포함되어 있지 않습니다.
동적 프록시는 일반적으로 타겟 객체를 감싸서 특정 작업(예: 로깅, 인증, 트랜잭션 처리 등)을 수행한 후, 실제 타겟 객체에 호출을 위임[전달]합니다. 이를 반영한 보다 완성된 예제를 제공하겠습니다.

1. 인터페이스 정의: 이전과 같습니다.

2. 타겟 클래스 구현: 이 인터페이스를 구현하는 실제 클래스를 만듭니다.

public class MyInterfaceImpl implements MyInterface {
    @Override
    public void doSomething() {
        System.out.println("Doing something in the target object");
    }
}


3. InvocationHandler 구현 개선: 이제 InvocationHandler는 타겟 객체에 대한 참조를 유지하고, 메서드 호출을 해당 객체로 전달합니다.

public class MyInvocationHandler implements InvocationHandler {
    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("Before method " + method.getName());
        Object result = method.invoke(target, args);
        System.out.println("After method " + method.getName());
        return result;
    }
}

 
4. 프록시 생성 및 사용 개선: 이제 타겟 객체를 프록시에 연결합니다.

public class ProxyExample {
    public static void main(String[] args) {
        MyInterface realObject = new MyInterfaceImpl();
        MyInterface proxyInstance = (MyInterface) Proxy.newProxyInstance(
            MyInterface.class.getClassLoader(),
            new Class[] { MyInterface.class },
            new MyInvocationHandler(realObject)
        );

        proxyInstance.doSomething();
    }
}


이 예제에서는 MyInterfaceImpl이 실제 작업을 수행하는 타겟 객체이며, MyInvocationHandler는 이 타겟 객체를 감싸서 메서드 호출 전후에 추가 작업을 수행합니다. Proxy.newProxyInstance를 사용하여 프록시 인스턴스를 생성하고, 이를 통해 메서드를 호출하면 InvocationHandler가 정의한 로직이 실행됩니다.

두 개의 타겟을 프록시하는 예제

그림 1.

 

그림 2.


 아래 코드 구현은 클래스 그림1을 실현합니다:

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

public class JavaDynamicProxyApp {
    
    public static void main(String[] args) {
        
        Person person = new Person("HauChee");
        Animal animal = new Animal();
        Object proxy = Proxy.newProxyInstance(
                MyInvocationHandler.class.getClassLoader(),
                new Class[] {IPerson.class, IAnimal.class},
                new MyInvocationHandler(person, animal));
        
        System.out.println(proxy instanceof Object); // output: true
        System.out.println(proxy instanceof IPerson); // output: true
        System.out.println(proxy instanceof IAnimal); // output: true
        
        /**
         * Proxy class implements the interfaces not the concrete class
         * therefore, any public method in Person such as think() will never
         * be invoked from proxy instance.
         */
        System.out.println(proxy instanceof Person); // output: false
        System.out.println(proxy instanceof Animal); // output: false
        
        IPerson proxiedPerson = (IPerson) proxy;
        proxiedPerson.getName(); // output: Intercepted.. Person name..
        proxiedPerson.eat(); // output: Intercepted.. Person eat..
        
        IAnimal proxiedAnimal = (IAnimal) proxy;
        proxiedAnimal.eat(); // output: Intercepted.. Person eat.. 
        /**
         * WAIT A MINUTE! ISN't IT SHOULD SHOW "Animal eat.." INSTEAD?
         * 
         * Although eat() method is called based on IAnimal interface
         * but because there is a duplicate method eat() in IPerosn
         * therefore Method object passed into MyInvocationHandler.invoke()
         * method always take from the foremost interface which is IPerson
         * in this case.
         */
    }
}

class MyInvocationHandler implements InvocationHandler {
    
    private Object proxiedPerson;
    private Object proxiedAnimal;

    public MyInvocationHandler(Object person, Object animal) {
        this.proxiedPerson = person;
        this.proxiedAnimal = animal;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        System.out.print("Intercepted.. ");
        if (method.getDeclaringClass() == IPerson.class) {
            // Invoke real method of Person object
            return method.invoke(proxiedPerson, args); 
        }
        // Invoke real method of Animal object
        return method.invoke(proxiedAnimal, args); 
    }
}

class Person implements IPerson {
    
    private String name;
    
    Person(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        System.out.println("Person name..");
        return name;
    }

    @Override
    public void eat() {
        System.out.println("Person eat..");
    }
    
    public void think() {
        System.out.println("Person think..");
    }
}

class Animal implements IAnimal {

    @Override
    public void eat() {
        System.out.println("Animal eat..");
    }
}

interface IPerson {
    String getName();
    void eat();
}

interface IAnimal {
    void eat();
}

 

조건에 따른 타겟 메소드 호출

특정 스펠링으로 시작되는 메소드만 호출하는 프록시를 구현하기 위해, InvocationHandler를 사용하여 조건을 검사하고 해당 조건을 만족하는 경우에만 타겟 메소드를 호출하도록 구현할 수 있습니다. 예를 들어, 메소드 이름이 "perform"으로 시작하는 경우에만 해당 메소드를 호출하도록 하겠습니다.

// 인터페이스 정의
public interface MyInterface {
    void performAction();
    void performAnotherAction();
    void otherAction();
}

// MyInterface 구현
public class MyImplementation implements MyInterface {
    @Override
    public void performAction() {
        System.out.println("Performing Action");
    }

    @Override
    public void performAnotherAction() {
        System.out.println("Performing Another Action");
    }

    @Override
    public void otherAction() {
        System.out.println("Other Action");
    }
}

 

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

public class MyInvocationHandler implements InvocationHandler {
    private final Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 메소드 이름이 "perform"으로 시작하는 경우에만 타겟 메소드 호출
        if (method.getName().startsWith("perform")) {
            System.out.println("Invoking " + method.getName());
            return method.invoke(target, args);
        } else {
            System.out.println(method.getName() + " is not allowed");
            return null; // 또는 적절한 처리
        }
    }
}
import java.lang.reflect.Proxy;

public class ProxyExample {
    public static void main(String[] args) {
        // 원래의 객체 생성
        MyInterface originalObject = new MyImplementation();

        // 동적 프록시 생성
        MyInterface proxyObject = (MyInterface) Proxy.newProxyInstance(
                MyInterface.class.getClassLoader(),
                new Class[]{MyInterface.class},
                new MyInvocationHandler(originalObject));

        // 프록시 객체를 통해 메소드 호출
        proxyObject.performAction(); // 이 메소드는 호출됩니다
        proxyObject.performAnotherAction(); // 이 메소드도 호출됩니다
        proxyObject.otherAction(); // 이 메소드는 호출되지 않습니다
    }
}

 

이 코드를 실행하면, performAction과 performAnotherAction 메소드는 호출되지만 otherAction 메소드는 호출되지 않습니다. MyInvocationHandler의 invoke 메소드에서 메소드 이름을 체크하여 "perform"으로 시작하지 않는 메소드는 실행하지 않도록 구현되어 있기 때문입니다.

 

Java Dynamic Proxy의 특징

Java Dynamic Proxy의 특징은 다음과 같습니다:

1. 인터페이스 메소드와 Object 클래스의 메소드만 가로챌 수 있음: 주어진 인터페이스에 선언된 메소드와 java.lang.Object의 메소드들(예: hashCode(), equals(), toString())만 프록시 인스턴스를 통해 가로챌 수 있습니다. 프록시된 클래스의 다른 public 메소드는 무시됩니다. 이는 프록시 인스턴스를 통해 다른 public 메소드를 호출할 방법이 없기 때문입니다. 예를 들어, 그림 1.0에서 Person의 think() 메소드는 프록시 범위 밖에 있습니다.
2. 프록시 인스턴스와 프록시된 인스턴스 사이의 명확한 구분: InvocationHandler 인스턴스는 이들 사이의 다리 역할을 합니다. 하나 이상의 프록시된 객체를 가질 수 있습니다. 결국 프로그래머가 어떻게 그리고 어떤 프록시된 객체의 실제 메소드를 호출할지 결정하기 때문입니다.
3. 프록시 인스턴스와 연관된 InvocationHandler 인스턴스는 하나뿐: InvocationHandler의 유일한 메소드인 invoke()는 단순하고 원시적입니다. InvocationHandler의 기능을 확장하는 것은 개발자의 임무입니다.

이러한 특징들은 Java Dynamic Proxy가 어떻게 작동하는지 이해하는 데 중요하며, 이를 활용하여 효과적인 프록시 기반 프로그래밍을 할 수 있게 합니다.
 

다이내믹 프록시와 스프링 팩토리빈

클래스 정보의 불확실성

다이내믹 프록시 객체의 클래스 타입은 런타임에 결정되며, 이는 사전에 정확히 알 수 없습니다. 따라서, 컴파일 타임에 클래스 타입을 알아야 하는 스프링 빈 정의에서는 다이내믹 프록시 객체를 직접 생성하고 관리하기 어렵습니다.


스프링 빈 팩토리의 한계

  • 스프링 빈 정의: 스프링에서 빈(bean)은 일반적으로 클래스 타입과 생성 방법이 미리 정의된 객체입니다. 이들은 빈 팩토리(bean factory)에 의해 관리되고, 애플리케이션에서 필요할 때 인스턴스화됩니다.
  • 정의된 클래스 타입 필요: 스프링 빈을 정의할 때는 해당 빈의 클래스 타입을 명시해야 합니다. 이는 스프링이 빈의 인스턴스를 생성하고 관리하는 데 필요한 정보입니다.

다이내믹 프록시의 생성

  • Proxy.newProxyInstance 메소드 사용: 자바의 다이내믹 프록시는 Proxy 클래스의 스태틱 메소드인 newProxyInstance를 통해 생성됩니다. 이 메소드는 런타임에 프록시 객체를 생성하며, 해당 객체는 주어진 인터페이스를 구현합니다.
  • 스프링 빈으로의 직접 등록 불가: 이러한 방식으로 생성된 다이내믹 프록시 객체는 스프링 빈 팩토리에서 직접 생성되고 관리될 수 없습니다. 왜냐하면 그 클래스 타입이 런타임에만 결정되므로, 스프링 빈 정의 시 필요한 클래스 정보를 사전에 알 수 없기 때문입니다.

    다이내믹 프록시의 클래스 타입은 런타임에만 결정되기 때문에, 스프링 빈 정의에서는 이를 미리 알 수 없어 스프링 빈으로 등록하는 것이 불가능합니다. 따라서 다이내믹 프록시는 Proxy.newProxyInstance 메소드를 사용하여 런타임에만 생성됩니다.

'Java' 카테고리의 다른 글

All about JAVA  (0) 2024.04.08
Class Loader  (0) 2024.04.08
Reflection  (0) 2024.04.08
JVM  (0) 2024.04.08
자바 클래스 파일(feat. compiler)  (0) 2024.04.08