다이나믹 프록시

개발새발log·2023년 3월 6일
0

Java/Spring

목록 보기
6/6

더 자바, 코드를 조작하는 다양한 방법를 듣고 정리한 글 입니다.

(지금 생각하면 비효율적인데) 작년에는 이론에 치우쳐서 공부하는 면이 있었다. 올해 인프런 라이브를 듣고 나서는 내 방향성이 틀렸구나, 생각이 많이 바뀌었다. (꼭 보세요 !)

최근 알게 된 다이나믹 프록시나 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를 활용할 수 있다.

구체 클래스를 상속받은 프록시 객체 만들기

구체적인 문법은 저도 잘 몰라서(..) 넘어가고, 바로 코드를 보면 유사한 방식으로 프록시 객체를 생성함을 알 수 있다.

  • CGLIB를 활용해 프록시 객체 생성
@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인 경우에는 불가능하다는 한계를 알아두자.

profile
⚠️ 주인장의 머릿속을 닮아 두서 없음 주의 ⚠️

0개의 댓글