이전 글에서 Proxy
객체를 직접 생성하고, Proxy
객체를 직접 생성할 때 겪을 수 있는 불편함들에 대하여 간단히 알아보았다.
그 불편함들을 개선하기 위해 이번 글에서는 Dynamic Proxy
에 대해 알아보기를 원한다.
Dynamic Proxy
에 대한 개념을 이해하고 구현하는 방법 study
Dynamic Proxy
란 이전과 같이 Proxy
객체를 직접 생성을 하는 것이 아니라 Runtime(애플리케이션이 실행되는 중)에 Interface를 구현하는 Class or 인스턴스를 만들어내는 것을 이야기 한다.
Java java.lang.reflect.Proxy
package에서 제공해주는 API를 이용하여 Dynamic Proxy
를 이용할 수 있다.
API에서 제공해주는 Syntax는 다음과 같다.
public static Object newProxyInstance(
ClassLoader loader, // 1
Class<?>[] interfaces, // 2
InvocationHandler h // 3
) throws IllegalArgumentException
Proxy
객체 정의하기 위한 Class Loader
를 지정한다. (Proxy
객체가 구현할 Interface에 Class Loader
를 얻어오는 것이 일반적)
newProxyInstance()
를 통해 생성 될 Proxy
객체가 구현할 Interface를 정의한다.
메소드 호출을 디스패치하기 위한 호출 핸들러. (디스패치: 어떤 메소드를 호출할 것인가를 결정하여 그것을 실행하는 과정을 이야기함.)
위에 정의된 대로 매개변수를 지정하여 API를 호출하면 return 되는 값은 다음과 같다고 문서에서 정의하고 있다.
Returns: a proxy instance with the specified invocation handler of a proxy class that is defined by the specified class loader and that implements the specified interfaces
직역하자면 지정된 Class Loader
에 의해 정의된 Interface를 구현한 Proxy
객체의 InvocationHandler
를 가지고 있는 Object
타입의 Proxy
인스턴스라는 것이다.
위에서는 API의 스팩에 대하여 알아봤으니 한번 코드로 작성해보자.
Java에서 제공하는 Dynamic Proxy
는 Interface 기반이기 때문에 Interface를 정의한다.
public interface BookService {
void printTitle(Book book);
}
해당 Interface를 구현하는 구현 Class를 정의한다.
public class BookServiceImpl implements BookService{
@Override
public void printTitle(Book book) {
System.out.println("book.getTitle() = " + book.getTitle());
}
}
newProxyInstance()
를 이용하여 Interface기반의 Proxy
인스턴스를 생성한다.
public static void main(String[] args) {
// interface 기반으로 해당 구현체의 인스턴스를 생성하였다.
// (pure 자바에서 제공하는 reflect.Proxy는 Class 기반의 Proxy를 만들지 못한다.)
BookService bookService = (BookService) Proxy.newProxyInstance(
BookService.class.getClassLoader(),
new Class[]{BookService.class},
(proxy, method, arguments) -> {
BookService realSubject = new BookServiceImpl();
System.out.println("prev method invoke");
Object invoke = method.invoke(realSubject, arguments);
System.out.println("after method invoke");
return invoke;
});
// class com.sun.proxy.$Proxy0
System.out.println("bookService.getClass() = " + bookService.getClass());
Book book = new Book("foobar");
bookService.printTitle(book);
// output
// prev method invoke
// bookService.getClass() = foobar
// after method invoke
}
위의 예제 코드와 같이 java.lang.reflect.Proxy
에서 제공해주는 API를 이용하면 런타임에도 해당 Interface를 구현하는 Proxy객체의 인스턴스를 생성할 수 있다.
java.lang.reflect.Proxy
를 통해 Proxy 인스턴스를 생성할 때
주의점java.lang.reflect.Proxy
에서 제공하는 newProxyInstance()
를 이용할 때 Class
기반으로는 Proxy 객체를
생성할 수 없다는 것이다.
위의 예제코드를 조금 변경하여 실행해보면 다음과 같은 Exception
만날 수 있다.
// Exception in thread "main" java.lang.IllegalArgumentException:
// me.leewoooo.dynamicproxy.BookServiceImpl is not an interface
BookServiceImpl bookService = (BookServiceImpl) Proxy.newProxyInstance(
BookServiceImpl.class.getClassLoader(),
new Class[]{BookServiceImpl.class},
(proxy, method, arguments) -> {
//...
}
)
Error Message만 봐도 해당 API를 어떻게 사용해야 하는지 명백하게 알려주고 있다.
Spring을 이용할 때 Interface
를 구현하지 않은 Class들 또한 Proxy
객체로 Bean에 등록되는데 이건 어떻게 동작을 하는 것일까?
추 후 AOP를 공부할 때 더 자세하게 다루겠지만 Spring은 Bean을 등록할 때 Spring AOP
를 이용하여 등록을 하는데
Bean으로 등록하려는 기본적으로 객체가 Interface를 하나라도 구현하고 있으면 java.lang.reflect.Proxy
를 이용하고 Interface를 구현하고 있지 않으면 CGLIB
라는 외부 라이브러리를 사용한다.
이번 글을 토대로 Proxy Pattern
과 Dynamic Proxy
에 대해 알아보았다. Spring은
내부적으로 Proxy
기술을 정말 많이 사용하고 있다.(Spring AOP, Spring Data JPA 등등...)
Spring에서는 Bean을 등록할 때 Singleton
을 유지하기 위해 Proxy
기법을 이용해 Proxy
객체를 Bean으로 등록하는 것이고, Spring Data JPA에서는 JpaRepository
만 상속 받아도 해당 구현체를 만들어 Bean으로 등록해 주는 것도 Proxy
기법을 이용한 것이다. (이 외 사용되는 곳이 더 많음.)
이와 같이 Spring의 여러곳에서 전반적으로 Proxy
기법은 많이 사용된다.
https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Proxy.html
https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html