1. Spring Data JPA는 어떻게 동작할까?
이번글의 가장 큰 주제는 Spring Data JPA에서 다음과 같은 코드들이 어떻게 구현체를 만드는지 이다!
public interface BookRepository extends JpaRepository<Book, Integer> {}
어떻게 interface만 정의했는데, 객체의 CRUD 가 작동할까?
도대체 누가 BookRepository 타입의 객체를 누가 만들어주는 것 일까!
이에 대한 핵심이 바로! Proxy 라는 class 이다!
Spring AOP를 기반으로 동작하며 RepositoryFactorySupport에서 프록시를 생성한다.
저 빨간 박스 안에서 Proxy를 생성하여 반환하여 DI 시켜준다!
(참고로 Proxy.class 같은 경우 java.lang.reflect 페키지 안에 들어있다.)
2. Proxy 패턴
우선 프록시 패턴에 대한 더 상세한 글을 내가 예전에 정리해둔 다음 글을 읽어보면 더 좋다.
https://blogshine.tistory.com/17
최종적으로 보이고 싶은 구조는 다음과 같다.
클라이언트는 요청을 보낼때 자신이 Real Subject에게 요청을 보내는줄 알지만, 사실은 Proxy에게 요청하고 있는 것 이다!
이번에는 간단하게 코드로 알아보자. 간단한 Book 이라는 class가 있다.
public class Book {
private String title;
public Book() {
}
public Book(String title) {
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
}
이제 사용할 서비스는 다음과 같다.
public interface BookService {
void rent(Book book);
}
이를 구현하는 구현체가 2개 있다!
각각 실재 객체와 프록시 이다.
public class DefaultBookService implements BookService{
@Override
public void rent(Book book) {
System.out.println("대여 : " + book.getTitle());
}
}
public class ProxyBookService implements BookService{
private BookService bookService;
public ProxyBookService(BookService bookService) {
this.bookService = bookService;
}
@Override
public void rent(Book book) {
System.out.println("대여 시간 : " + LocalDateTime.now());
bookService.rent(book);
System.out.println("대여 기간은 7일 입니다.");
}
}
이제 각각을 실행하는 테스트 코드는 다음과 같다.
class BookServiceTest {
BookService defaultBookService = new DefaultBookService();
BookService proxyBookService = new ProxyBookService(defaultBookService);
@Test
public void default_test() {
Book book = new Book("defualt Book!");
defaultBookService.rent(book);
}
@Test
public void proxy_test() {
Book book = new Book("proxy Book!");
proxyBookService.rent(book);
}
}
실행 결과는 다음과 같다.
대여 시간 : 2022-06-06T21:10:37.901231
대여 : proxy Book!
대여 기간은 7일 입니다.
대여 : defualt Book!
하지만 매번 이렇게 직접 Proxy를 만드는 것도 매우 큰 일이다.
이를 동적으로 Proxy를 만드는 기술을 동적 프록시(Dynamic Proxy)라고 부른다. 이에 대하여 알아보자!
3. Dynamic Proxy
이번시간에는 특정 Interface의 Proxy를 Runtime 때 만드는 방법에 대하여 알아보자.
Proxy는 Proxy.newProxyInatnace() 라는 메서드를 통해서 생성하게 된다.
이때 전달할 인자로는
1) classLoader
2) Proxy로 만들 대상 Interface
3) InvocationHandler() : Proxy의 메서드가 호출이 될때 어떻게 처리할지를 결정한다.
를 전달해야 한다.
우선 다음 사용 코드를 살펴보자.
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 {
System.out.println("대여 시간 : " + LocalDateTime.now());
Object invoke = method.invoke(bookService, args); // 호출되는 메서드
System.out.println("대여 기간은 7일 입니다.");
return invoke;
}
}
);
@Test
public void proxy_test() {
Book book = new Book("proxy Book!");
bookService.rent(book);
}
}
Ovverride한 invoke 메서드는 인자로 method를 받는데, 이는 Proxy를 통해 호출한 메서드를 가리킨다.
invoke(Object proxy, Method method, Object[] args)
첫번째 인자인 proxy는 해당 newProxyInatnace()로 생성한 proxy를 의미한다.
이후 realSubject에 해당하는 bookService를 인자로 넘기고, 나머지 args를 넘겨준다.
Object invoke = method.invoke(bookService, args);
즉, 위 코드는 realSubject에 있는 메서드를 호출하는 부분이다.
실행 결과는 다음과 같다.
▶ 동적프록시에는 단점이 몇가지 있다.
우선 단점중 하나는 모든 Proxy의 앞뒤에 대여 시간과, 대여 기간이 나오게 되어버린다.
왜냐하면 우리는 모든 메서드를 대상으로 앞뒤에 추가해줬기 때문이다.
잘 생각해보면 위 코드에서 method 인자를 통해 함수를 구분하거나 하는 일을 하지 않았었다.
따라서 메서드 별로 구분하려면 다음과 같이 코드를 작성해줘야 한다.
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("대여 시간 : " + LocalDateTime.now());
Object invoke = method.invoke(bookService, args); // 호출되는 메서드
System.out.println("대여 기간은 7일 입니다.");
return invoke;
}
return method.invoke(bookService, args);
}
}
);
@Test
public void proxy_test() {
Book book = new Book("proxy Book!");
bookService.rent(book);
}
}
위 코든느 함수의 이름이 "rent"인 경우에만 대여시간과 대여일을 출력해주게 된다.
또한 다이나믹 프록시의 가장 큰 단점중 하나는 Class 기반의 Proxy를 만들수 없다는 점 이다.
반드시 Interface를 기반으로 Proxy를 만들어야 한다.
다음 단락에서 Class 기반의 Proxy를 만들어 보자.
소제목 입력
이번시간에는 Interface가 겂는 경우에 Proxy를 만드는 방법에 대하여 알아보자. 총 2가지 방법에 대하여 알아보자.
1. CGlib 사용
2. ByteBuddy 사용
우선 사용할 class는 다음과 같다.
public class DefaultBookService {
public void rent(Book book) {
System.out.println("대여 : " + book.getTitle());
}
}
CGlib의 Enhander.creater라는 메서드를 사용하는데 handler가 필요하다.
Proxy 객체의 메서드를 호출할때 마다 어떤일을 해야하는지 알려주는 handler를 넘겨주어야 한다.
@Test
public void CGlib_test() {
MethodInterceptor handler = new MethodInterceptor() {
DefaultBookService bookService = new DefaultBookService();
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("대여 시간 : " + LocalDateTime.now());
return method.invoke(bookService, args);
}
};
DefaultBookService bookService = (DefaultBookService) Enhancer.create(DefaultBookService.class, handler);
Book book = new Book("proxy Book!");
bookService.rent(book);
}
출력 결과는 다음과 같다.
대여 시간 : 2022-06-07T07:44:23.616033
대여 : proxy Book!
이번에는 ByteBuddy를 통해 class의 proxy를 만들어보자.
원래의 class를 상속하는 subclass를 만든후 method이름으로 intercept하여 사전에 만든 InvocatoinHandler를 호출하게 된다.
@Test
public void byte_buddy_test() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<? extends DefaultBookService> proxyClass = new ByteBuddy().subclass(DefaultBookService.class)
.method(named("rent")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
DefaultBookService bookService = new DefaultBookService();
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("대여 시간 : " + LocalDateTime.now());
Object invoke = method.invoke(bookService, args);
System.out.println("대여 기간은 7일 입니다.");
return invoke;
}
}))
.make()
.load(DefaultBookService.class.getClassLoader())
.getLoaded();
DefaultBookService bookService = proxyClass.getConstructor(null).newInstance();
Book book = new Book("proxy Book!");
bookService.rent(book);
}
출력 결과는 다음과 같다.
대여 시간 : 2022-06-07T07:58:19.861486
대여 : proxy Book!
대여 기간은 7일 입니다.
서브 클래스를 만드는 방법에도 단점은 있다.
1) 상속을 사용하지 못하는 경우 프록시를 만들수 없다.
- Private 생성자만 있는 경우
- Final 클래스인 경우
2) 인터페이스가 있을 때는 인터페이스의 프록시를 만들어 사용할 것.
'BackEnd > Java' 카테고리의 다른 글
[Java] 동일성(identity)과 동등성(equality) (0) | 2022.09.06 |
---|---|
[Java] Annotation Processor (0) | 2022.06.08 |
[Java] Reflection (0) | 2022.05.22 |
[Java] 바이트코드 조작하기 (0) | 2022.05.20 |
[Java] JVM 구조 (0) | 2022.05.19 |
댓글