[Velog ] Java Dynamic Proxy vs CGlib
What is a Proxy?Dynamic ProxyProxy를 런타임에 구현하는 방법 (JDK Dynamic Proxy)JpaRepository의 예CGLib(Code Generator Library)
What is a Proxy?
- 이미 존재하는 클래스에 어떤 기능을 추가하거나 수정할 때, 프록시 오브젝트를 만들어 사용함
- 프록시 오브젝트는 타겟 오브젝트 대신 클라이언트에서 사용됨
- 일반적으로 프록시 오브젝트는 타깃 오브젝트와 동일한 메소드를 가지며(같은 인터페이스 이므로), 자바 프록시 클래스에서는 대개 원본 클래스를 확장함
- 프록시는 원래 오브젝트(
target
)에 대한 제어권을 가지기 때문에 메소드를 호출 할 수 있다
프록시 클래스는 많은 것들을 원래 코드를 수정하지 않고 편리하게 구현할 수 있다
- 메소드가 시작하고 끝날 때 로그를 남김
- argument에 대한 추가적인 확인을 수행
- 원래 클래스의 행동을 모킹한다
- 비싼 자원에 대한 Lazy 접근을 실행
- 프록시는 사용 목적에 따라 두 가지로 구분할 수 있음
1. 클라이언트가 타깃에 접근하는 방법을 제어하기 위해서 →
Proxy pattern
데코레이터 패턴과 달리 기능을 확장하거나 추가하지는 않음
2. 타깃에 부가적인 기능을 부여해주기 위해서 →
Decorator Pattern
Proxy vs Proxy Pattern
일반적으로 부르는 Proxy는 실제 Target의 기능을 수행하면서 기능을 확장하거나 추가하는 실제 “객체”를 의미
Proxy Pattern은 실제로 Target에 대한 기능을 확장하거나 추가하지 않고 그저 클라이언트 타깃에 접근하는 방식을 변경해주는 역할을 함
Dynamic Proxy
- 런타임 중에 프록시 객체와 프록시 클래스가 생성되는 상황에만 적용되는 것이 아님에도 불구하고, Java에서 매우 흥미로운 주제임
- 이 주제는 reflection class, 바이트 코드 조작, 또는 동적으로 생성된 자바코드를 컴파일하는 등의 사용이 필요하기에 고급 주제이다.
- 런타임 중에 바이트코드가 이용이 불가능한 새 클래스를 가지려면 바이트코드 생성 혹은 바이트 코드를 로드할 수 있는 클래스 로더 또한 필요함
- 바이트코드 생성을 위해 CGLib과 bytebuddy, 내장 Java 컴파일러를 사용할 수 있음
- 프록시 클래스와 클래스가 호출하는 핸들러(
InvocationHandler
)를 생각해보면, 왜 책임의 분리가 중요한지 이해할 수 있음 - Proxy 클래스는 런타임에 생성되지만 Proxy 클래스에 의해 호출되는 핸들러는 일반 소스코드에 코딩되어 있고 전체 프로그램이 컴파일 될 때 같이 컴파일 되기 때문임
Proxy를 런타임에 구현하는 방법 (JDK Dynamic Proxy)
java.lang.reflection.Proxy
클래스 이용
- 해야할 일은 프록시 오브젝트가 호출할 수 있도록
java.lang.InvocationHandler
를 구현하는 것 InvocationHandler
인터페이스는invoke()
라는 단 하나의 메서드만 가짐invoke()
메서드 호출 시, argument = (프록시될 원본 오브젝트, 호출된 메서드(리플렉션Method
오브젝트), 원본 argument)
package proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class JdkProxyDemo { interface If { void originalMethod(String s); } static class Original implements If { @Override public void originalMethod(String s) { System.out.println(s); } } static class Handler implements InvocationHandler { private final If original; public Handler(If original) { this.original = original; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException { System.out.println("BEFORE"); method.invoke(original, args); System.out.println("AFTER"); return null; } } public static void main(String[] args){ Original original = new Original(); Handler handler = new Handler(original); If f = (If) Proxy.newProxyInstance( If.class.getClassLoader(), // 동적으로 생성되는 다이나믹 프록시 클래스의 로딩에 사용될 클래스 로더 new Class[] {If.class}, // 해당 Proxy가 가져야할 Interface 타입 handler // InvoacationHandler를 구현한 클래스 (부가기능 + 위임 역할 담당) ); f.originalMethod("Hallo"); } }


- 핸들러(
InvocationHandler
의 구현체)가 원본 객체에 대해 원본 메서드를 호출하려면, 그것에 대한 액세스 권한이 있어야 함. 원본 객체(Original,target
)가InvocationHandler
에 인수로 전달 Handler handler = new Handler(original);
이렇게 하면 각 원본 클래스에 대해 별도의 핸들러 객체를 이용할 수 있음
- 특별한 경우에 호출 핸들러와 원본 오브젝트가 없는 인터페이스 프록시를 만들 수 있음. 또한 소스코드 내에서 이 인터페이스를 구현하기 위한 클래스도 필요하지 않음. 동적으로 생성된 프록시 클래스는 인터페이스를 구현
JpaRepository의 예
package com.uplus.virtualoffice.domain; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.Test; public class ProxyTest { interface SimpleRepositoryInterface { User findById(Long id); void save(User user); } static class SimpleJpaRepository implements SimpleRepositoryInterface { private static Long key = 1L; private static final Map<Long, User> data = new HashMap<>(); @Override public User findById(Long id) { return data.get(id); } @Override public void save(User user) { data.put(key++, user); } } interface CustomRepository extends SimpleRepositoryInterface {} static class Handler implements InvocationHandler { private final SimpleRepositoryInterface original; public Handler(SimpleRepositoryInterface original) { this.original = original; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("Before"); Object ret = method.invoke(original, args); System.out.println("After"); return ret; } } @Test void name() { SimpleRepositoryInterface original = new SimpleJpaRepository(); Handler handler = new Handler(original); CustomRepository f = (CustomRepository)Proxy.newProxyInstance(CustomRepository.class.getClassLoader(), new Class[] {CustomRepository.class}, handler); f.save(new User("testuser@gmail.com", "asdfasdf123!", Role.ROLE_USER)); f.save(new User("testuser2@gmail.com", "asdfasdf123!", Role.ROLE_USER)); User user = f.findById(2L); System.out.println(user.getEmail()); } }

CGLib(Code Generator Library)
[Github ] CGLib
- 코드 생성 라이브러리, 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공함
- 순수 Java JDK 라이브러리를 이용하는 것이 아니므로 CGLib이라는 외부 라이브러리를 추가함으로써 사용할 수 있음
- 실제 CGLib의 Enhancer라는 클래스를 바탕으로 프록시를 생성하며, 인터페이스가 아닌 클래스에 대해서 동적 프록시를 생성할 수 있기 때문에 다양한 프로젝트에서 널리 사용되고 있음
- Hibernate는 자바빈 객체에 대한 프록시를 생성할 때 CGLib을 사용하며, Spring은 프록시 기반 Aop를 구현할 때 CGLib을 사용하고 있음
- CGLib 프록시는 Target Class를 상속받아 생성되기 때문에 개발자는 Proxy 생성을 위해 굳이 Interface를 만드는 수고를 덜 수 있게 됨
- ⇒ 상속을 이용하기에,
final
혹은private
메서드, 필드에 대해 오버라이딩 지원하지 않는 경우 Proxy에서 해당 메서드에 대한 Aspect를 적용할 수 없게 됨
- CGLib Proxy의 경우 실제 바이트 코드를 조작하여 JDK Dynamic Proxy 보다는 퍼포먼스가 상대적으로 빠른 장점이 있음