스프링 데이터 JPA는 어떻게 동작하나?
- java.lang.reflect.Proxy 클래스
- 인스턴스와 같이 행동하는 Proxy 객체를 리턴해준다.
InvocationHandler handler = new MyInvocationHanler(...); Foo f = (Foo)Proxy.newProxyInstance(Foo.class.getClassLoader(), new Class<?>[] {Foo.class}, handler);
- 인스턴스와 같이 행동하는 Proxy 객체를 리턴해준다.
- org.springframework.aop.framework.ProxyFactory 클래스
- Java의 다이나믹 프록시를 좀 더 추상화하여 제공하는 스프링 AOP의 핵심 클래스
- 스프링 데이터 JPA에서 인터페이스 타입의 인스턴스는 누가 만들어 주는 것인가?
- Spring AOP 기반으로 동작하며 RepositoryFactorySupport에서 프록시를 생성한다.
프록시 패턴
- 프록시와 리얼 서브젝트가 공유하는 인터페이스가 있고, 클라이언트는 해당 인터페이스 타입으로 프록시를 사용
- 클라이언트는 프록시를 거쳐 리얼 서브젝트를 사용
- 클라이언트는 프록시를 거쳐 리얼 서브젝트를 사용하기 때문에 프록시는 리얼 서브젝트에 대한 접근을 관리하거나 부가 기능을 제공하거나 리턴값을 변경할 수도 있다.
-
리얼 서브젝트는 자신이 해야 할 일만 하면서(SRP - Single Responsibility Principle) 프록시를 사용해서 부가적인 기능(접근 제한, 로깅, 트랜잭션 등)을 제공할 때 이런 패턴을 주로 사용한다.
- 예제
- BookService: Interface로 서브젝트
public interface BookService { void rent(Book book); }
- DefaultBookService: BookService 구현체로 리얼 서브젝트
@Service public class DefaultBookService implements BookService{ @Autowired public BookRepository bookRepository; @Override public void rent(Book book) { // TODO Auto-generated method stub System.out.println("Rent: " + book.getTitle()); } }
- BookServiceProxy: Proxy로 클라이언트는 서브젝트
- BookService를 사용할 때 프록시인 BookServiceProxy를 사용하도록 되어 있다.
- BookServiceProxy가 리얼 서브젝트인 DefaultBookService를 사용한다.
@Service public class BookServiceProxy implements BookService { // Qualifier 빈의 가져올 이름(클래스명) @Autowired @Qualifier("defaultBookService") private BookService bookService; @Override public void rent(Book book) { // TODO Auto-generated method stub System.out.println("AAA"); bookService.rent(book); System.out.println("BBB"); } }
- Test
@SpringBootTest public class BookServiceProxyTest { @Autowired @Qualifier("bookServiceProxy") private BookService bookService; @Test public void test() { Book book = new Book(); book.setTitle("spring"); bookService.rent(book); } }
- BookService: Interface로 서브젝트
- 참고
- https://www.oodesign.com/proxy-pattern.html
- https://en.wikipedia.org/wiki/Proxy_pattern
- https://en.wikipedia.org/wiki/Single-responsibility_principle
다이나믹 프록시 실습
- 런타임에 특정 인터페이스들을 구현하는 클래스 또는 인스턴스를 만드는 기술
- an application can use a dynamic proxy class to create an object that implements multiple arbitary event listener interfaces
- 프록시 인스턴스 만들기
- Object Proxy.newProxyInstance(ClassLoader, Interfaces, InvocationHandler)
- 프록시 패턴을 만들지 않고도 구현이 가능하다.
- 인터페이스(서브젝트)와 구현 클래스(리얼 서브젝트)만 있으면 된다.
- InvocationHandler에서 invoke를 오버라이딩하여 프록시 설정
- 하지만 유연한 구조가 아니다. 그래서 스프링 AOP 등장
- 스프링 AOP에 대한 더 자세한 사항은 토비의 스프링 AOP 참고
@SpringBootTest public class BookServiceProxyTest { 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 { // TODO Auto-generated method stub // if(method.getName().equals("rent") {...} 로 메소드별로 수정할 수 있다. System.out.println("aaaa"); Object invoke = method.invoke(bookService, args); System.out.println("bbbbb"); return invoke; } }); @Test public void test() { Book book = new Book(); book.setTitle("spring"); bookService.rent(book); } }
- Object Proxy.newProxyInstance(ClassLoader, Interfaces, InvocationHandler)
- 참고
- https://docs.oracle.com/javase/8/docs/technotes/guides/reflection/proxy.html
- https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Proxy.html#newProxyInstance-java.lang.ClassLoader-java.lang.Class:A-java.lang.reflect.InvocationHandler-
클래스의 프록시가 필요하다면?
- 서브 클래스를 만들 수 있는 라이브러리를 사용하여 프록시를 만들 수 있다.
- CGlib
- https://github.com/cglib/cglib/wiki
- 스프링, 하이버네이트가 사용하는 라이브러리
- 버전 호환성이 좋지 않아서 서로 다른 라이브러리 내부에 내장된 형태로 제공되기도 한다.
- dependency
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
- 예제
- 인터페이스(서브젝트) 없이 구현 가능
@SpringBootTest public class BookServiceTest { @Test public void di() { MethodInterceptor handler = new MethodInterceptor() { BookService bookService = new BookService(); @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { if(method.getName().equals("rent") { System.out.println("aaaa"); Object invoke = method.invoke(bookService, objects); System.out.println("bbbb"); return invoke; } return method.invoke(bookService, objects); } } BookService bookService = (BookService)Enhancer.create(BookService.class, handler); bookService.rent(); } }
- 인터페이스(서브젝트) 없이 구현 가능
- ByteBuddy
- https://bytebuddy.net/#/
- 바이트 코드 조작뿐 아니라 런타임(다이나믹) 프록시를 만들 때도 사용할 수 있다.
- 예제
@SpringBootTest public class BookServiceTest { @Test public void di() throws Exception{ Class<? extends BookService> proxyClass = new ByteBuddy().subclass(BookService.class) .method(named("rent")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() { BookService bookService = new BookService(); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // TODO Auto-generated method stub // method에서 rent이름만 가져와 intercept하기 때문에 분기탈 필요가 없다 System.out.println("aaaa"); Object invoke = method.invoke(bookService, args); System.out.println("bbbbb"); return invoke; } }).make().load(BookService.class.getClassLoader()).getLoaded(); BookService bookService = proxyClass.getCosntructor(null).newInstance(); } }
- 서브 클래스를 만드는 방법의 단점
- 상속을 사용하지 못하는 겨우 프록시를 만들 수 없다.
- private 생성자만 있는 경우
- Final 클래스인 경우
- 인터페이스가 있을 때에는 인터페이스의 프록시를 만들어 사용할 것
- 상속을 사용하지 못하는 겨우 프록시를 만들 수 없다.
다이나믹 프록시 정리
- 다이나믹 프록시
- 런타임에서 인터페이스 똔느 클래스의 프록시 인스턴스 또는 클래스를 만들어 사용하는 프로그래밍 기법
- 다이나믹 프록시 사용처
- 스프링 데이터 JPA
- 스프링 AOP
- Mockito
- 하이버네이트 lazy initialization
- …
- 참고
- http://tutorials.jenkov.com/java-reflection/dynamic-proxies.html