더 자바, 코드를 조작하는 다양한 방법를 듣고 정리한 글 입니다.
(지금 생각하면 비효율적인데) 작년에는 이론에 치우쳐서 공부하는 면이 있었다. 올해 인프런 라이브를 듣고 나서는 내 방향성이 틀렸구나, 생각이 많이 바뀌었다. (꼭 보세요 !)
최근 알게 된 다이나믹 프록시나 AOP가 설명을 읽어봤을 때 이해는 가지만, 찝찝한 느낌이 들어서 이참에 직접 코드를 쳐보며 익혀야겠다 싶었다. (그렇게 진행할 땐 개인적으로 백기선 님 강의가 잘 맞는 듯)
다이나믹 프록시를 다루기 전에 우선 프록시부터 알아야 한다.
프록시(Proxy)는 직역하자면 "대리"다. 뭔갈 대신한다는 것이다.
그렇다면 무엇을 대신한다는걸까? 실제 객체이다.
그렇다면 언제, 왜 실제 객체를 대신하는걸까?
핵심 기능과 구분되는 부가 기능을 추가할 때 실객체의 코드를 수정하지 않고 활용할 수 있기 때문이다. (로깅, 접근제한, 트랜잭션 처리 등)
실객체를 상속한 프록시 객체에 실객체를 target으로 가지고 있으면, 클라이언트는 프록시 객체를 통해 실객체를 호출할 수 있게 된다.
런타임 시 의존관계는 Client -> Proxy -> Real Object
이렇게 될 것이다.
✨프록시 추가 정리 (TIL)
// 프록시 클래스
public class BookServiceProxy implements BookService{
BookService bookService;
public BookServiceProxy(BookService bookService) {
this.bookService = bookService;
}
@Override
public void rent(Book book) {
System.out.println("------log start------");
bookService.rent(book);
System.out.println("------log end------");
}
}
프록시 클래스에는 앞뒤로 로그를 추가하는 단순한 기능의 메소드가 있다.
// 대상 객체
public class DefaultBookService implements BookService{
Book book = new Book();
@Override
public void rent(Book book) {
System.out.println("this book = " + book.getTitle());
}
}
// 테스트 코드
@Test
@DisplayName("직접 구현한 프록시 패턴 활용")
public void pureProxy() {
BookService bookService = new BookServiceProxy(new DefaultBookService());
Book book = new Book();
book.setTitle("real log printed");
bookService.rent(book);
}
이를 실행하면
프록시 객체를 통해 실행하면, 앞뒤로 로그를 추가한 결과가 잘 나오는 걸 볼 수 있다.
대상 클래스 수만큼 프록시 클래스를 만들어야 할 것이다.
백개의 클래스에 로깅을 붙여야 한다면? target 클래스만 다를 뿐인데 같은 비슷한 프록시 클래스를 계속 만들어야 할 것이다.
이러한 개발자의 수고를 덜기 위해 동적으로 프록시 객체를 생성 시켜주는 "다이나믹 프록시"를 활용할 수 있다.
@Test
@DisplayName("다이나믹 프록시 활용")
public void dynamicProxy() {
BookService proxyService = (BookService) Proxy
.newProxyInstance(BookService.class.getClassLoader(), new Class[]{BookService.class},
new InvocationHandler() {
BookService realService = new DefaultBookService();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("rent")) {
System.out.println("------log start------");
Object invoke = method.invoke(realService, args);
System.out.println("------log end------");
return invoke;
}
return method.invoke(realService, args);
}
});
Book book = new Book();
book.setTitle("real log printed");
proxyService.rent(book);
}
Proxy.newProxyInstance(클래스 로더, 대상 클래스, 핸들러 로직)
을 통해 프록시 객체를 만들 수 있다. 개발자는 핸들러 로직만 작성하면 된다.
다만 이때 대상은 무조건 인터페이스여야 한다.
BookService realService = new DefaultBookService(); // 인터페이스
DefaultBookService realService = new DefaultBookService(); // 구체 클래스
구체 클래스로 바꿀 시 동작하지 않는다.
클래스를 상속 받아서 프록시 객체를 동적으로 만들기 위해서는 CGLIB나 바이트코드를 조작하는 기술인 ByteBuddy를 활용할 수 있다.
구체적인 문법은 저도 잘 몰라서(..) 넘어가고, 바로 코드를 보면 유사한 방식으로 프록시 객체를 생성함을 알 수 있다.
@Test
@DisplayName("CGLIB로 클래스 기반의 프록시 만들기")
public void cglib() {
MethodInterceptor handler = new MethodInterceptor() {
DefaultBookService realService = new DefaultBookService(); // 구체클래스로 바꿔도 동작
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
if (method.getName().equals("rent")) {
System.out.println("******log start******");
Object invoke = method.invoke(realService, args);
System.out.println("******log end******");
return invoke;
}
return method.invoke(realService, args);
}
};
BookService proxyService = (BookService) Enhancer.create(BookService.class, handler);
Book book = new Book();
book.setTitle("real log printed");
proxyService.rent(book);
}
실행 결과:
@Test
@DisplayName("바이트버디 활용(바이트코드 조작) 프록시 만들기")
public void byteBuddy() throws Exception {
Class<? extends BookService> proxyClass = new ByteBuddy().subclass(BookService.class)
.method(named("rent")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
DefaultBookService realService = new DefaultBookService();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("//////log start//////");
Object invoke = method.invoke(realService, args);
System.out.println("//////log end//////");
return invoke;
}
}))
.make().load(BookService.class.getClassLoader()).getLoaded();
BookService proxyService = proxyClass.getConstructor(null).newInstance();
Book book = new Book();
book.setTitle("real log printed");
proxyService.rent(book);
}
실행 결과:
다만 이렇게 구체 클래스를 기반으로 프록시를 만드는 경우, final 클래스나 생성자가 private인 경우에는 불가능하다는 한계를 알아두자.