티스토리 뷰

Java

프록시 패턴(Proxy Pattern)

J-Mandu 2022. 4. 5. 09:00

출처 :https://www.inflearn.com/course/the-java-code-manipulation

프록시 패턴이란?

리얼 서브젝트는 자신이 해야 할 일만 하면서 프록시를 사용하여 부가적인 기능을 제공할 때 쓰는 패턴입니다.

프록시와 리얼 서브젝트가 공유하는 인터페이스(서브젝트)가 있고,

클라이언트가 해당 인터페이스(서브젝트) 타입으로 프록시를 사용합니다.

클라이언트는 프록시를 거쳐서 리얼 서브젝트를 사용하기 때문에 프록시는

리얼 서브젝트에 대한 접근을 관리하거나 부가기능을 제공하거나, 리턴값을 변경할 수도 있습니다.

 

그럼 프로젝트를 생성해서 간단히 살펴보겠습니다.

package me.whiteship;

public class Book {

    private Integer id;

    private String title;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

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

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

서브젝트

package me.whiteship;

public class BookServiceProxy implements  BookService {

    BookService bookService;

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

    @Override
    public void rent(Book book) {
        System.out.println("aaaaa");
        bookService.rent(book);
        System.out.println("bbbbb");
    }
}

프록시

package me.whiteship;

public class DefaultBookService implements BookService {

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

리얼 서브젝트

package me.whiteship;

import org.junit.Test;
import static org.junit.Assert.*;

public class BookServiceTest {
    BookService bookService = new DefaultBookService();
    BookService bookServiceProxy = new BookServiceProxy(new DefaultBookService());

    @Test
    public void noProxy() {
        Book book = new Book();
        book.setTitle("spring");
        bookService.rent(book);
    }

    @Test
    public void proxy() {
        Book book = new Book();
        book.setTitle("spring");
        bookServiceProxy.rent(book);
    }
}

noProxy 메소드는 spring이 출력되며,

proxy() 예상대로

aaaaa

rent : spring

bbbbb

이렇게 출력됩니다.

이런 식으로 프록시 패턴을 구현할 수도 있으며

다른 방법으로 자바에서 지원하는 Dynamic Proxy를 살펴보겠습니다.

 

Dynamic Proxy란?

Runtime에 특정 interface들을 구현하는 class 또는 instance를 만드는 기술입니다.

애플리케이션은 다이나믹 프록시를 사용하여 여러 임의의 이벤트 인터페이스를 구현하는 객체를 만들 수 있습니다.

Object Proxy.newProxyInstance(ClassLoader, Interfaces, InvocationHandler)

https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html

 

실습을 위해 프로젝트를 생성하고 살펴보겠습니다.

package me.whiteship;

public class Book {

    private Integer id;

    private String title;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

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

public interface BookService {

    void rent(Book book);

    void returnBook(Book book);
}
package me.whiteship;

public class DefaultBookService implements BookService {

    @Override
    public void rent(Book book) {

        System.out.println("rent : " + book.getTitle());
    }

    @Override
    public void returnBook(Book book) {
        System.out.println("return : "+ book.getTitle());
    }
}
package me.whiteship;

import org.junit.Test;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import static org.junit.Assert.*;

public 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("aaaa");
                        Object invoke = method.invoke(bookService, args);
                        System.out.println("bbbb");
                        return invoke;
                    }
                    return method.invoke(bookService, args);
                }
            });

    @Test
    public void dynamicProxy() {
        Book book = new Book();
        book.setTitle("spring");
        bookService.rent(book);
        bookService.returnBook(book);
    }
}

Proxy.newProxyInstance를 사용하여 이전의 실습한 BookServiceProxy 클래스를 구현한 코드입니다.

dynamicPorxy 메소드의 결과값은 

aaaa

rent : spring

bbbb

return : spring 이렇게 출력됩니다.

 

BookService의 타입을 DefaultBookService로 바꾸면 에러가 납니다.

그 이유는 Interface 기반의 proxy는 생성할 수 있지만, class 기반의 proxy는 생성 할 수가 없기 때문입니다.

따라서, java proxy는 무조건 interface를 써야 한다는 제약 조건이 있습니다.

 

그러면 class기반의 proxy가 필요할 경우 어떻게 해야 할까요?

class기반의 proxy는 서브 클래스를 만들 수 있는 라이브러리를 사용하여 proxy를 만들 수 있습니다.

 

서브 클래스를 만드는 방법의 단점을 살펴보겠습니다.

  • 상속을 사용하지 못하는 경우 프록시를 만들 수 없습니다.
    • private 생성자만 있는 경우
    • Final 클래스인 경우

라이브러리 ByteBuddy를 사용하여 실습해 보겠습니다.

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>me.whiteship</groupId>
  <artifactId>javaproxy</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>demospringdi</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.example.com</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>11</maven.compiler.source>
    <maven.compiler.target>11</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy -->
    <dependency>
      <groupId>net.bytebuddy</groupId>
      <artifactId>byte-buddy</artifactId>
      <version>1.12.8</version>
    </dependency>
  </dependencies>
</project>
package me.whiteship;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.implementation.InvocationHandlerAdapter;
import org.junit.Test;


import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

import static net.bytebuddy.matcher.ElementMatchers.named;
import static org.junit.Assert.*;

public class BookServiceTest {

    @Test
    public void dynamicProxy() throws Exception{
        Class<? extends DefaultBookService> proxyClass = new ByteBuddy().subclass(DefaultBookService.class)
                .method(named("rent")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
                    DefaultBookService defaultBookService = new DefaultBookService();

                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("aaaa");
                        Object invoke = method.invoke(defaultBookService, args);
                        System.out.println("bbbb");
                        return invoke;
                    }
                }))
                .make().load(DefaultBookService.class.getClassLoader()).getLoaded();

        DefaultBookService bookService = proxyClass.getConstructor(null).newInstance();

        Book book = new Book();
        book.setTitle("spring");
        bookService.rent(book);
        bookService.returnBook(book);
    }
}

자바 Dynamic Proxy 실습과 마찬가지로 똑같이 구현되게 테스트 코드를 작성하였습니다.

dynamicProxy 메소드의 결과값은

aaaa

rent : spring

bbbb

return : spring 이렇게 출력됩니다.

Dynamic Proxy는 런타임에 인터페이스 또는 클래스의 인스턴스 또는 클래스를 만들어 사용하는 프로그래밍 기법입니다.

사용처는 Spring data JPA, Spring AOP, Mockito 등이 있습니다.

 

이로써 공부한 내용을 간략히 정리해보았습니다. 

감사합니다.

 


출처

https://www.inflearn.com/course/the-java-code-manipulation