자바 동적 프록시(Dynamic Proxy) 란?
포스트
취소

자바 동적 프록시(Dynamic Proxy) 란?

proxy

동적 프록시(Dynamic Proxy)란?

  • 자바 동적 프록시는 자바에서 객체의 인터페이스를 구현하여 런타임 시에 객체의 메서드 호출을 제어하는 기술.
  • 대부분의 사용자가 자신도 모르게 사용하고 있으며, AOP(Aspect-Oriented Programming)와 같은 기술의 핵심 요소로 활용된다

동적 프록시의 개념

  • 동적 프록시는 ‘프록시(Proxy)’라는 용어에서 시작한다
  • 프록시는 대리자 또는 중계자라는 의미로, 어떤 객체를 대신하여 그 객체의 기능을 제공한다
  • 자바에서 프록시 객체는 대상 객체와 같은 인터페이스를 구현하며, 대상 객체의 메서드 호출을 중계하는 역할을 한다
  • “전화 응답 서비스”에 비유해 볼 수 있다. 예를 들어, 어떤 회사에 전화를 걸었을 때, 직접 관련된 사람과 바로 통화하는 것이 아니라 대리자(Proxy)가 대신 수신받는다. 대리자는 누가 전화를 걸었는지, 어떤 요청을 하는지를 먼저 확인하고, 그 다음에 적절한 부서나 사람에게 전화를 연결해 준다.
    • 동적 프록시는 비슷한 역할을 한다. 프로그램에서 어떤 메소드(작업)를 호출하려고 할 때, 동적 프록시가 먼저 이 호출을 받고, 프록시는 이 호출에 대한 정보를 분석하고, 필요한 경우 추가 작업을 수행한 후, 실제 메소드로 요청을 전달한다. 이 과정을 통해, 개발자는 보안 검사, 로깅, 트랜잭션 처리 등의 공통적인 기능들을 메소드 호출에 쉽게 추가할 수 있다

프록시의 기본 원리

  • 메서드 호출 중계 : 대상 객체의 메서드를 호출할 때, 프록시 객체를 거쳐 메서드가 호출된다.
  • 추가 기능 구현 : 프록시 객체는 원래 객체의 기능에 추가적인 기능을 수행할 수 있다.

동적 프록시의 특징

  • 동적 프록시 객체는 프로그램 실행 중에 생성된다
  • 하나의 프록시 클래스로 여러 인터페이스의 구현체를 대신할 수 있다
  • 공통 기능을 프록시 클래스에 구현하여 코드 중복을 줄일 수 있다

동적 프록시의 사용 사례

  • AOP(Aspect-Oriented Programming)
    • 트랜잭션 관리 : 메서드 실행 전후로 트랜잭션을 시작하고 커밋 또는 롤백한다
    • 로깅 및 모니터링 : 메서드 실행 정보를 로깅하거나 성능을 모니터링한다
  • 리모트 메서드 호출(Remote Method Invocation, RMI)
    • 네트워크 통신 : 원격 객체에 대한 메서드 호출을 네트워크를 통해 전달한다
  • 리소스 관리
    • 자원 해제 자동화 : 객체의 메서드 실행 후 자동으로 리소스를 해제한다

동적 프록시 구현 방법

  • 자바에서 동적프록시를 구현하는 방법은 크게 두 가지가 있다.
    • 인터페이스 기반 동적 프록시 : 주로 java.lang.reflect.Proxy 클래스와 java.lang.reflect.InvocationHandler 인터페이스를 사용하여 동적 프록시를 구현한다
    • 클래스 기반 동적 프록시 : 주로 CGLib 라이브러리를 사용하여 동적 프록시를 구현한다

인터페이스 기반 동적 프록시

InvocationHandler 인터페이스

  • 대상 객체의 메서드 호출을 처리하는 인터페이스로, invoke 메서드를 오버라이딩하여 메서드 호출 시 수행할 작업을 정의한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Java Reflect API를 이용한 InvocationHandler 구현 예제
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class DynamicProxyHandler implements InvocationHandler {
    private Object target;

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 메서드 호출 전 처리할 작업
        System.out.println("[Proxy] Before method: " + method.getName());

        // 실제 객체의 메서드 호출
        Object result = method.invoke(target, args);

        // 메서드 호출 후 처리할 작업
        System.out.println("[Proxy] After method: " + method.getName());

        return result;
    }
}

Proxy 클래스

  • 동적 프록시 객체를 생성하는 클래스로, newProxyInstance 메서드를 사용하여 프록시 객체를 생성한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Proxy 클래스를 이용한 동적 프록시 객체 생성 예제
import java.lang.reflect.Proxy;

public class DynamicProxyDemo {
    public static void main(String[] args) {
        // 원본 객체 생성
        MyInterface original = new MyInterfaceImpl();

        // 프록시 객체 생성
        MyInterface proxy = (MyInterface) Proxy.newProxyInstance(
                MyInterface.class.getClassLoader(),
                new Class<?>[] { MyInterface.class },
                new DynamicProxyHandler(original)
        );

        // 프록시 객체를 통한 메서드 호출
        proxy.someMethod();
    }
}

interface MyInterface {
    void someMethod();
}

class MyInterfaceImpl implements MyInterface {
    @Override
    public void someMethod() {
        System.out.println("Executing someMethod");
    }
}

실행 결과

1
2
3
[Proxy] Before method: someMethod
Executing someMethod
[Proxy] After method: someMethod

클래스 기반 동적 프록시 (GCLib)

  • CGLib는 코드 생성 라이브러리로, 자바 클래스의 바이트 코드를 조작하여 서브 클래스를 동적으로 생성하는 기능을 제공한다
    • 동적 프록시 외에도 많은 강력한 기능들이 있다
  • CGLib을 사용하면 인터페이스가 아닌 클래스를 상속받아 프록시를 생성할 수 있다
  • 외부 라이브러리를 사용하므로, 별도의 의존성 추가가 필요하다
    • implementation group: 'cglib', name: 'cglib'

MethodInterceptor 인터페이스

  • 대상 객체의 메서드 호출을 가로채는 인터페이스로, intercept 메서드를 구현하여 메서드 호출 시 수행할 작업을 정의한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// CGLib를 이용한 MethodInterceptor 구현 예제
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class CGLibDynamicProxy implements MethodInterceptor {
    private Object target;

    public Object createProxy(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        // 메서드 호출 전 처리할 작업
        System.out.println("[Proxy] Before method: " + method.getName());

        // 실제 객체의 메서드 호출
        Object result = proxy.invokeSuper(obj, args);

        // 메서드 호출 후 처리할 작업
        System.out.println("[Proxy] After method: " + method.getName());

        return result;
    }
}

Enhancer 클래스

  • CGLib 프록시 객체를 생성하는 클래스로, create 메서드를 사용하여 프록시 객체를 생성한다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// CGLib를 이용한 동적 프록시 객체 생성 예제
public class CGLibDemo {
    public static void main(String[] args) {
        // 원본 객체 생성
        MyClass original = new MyClass();

        // CGLib 프록시 객체 생성
        MyClass proxy = (MyClass) new CGLibDynamicProxy().createProxy(original);

        // 프록시 객체를 통한 메서드 호출
        proxy.someMethod();
    }
}

class MyClass {
    public void someMethod() {
        System.out.println("Executing someMethod");
    }
}

실행 결과

1
2
3
[Proxy] Before method: someMethod
Executing someMethod
[Proxy] After method: someMethod

만약 자바 모듈을 사용할 경우 다음과 같은 에러가 발생할 수 있다

java.lang.reflect.InaccessibleObjectException-->Unable to make protected final java.lang.Class java.lang.ClassLoader.defineClass(java.lang.String,byte[],int,int,java.security.ProtectionDomain) throws java.lang.ClassFormatError accessible: module java.base does not "opens java.lang"

이 에러는 CGLib와 같은 라이브러리가 Java의 내부 API에 접근하려고 할 때 발생하며, 이 문제를 해결하려면 다음 옵션을 JVM에 옵션을 추가해야한다

--add-opens java.base/java.lang=ALL-UNNAMED

동적 프록시의 장단점

장점

  • 다양한 타입의 객체에 대해 동일한 프록시 클래스를 사용할 수 있어 유연한 코드 작성이 가능해진다
  • 공통적인 기능을 프록시에서 처리하여 중복을 줄일 수 있다
  • 공통 기능 변경 시 프록시 클래스만 수정하면 되므로 유지보수가 용이해질 수 있다

단점

  • 프록시의 사용으로 인해 시스템의 복잡성이 증가할 수 있다
  • 추가적인 메서드 호출과 리플렉션 사용으로 인한 성능 저하가 발생할 수 있다
    • 현대의 컴퓨터 성능과 JVM 최적화로 인해 어느정도 무시해도 되는 수준이긴 하다
  • 인터페이스 기반 동적 프록시를 사용할 경우, 인터페이스를 사용하는 클래스 구조로 설계해야한다
  • 클래스 기반 동적 프록시를 사용할 경우, 추가적인 라이브러리 의존성이 발생한다 (CGLib)

마치며

자바 동적 프록시의 개념과 그 활용법에 대해 이해하고 적절히 활용한다면, 보다 효율적이고 강력한 프로그래밍이 가능해진다

동적프록시를 처음 접해봤다면 이해하는 데 어려움을 겪을 수도 있겠지만, 이해하고 나면 매우 강력하고 유용한 도구라는 것을 알게 될 것이다

항상 새로운 기술을 배우고 적용하는 것은 쉽지 않지만 깊은 이해를 위해 동적 프록시를 적극 활용하게 된다면, 그 과정에서 얻는 지식과 경험은 분명히 개발 역량을 한층 더 끌어올릴 것이다

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.

자바의 Record 란?

컴퓨터의 음수표현과 2의 보수

Comments powered by Disqus.