본문 바로가기
Spring 이해하기

대체 JPA에서 Proxy 로 초기화된다는 말이 뭔데?

by simplify-len 2020. 10. 2.

인프런에 실제로 질문했던 내용

들어가기

인프런에서 JPA관련 학습 동영상을 듣다가 LazyInitalization 의 경우, null로 Porxy가 초기화 된다는 말을 했었습니다. 그래서, 코드를 찬찬히 보니 어떤 가짜 객체를 만드는 것처럼 보였습니다. 이건 대체 무엇일까? 라는 고민으로 "다이나믹프록시" 에 대한 학습을 하게 되었습니다.

 대부분의 내용은 백기선님의 더 자바, 코드를 조작하는 다양한 방법에서 발췌한 부분을 베이스로, 이를 제가 이해한 방식으로 풀어 적어내려가볼까 합니다.

위 질문에서 받은 답변

스프링부트에서 디버깅을 하다 보면 자주 마주치는 이름이 바로 cglib 이라는 라이브러리입니다.  이 라이브러리는 코드를 생성해주는 것이라고 이해하고 있었고, 자세히는 몰랐었습니다.

 인터넷에 검색을 해도 생각했던 것 만큼 자세히 나오지는 않았습니다. 그러던 중 "다이나믹 프록시" 에 대한 학습을 하면서 cglib, bytecode 와 같은 도구가 왜 사용되어야 했는지 이해할 수 있었습니다.

다이나믹 프록시란?

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

주로 다이나믹 프록시는

- 스프링 데이터 JPA
- 스프링 AOP
- Mockito
- 하이버네이트 lazy initalization에 사용됩니다.

그렇다면 프록시(proxy)란 무슨 의미일까?

 사실 프록시라는 말 때문에, 무엇인가 이해하기 어려워지는 현상이 분명히 있는 것 같습니다. 프록시는 단순히 A -> B 가는 데 있어서, 중간에 비서와 같은 업무를 해주는 객체를 놓는 것을 의미합니다. 즉, A -> C -> B 이런 식으로 말이죠.

더 자바, 강의 자료 일부

이제 동작하는 코드로 프록시를 살펴보겠습니다.

동작하는 프록시 패턴을 이해해보십시다.

 

메인 코드

class BookServiceTest {

        BookService defaultBookService = new DefaultBookService();

        BookService bookService = new BookServiceProxy(defaultBookService);

        @Test void di() {
                assertNotNull(bookService);

                Book book = new Book();
                book.setTitle("spring");
                bookService.rent(book);
        }
}
public class DefaultBookService implements BookService {

  //빌려주기 전에 필요 로깅을 해야 한다면?
  @Override
  public void rent(Book book) {
    System.out.println("rent: " + book.getTitle() + "");
  }
}
public class BookServiceProxy implements BookService {

  BookService bookService;

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

  //빌려주기 전에 필요 로깅을 해야 한다면?
  @Override
  public void rent(Book book) {
    System.out.println("XAXAXA");
    bookService.rent(book);
    System.out.println("XBXBXB");
  }
}
public interface BookService {

  void rent(Book book);

}

 

그러나 만약 BookService 에 메소드 하나가 더 늘어나면, DefaultBookSerivce와 BookServiceProxy 에 메소드를 하나씩 추가해주면서 비슷한 코드가 더해지는 단점을 안고있습니다. 

그래서,

다이나믹 프록시는 클래스를 만드는 것이 아니라, 동적으로 런타임에 생성해내는 방법을 말합니다. 자바의 리플랙션 패키지에서 제공하는 기능을 통해서 할 수 있습니다.

다시 다이나믹 프록시를 설명해보면, 런타임에 인터페이스 또는 클래스의 프록시 인스턴스 또는 클래스를 만들어 사용하는 프로그래밍 기법을 말합니다. 

그럼 다시 위 코드를 실행시점에 만들어지는 코드를 생성해보도록 하겠습니다.

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

  @Test
  void di() {
    assertNotNull(bookService);

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

위 코드를 실행시키면,

AAAA
rent: spring
AAAA

이런 형태의 코드가 나올 것입니다.

런타임시점에 코드를 생성하는 것은 좋지만, 유연하게 할 수는 없습니다.

Spring AOP가 Proxy 기반의 AOP라고 생각하는 것이 좋습니다.

이제 본격적으로 Cglib과 bytebuddy로 Proxy를 생성하는 것을 실습해보겠습니다.

@Test
  void di() {
    MethodInterceptor handler = new MethodInterceptor() {
      @Override
      public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
          throws Throwable {
        System.out.println("XXX");
        Object invoke = method.invoke(bookService, objects);
        System.out.println("YYYY");
        return invoke;
      }
    };
    Book book = new Book();
    book.setTitle("spring");
    BookService bookService2 = (BookService) Enhancer.create(BookService.class, handler);
    bookService2.rent(book);

    assertNotNull(bookService);

CGlib을 통해서 Proxy가 생성되는 코드입니다.

    Class<? extends BookService> proxyClass = new ByteBuddy().subclass(BookService.class)
        .method(named("rent")).intercept(InvocationHandlerAdapter.of(new InvocationHandler() {
          BookService bookService = new DefaultBookService();
          @Override
          public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println("OOOOO");
            Object invoke = method.invoke(bookService, args);
            System.out.println("IIIIIII");
            return invoke;
          }
        }))
        .make().load(BookService.class.getClassLoader()).getLoaded();

    BookService bookService = proxyClass.getConstructor().newInstance();
    bookService.rent(book);

바이트버디(ByteBuddy)를 통해서 Proxy를 생성하는 코드입니다.

또한 바이트버디는 바이트 코드를 조작할 때도 사용할 수 있습니다.

그러나, 서브 클래스를 만드는 방법에 대한 단점으로는, 상속을 사용하지 못하는 경우 프록시를 만들 수 없습니다. (Private 생성자만 있는 경우와 final 클래스인 경우) 그리고, 인터페이스가 있을 때는 인터페이스의 프록시를 만들어 사용할 것을 권고하고 있습니다.

지금까지 다이나믹 프록시에 대해서 인프런의 강의를 통해서 이해한 바를 정리했습니다.

다음에는 토비의 스프링에서 말하는 AOP와 다이나믹 프록시에 대한 설명을 정리하는 시간을 가져보겠습니다.

댓글