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클래스.
- 인터페이스가 있을때는 인터페이스 프록시를 써라