2020-10-8 바이트코드 강의 정리(3) 동적프록시

프록시

  • 스프링데이터JPA는 어떻게 Repository를 가져올 수 있는걸까?

    • public interface BookRepository extends JpaRepository<Book, Integer> {
      
      }
      
    • 빈등록을 안했는데도 가져와서 쓸 수 있다.

    • 인터페이스 인스턴스가 어떻게 만들어졌나?? 핵심은 reflect.Proxy클래스에 있다.

    • 스프링 AOP가장위에는 아래와 같은 코드처럼 동작한다.

    • InvocationHandler handler = new MyInvocationHandler(...);
      Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),new Class<?>[] {Foo.class },                                                                 handler);
      
    • 스프링 AOP는 ProxyFactory라는 클래스를 제공

    • 스프링데이터JPA에서는 ProxyFactory를 사용

    • Mockito

    • hibernate lazy-loading

프록시패턴

  • 대리인.
  • 프록시와 리얼 오브젝트가 공유하는 인터페이스가 있고, 클라이언트는 해당 인터페이스 타입으로 프록시를 사용한다.
  • 프록시는 오브젝트를 참조하고 있다.
  • 클라이언트는 프록시를 통해 실제 오브젝트를 사용하기 때문에 프록시는 오브젝트에 대한 접근을 관리, 부가기능을 제공하거나 리턴값을 변경할 수 있다.
  • 객체지향 SOLID중에 단일책임원칙에 따라 오브젝트는 부가적인 기능을 제공하기보다 프록시를 통해 부가기능을 제공해야한다.

구현

// interface
public interface StudentService {
  public void addStudent(Student student);
}
// Proxy
public class ProxyStudentService implements StudentService {
  private StudentService studentService;
  public ProxyStudentService(StudentService studentService) {
    this.studentService = studentService;
  }
  @Override
  public void addStudent(Student student) {
    System.out.println("add message");
    studentService.addStudent(student);
    System.out.println("add message2");
  }
}
// default
public class DefaultStudentService implements StudentService {
  StudentRepository studentRepository;
  @Override
  public void addStudent(Student student) {
    System.out.println("name : " + student.name);
  }
}
// Test
public class StudentServiceTest {
  private StudentService studentService = new ProxyStudentService(new DefaultStudentService());
  private Student student;
  @Before
  public void setUp() {
    student = new Student();
    student.name = "jimmy";
  }
  @Test
  public void init() {
    studentService.addStudent(student);
  }
}

// result
add message
name : jimmy
add message2
  • 프록시가 실제 오브젝트를 가지고 있는게 중요. 프록시가 내부에서 실제 오브젝트를 호출하고 그 전후로 부가기능을 추가하면 된다.
  • 위와 같은 코드는 굉장히 번거롭다라는 문제가 발생.. 부가기능을 추가할때마다 프록시를 만들어야되고. 프록시에 프록시를 만들수도 있다. 이 과정에서 중복될 수 있다.
  • 해결법 : 프록시를 매번 만드는게 아니라, 동적으로 런타임에 프록시를 생성한다. 동적프록시라고 부른다.

다이나믹 프록시

  • 런타임에 특정 인터페이스들을 구현하는 클래스, 인스턴스를 만드는 기술.

  • Proxy.new ProxyInstance(Classloader ,INterface, InvocationHandler)

  • 유연한 구조가 아니기때문에 스프링AOP가 등장

  • public class StudentServiceTest {
      private StudentService studentService = (StudentService) Proxy.newProxyInstance(StudentService.class.getClassLoader(), new Class[]{StudentService.class},                                                                new InvocationHandler() {                                                                                    StudentService studentService = new DefaultStudentService();
                                                                @Override
                                                                                        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                                                                                          System.out.println("add message");
                                                                                          Object invoke = method.invoke(studentService, args);
                                                                                          System.out.println("add message2");
                                                                                          return invoke;
                                                                                        }
                                                                                      });
    
      private Student student;
      @Before
      public void setUp() {
        student = new Student();
        student.name = "jimmy";
      }
      @Test
      public void addStudent() {
        studentService.addStudent(student);
      }
    
      @Test
      public void remove() {
        studentService.removeStudent(student);
      }
    }
    // result
    add message
    remove student : jimmy
    add message2
    add message
    name : jimmy
    add message2
    
  • invoke에서 부가기능을 추가해야된다. 이때 실제 객체를 가지고 있어야한다.

  • invoke(proxy, method, args)에서 method는 실제 객체의 메서드를 의미. 하지만 addStudent, removeStudent에 모두 부가기능이 적용되는 문제가 있다.

  • 기능을 나누고 싶다면??

    • private StudentService studentService = (StudentService) Proxy.newProxyInstance(StudentService.class.getClassLoader(), new Class[]{StudentService.class},
                  new InvocationHandler() {
              StudentService studentService = new DefaultStudentService();
                      @Override
                      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                          if (method.getName().equals("addStudent")) {
                              System.out.println("add message");
                              Object invoke = method.invoke(studentService, args);
                              System.out.println("add message2");
                              return invoke;
                          }
                          return method.invoke(studentService, args);
                      }
                  });
      
      // result
      remove student : jimmy
      add message
      name : jimmy
      add message2
      
    • invoke를 변경하면 특정 메서드에만 부가기능을 넣을 수 있지만 유연한 구조는 아니다.

    • 이 구조를 변경하고 개선한게 Spring-AOP

한계

private StudentService studentService = (StudentService) Proxy.newProxyInstance(StudentService.class.getClassLoader(), new Class[]{DefaultStudentService.class},
            new InvocationHandler() {
        StudentService studentService = new DefaultStudentService();
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (method.getName().equals("addStudent")) {
                        System.out.println("add message");
                        Object invoke = method.invoke(studentService, args);
                        System.out.println("add message2");
                        return invoke;
                    }
                    return method.invoke(studentService, args);
                }
            });
// error
java.lang.IllegalArgumentException: me.jimmy.dynamic.DefaultStudentService is not an interface

  • Proxy.newInstance메서드에서 2번째 인자로 전달되는 클래스들이 반드시 인터페이스여야한다. 클래스기반으로 하면 에러난다.

  • 인터페이스가 없고, 클래스만 있으면 동적프록시 못하나? 못한다.

클래스의 프록시

  • cglib, ByteBuddy를 이용해서 클래스기반 프록시를 만든다.
  • cglib : spring, hibernate에서도 사용중. 하위호환성이 안좋아서, 각자 들고있다.

Cglib

  • Enhancer()를 통해 부가기능 구현

  • @Test
    public void addStudent() {
      MethodInterceptor handler = new MethodInterceptor() {
        StudentService studentService = new StudentService();
        @Override
        public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
          System.out.println("add message");
          Object invoke = method.invoke(studentService, args);
          System.out.println("add message2");
          return invoke;
        }
      };
      StudentService studentService = (StudentService) Enhancer.create(StudentService.class, handler);
      studentService.addStudent(student);
    }
    
  • cglib에 있는 Enhance.create()를 통해 부가기능을 추가했다.

  • create메서드의 정의는 public static Object create(Class type, Callback callback)와 같다.

  • callBack은 인터페이스이고, MethodInterceptor인터페이스가 callback인터페이스를 상속하고 있다.

  • MethodInterceptor인터페이스에 있는 intercept메서드를 구현하면 된다.

ByteBuddy

@Test
public void addStudent_byteBuddy() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
  Class<? extends StudentService> proxyClass = new ByteBuddy().subclass(StudentService.class)
    .method(named("addStudent")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
    StudentService studentService = new StudentService();
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      System.out.println("byte adding 111");
      Object invoke = method.invoke(studentService, args);
      System.out.println("byte adding 2222");
      return invoke;
    }
  }))
    .make()
    .load(StudentService.class.getClassLoader()).getLoaded();
  StudentService studentService = proxyClass.getConstructor(null).newInstance();

  studentService.addStudent(student);
}
  • bytebuddy의 바이트코드 조작을 이용해서 프록시를 만들고, 부가기능을 추가할 수 있다.
  • method(named(“addStudent”))로 제한했기때문에 해당 메서드에만 제한할 수 있다.

서브클래스를 만드는 방법 단점

  • cglib, bytebuddy 둘다해당.

  • 상속을 사용하지 못하면 프록시를 못만든다.
  • private생성자만 있을때, fianl클래스.
  • 인터페이스가 있을때는 인터페이스 프록시를 써라
Written on October 8, 2020