Search
Duplicate

다이나믹 프록시

스프링데이터 JPA는 어떻게 동작할까?

스프링 데이터 JPA에서 DB와 데이터를 주고받는 Repository 인터페이스가 있습니다.
여기서 신기한점은 이 리포지토리는 따로 애노테이션을 통해 Dependency Injection이 되는것도 아닌 것 같고, 구현체 정의를 한적도 없는데
JpaRepository를 상속받기만하면 해당 타입의 객체도 만들어지고 해당 타입의 빈(bean)도 등록이 됩니다.
과연 스프링데이터JPA에서 인터페이스 타입의 인스턴스는 누가 만들어주는 것일까?
JpaRepository 역시 인터페이스이고 사용하게 되는 기능(메서드)들도 정의만 되어있는데 실제로 동작을 하는걸 볼 수 있습니다.

Proxy

⇒ 핵심은 리플렉션 패키지 내부에 있는 프록시(proxy)클래스 입니다.
스프링데이터JPA에서는 스프링 AOP를 쓰고있고, 스프링AOP가 프록시의 코드를 조금 더 추상화 한 (ProxyFactory)를 제공하는데, 이 프록시 팩토리가 자바의 다이나믹프록시를 좀 더 추상화 해 놓은 스프링 AOP의 핵심 클래스입니다.
스프링 데이터 JPA는 RepositoryFactorySupport에서 사용합니다.
ProxyFactory result = new ProxyFactory(); result.setTarget(target); result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
Java
복사
⇒ RepositoryFactorySupport 내부의 코드로 프록시 객체를 만들어주는 부분입니다. 여기서 만들어주는 프록시 객체가 빈으로 등록이 되서
프로젝트 내부에 @Autowired 등등으로 선언된 곳에 주입됩니다.

프록시 패턴

프록시는 직역하자면 대리인이라고 볼 수 있는데, 프로그래밍에서는 리얼 서브젝트(실제 객체)와 프록시가 동일한 인터페이스(서브젝트)를 구현하고 있으며, 프록시는 리얼 서브젝트를 참조합니다. 그리고 서브젝트는 프록시를 사용합니다.
이럴 시퀀스 다이어그램으로 그리면 위와 같습니다.
예를들면 회사내에서 프록시를 비서 정도로 볼 수 있습니다. 클라이언트들의 요청을 비서(프록시)가 받아서 회장님(리얼서브젝트)에게 연결할 수도 있고, 안 할수도 있습니다. 어째서 프록시 패턴을 사용하는 것일까요.
개발을 할 때 단일 책임(SRP)를 지키며 개발을 하기위해선 부가기능, 접근제한 AOP등등 리얼 서브젝트에 들어가선 안됩니다. 하지만, 이런 모든 기능들을 모두 리얼 서브젝트(도메인)에 집어넣게 되면 코드는 복잡해지고 객체지향적이지도 않게 됩니다.

특징

프록시가 리얼 서브젝트가 공유하는 인터페이스가 있고, 클라이언트는 해당 인터페이스 타입으로 프록시를 사용한다.
클라이언트는 프록시를 거쳐 리얼 서브젝트를 사용하기 때문에 프록시는 리얼 서브젝트에 대한 접근을 관리하거나 부가기능을 제공하거나, 반환값을 변경할 수도 있습니다.
리얼 서브젝트는 자신이 해야 할 일만 하면서(SRP) 프록시를 사용해서 부가적인 기능(접근 제한, 로깅, 트랜잭션, 등)을 제공할 때 이런 패턴을 주로 사용합니다.
참고: SRP(Single_Responsibility_principle)

간단한 예제

1. BookService.java

Code

2. DefaultBookService.java

Code

3. BookServiceProxy.java

Code

4. BookServiceTest.java

Code
위와같이 Real Subject의 비즈니스 로직들이 수행하는 시점에서 전,후로 부가기능들을 넣어줄 수 있습니다. 하지만 단점이 명확합니다. 메서드가 늘어날수록 해야할 작업들이 몹시 번거로워집니다. 위 예제와 같은 다이나믹 프록시 기능을 모든 메서드마다 구현해주고 프록시에서 프록시로 감싸거나 모든 구현체에서 원래 타겟으로 위임하는 코드가 중복해서 작성되야 할 수도 있습니다.
이렇게 프록시패턴을 사용할 경우 문제점들이 있기 때문에 이를 동적으로 런타임에 생성해내는 방법이 있는데 이것을 다이나믹 프록시라 하는 리플렉션의 기능이 있습니다.
아래 다이나믹 프록시 실습 에서 예제를 통해 알아보겠습니다.

다이나믹 프록시 실습

프록시 인스턴스 만들기

Object Proxy.newProxyInstance(ClassLoader, interfaces, InvocationHandler);
Java
복사

사용예제

1. BookServiceTest.java

Code

제약사항

위 예제에 나와있는 다이나믹 프록시기술에는 인터페이스단위로만 구현이 가능하다는 제약사항이 존재한다.
즉, 위 테스트코드의 BookService를 DefaultBookService 로 바꾸면 다이나믹 프록시에서 클래스기반의 프록시를 만들지 못합니다.
그렇기에 클래스기반의 다이나믹 프록시 기능을 사용하려면

클래스의 프록시가 필요하다면?

위에서 얘기했다시피 기존의 다이나믹 프록시 기능으로는 인터페이스가 아닌 클래스기반의 프록시를 만들 수 없습니다.
하지만, 다이나믹 프록시가 필요하다면 서브 클래스를 만들 수 있는 라이브러리를 사용해서 프록시를 만들 수 있습니다.

1. CGlib

의존성 주입
<dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>3.3.0</version> </dependency>
XML
복사
사용예제
Code

2. ByteBuddy

라이브러리 추가(Maven or Gradle 취사 선택)
<!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy --> <dependency> <groupId>net.bytebuddy</groupId> <artifactId>byte-buddy</artifactId> <version>1.10.13</version> </dependency> // https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy compile group: 'net.bytebuddy', name: 'byte-buddy', version: '1.10.13'
XML
복사
사용예제
Code

제한 사항

상속을 사용하지 못하는 경우 프록시를 만들 수 없습니다.
생성자의 접근제한자가 private뿐인 경우
클래스가 Final 클래스인 경우
인터페이스가 있을 때는 인터페이스의 프록시를 만들어 사용하는게 좋습니다.

다이나믹 프록시 정리

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

사용처

스프링 데이터 JPA
스프링 AOP
Mockito
하이버네이트 lazy initialzation
...