BackEnd/Java

[Java] Dynamic Proxy

샤아이인 2022. 6. 7.

 

1. Spring Data JPA는 어떻게 동작할까?

이번글의 가장 큰 주제는 Spring Data JPA에서 다음과 같은 코드들이 어떻게 구현체를 만드는지 이다!

public interface BookRepository extends JpaRepository<Book, Integer> {}

어떻게 interface만 정의했는데, 객체의 CRUD 가 작동할까?

도대체 누가 BookRepository 타입의 객체를 누가 만들어주는 것 일까!

 

이에 대한 핵심이 바로! Proxy 라는 class 이다!

Spring AOP를 기반으로 동작하며 RepositoryFactorySupport에서 프록시를 생성한다.

저 빨간 박스 안에서 Proxy를 생성하여 반환하여 DI 시켜준다!

(참고로 Proxy.class 같은 경우 java.lang.reflect 페키지 안에 들어있다.)

 

2. Proxy 패턴

우선 프록시 패턴에 대한 더 상세한 글을 내가 예전에 정리해둔 다음 글을 읽어보면 더 좋다.

https://blogshine.tistory.com/17

 

[Design Patterns] Proxy Pattern : 프록시 패턴

Proxy Pattern 이란? Proxy Pattern - 어떤 객체에 대한 접근을 제어하기 위한 용도로 대리인이나 대변인에 해당하는 객체를 제공하는 패턴 위의 정의에서 접근을 제어하는 프록시는 어떤 것 일까요? 아

blogshine.tistory.com

 

최종적으로 보이고 싶은 구조는 다음과 같다.

클라이언트는 요청을 보낼때 자신이 Real Subject에게 요청을 보내는줄 알지만, 사실은 Proxy에게 요청하고 있는 것 이다!

출처 - 인프런 백기선 더 자바

 

이번에는 간단하게 코드로 알아보자.  간단한 Book 이라는 class가 있다.

public class Book {

    private String title;

    public Book() {
    }

    public Book(String title) {
        this.title = title;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

이제 사용할 서비스는 다음과 같다.

public interface BookService {
    void rent(Book book);
}

이를 구현하는 구현체가 2개 있다!

각각 실재 객체와 프록시 이다.

public class DefaultBookService implements BookService{

    @Override
    public void rent(Book book) {
        System.out.println("대여 : " + book.getTitle());
    }
}
public class ProxyBookService implements BookService{

    private BookService bookService;

    public ProxyBookService(BookService bookService) {
        this.bookService = bookService;
    }

    @Override
    public void rent(Book book) {
        System.out.println("대여 시간 : " + LocalDateTime.now());
        bookService.rent(book);
        System.out.println("대여 기간은 7일 입니다.");
    }
}

이제 각각을 실행하는 테스트 코드는 다음과 같다.

class BookServiceTest {

    BookService defaultBookService = new DefaultBookService();
    BookService proxyBookService = new ProxyBookService(defaultBookService);

    @Test
    public void default_test() {
        Book book = new Book("defualt Book!");
        defaultBookService.rent(book);
    }

    @Test
    public void proxy_test() {
        Book book = new Book("proxy Book!");
        proxyBookService.rent(book);
    }
}

실행 결과는 다음과 같다.

대여 시간 : 2022-06-06T21:10:37.901231
대여 : proxy Book!
대여 기간은 7일 입니다.

대여 : defualt Book!

하지만 매번 이렇게 직접 Proxy를 만드는 것도 매우 큰 일이다.

이를 동적으로 Proxy를 만드는 기술을 동적 프록시(Dynamic Proxy)라고 부른다. 이에 대하여 알아보자!

 

3. Dynamic Proxy

이번시간에는 특정 Interface의 Proxy를 Runtime 때 만드는 방법에  대하여 알아보자.

Proxy는 Proxy.newProxyInatnace() 라는 메서드를 통해서 생성하게 된다.

 

이때 전달할 인자로는

1) classLoader

2) Proxy로 만들 대상 Interface

3) InvocationHandler() : Proxy의 메서드가 호출이 될때 어떻게 처리할지를 결정한다.

를 전달해야 한다.

 

우선 다음 사용 코드를 살펴보자.

class BookServiceTest {

    BookService bookService = (BookService) Proxy.newProxyInstance(BookService.class.getClassLoader(), new Class[]{BookService.class},
            new InvocationHandler() {
                BookService bookService = new DefaultBookService(); // 실재 객체가 필요함

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("대여 시간 : " + LocalDateTime.now());
                    Object invoke = method.invoke(bookService, args); // 호출되는 메서드
                    System.out.println("대여 기간은 7일 입니다.");
                    return invoke;
                }
            }
    );

    @Test
    public void proxy_test() {
        Book book = new Book("proxy Book!");
        bookService.rent(book);
    }
}

Ovverride한 invoke 메서드는 인자로 method를 받는데, 이는 Proxy를 통해 호출한 메서드를 가리킨다.

invoke(Object proxy, Method method, Object[] args)

첫번째 인자인 proxy는 해당 newProxyInatnace()로 생성한 proxy를 의미한다.

 

이후 realSubject에 해당하는 bookService를 인자로 넘기고, 나머지 args를 넘겨준다.

Object invoke = method.invoke(bookService, args);

즉, 위 코드는 realSubject에 있는 메서드를 호출하는 부분이다.

 

실행 결과는 다음과 같다.

 

▶ 동적프록시에는 단점이 몇가지 있다.

우선 단점중 하나는 모든 Proxy의 앞뒤에 대여 시간과, 대여 기간이 나오게 되어버린다.

왜냐하면 우리는 모든 메서드를 대상으로 앞뒤에 추가해줬기 때문이다.

잘 생각해보면 위 코드에서 method 인자를 통해 함수를 구분하거나 하는 일을 하지 않았었다.

 

따라서 메서드 별로 구분하려면 다음과 같이 코드를 작성해줘야 한다.

class BookServiceTest {

    BookService bookService = (BookService) Proxy.newProxyInstance(BookService.class.getClassLoader(), new Class[]{BookService.class},
            new InvocationHandler() {
                BookService bookService = new DefaultBookService();

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (method.getName().equals("rent")) { 
                        System.out.println("대여 시간 : " + LocalDateTime.now());
                        Object invoke = method.invoke(bookService, args); // 호출되는 메서드
                        System.out.println("대여 기간은 7일 입니다.");
                        return invoke;
                    }
                    
                    return method.invoke(bookService, args);
                }
            }
    );

    @Test
    public void proxy_test() {
        Book book = new Book("proxy Book!");
        bookService.rent(book);
    }
}

위 코든느 함수의 이름이 "rent"인 경우에만 대여시간과 대여일을 출력해주게 된다.

 

또한 다이나믹 프록시의 가장 큰 단점중 하나Class 기반의 Proxy를 만들수 없다는 점 이다.

반드시 Interface를 기반으로 Proxy를 만들어야 한다.

 

다음 단락에서 Class 기반의 Proxy를 만들어 보자.

 

소제목 입력

이번시간에는 Interface가 겂는 경우에 Proxy를 만드는 방법에 대하여 알아보자. 총 2가지 방법에 대하여 알아보자.

1. CGlib 사용

2. ByteBuddy 사용

 

우선 사용할 class는 다음과 같다.

public class DefaultBookService {

    public void rent(Book book) {
        System.out.println("대여 : " + book.getTitle());
    }
}

 

CGlib의 Enhander.creater라는 메서드를 사용하는데 handler가 필요하다.

Proxy 객체의 메서드를 호출할때 마다 어떤일을 해야하는지 알려주는 handler를 넘겨주어야 한다.

@Test
public void CGlib_test() {
    MethodInterceptor handler = new MethodInterceptor() {
        DefaultBookService bookService = new DefaultBookService();

        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
            System.out.println("대여 시간 : " + LocalDateTime.now());
            return method.invoke(bookService, args);
        }
    };

    DefaultBookService bookService = (DefaultBookService) Enhancer.create(DefaultBookService.class, handler);
    Book book = new Book("proxy Book!");
    bookService.rent(book);
}

출력 결과는 다음과 같다.

대여 시간 : 2022-06-07T07:44:23.616033
대여 : proxy Book!

 

이번에는 ByteBuddy를 통해 class의 proxy를 만들어보자.

원래의 class를 상속하는 subclass를 만든후 method이름으로 intercept하여 사전에 만든 InvocatoinHandler를 호출하게 된다.

@Test
public void byte_buddy_test() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Class<? extends DefaultBookService> proxyClass = new ByteBuddy().subclass(DefaultBookService.class)
            .method(named("rent")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
                DefaultBookService bookService = new DefaultBookService();

                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    System.out.println("대여 시간 : " + LocalDateTime.now());
                    Object invoke = method.invoke(bookService, args);
                    System.out.println("대여 기간은 7일 입니다.");
                    return invoke;
                }
            }))
            .make()
            .load(DefaultBookService.class.getClassLoader())
            .getLoaded();

    DefaultBookService bookService = proxyClass.getConstructor(null).newInstance();
    Book book = new Book("proxy Book!");
    bookService.rent(book);
}

출력 결과는 다음과 같다.

대여 시간 : 2022-06-07T07:58:19.861486
대여 : proxy Book!
대여 기간은 7일 입니다.

 

서브 클래스를 만드는 방법에도 단점은 있다.

1) 상속을 사용하지 못하는 경우 프록시를 만들수 없다.

  • Private 생성자만 있는 경우
  • Final 클래스인 경우

2) 인터페이스가 있을 때는 인터페이스의 프록시를 만들어 사용할 것.

'BackEnd > Java' 카테고리의 다른 글

[Java] 동일성(identity)과 동등성(equality)  (0) 2022.09.06
[Java] Annotation Processor  (0) 2022.06.08
[Java] Reflection  (0) 2022.05.22
[Java] 바이트코드 조작하기  (0) 2022.05.20
[Java] JVM 구조  (0) 2022.05.19

댓글