진짜는 프록시(Proxy)다
아래 소스코드들의 출처는 spring-project임을 밝힙니다.
spring-data-jpa는 2.6.x를 기준으로 작성했습니다.
엔티티의 타입이 Country인 CountryRepository 인터페이스를 예시로 정의했다.
public interface CountryRepository extends JpaRepository<Country, Long> {
Country findByName(String name);
...
}
간단히 인터페이스로만 선언하면 되기 때문에 편리하다. 그런데 직접 구현하지 않은 save(), saveAll(), findAll() 등 같은 메서드들은 대체 어디서 나온 걸까? 라는 궁금증을 가진적이 있을까.
비밀은 SimpleJpaRepository에 있다. 지난 글 'JpaRepository는 Save와 Update를 어떻게 구분하는지'에서 SimpleJpaRepository에 대해서 간단하게 언급했었다.
인터페이스로 선언된 JpaRepository의 save()를 호출하면 내부적으로 SimpleDataJpaRepository의 save()를 호출한다.
CountryRepository 인터페이스는 자체는 사실 껍데기에 불과하다. spring-data-jpa에는 JPARepository를 생성하는 JpaRepositoryFactory 클래스가 존재하는데 개발자가 정의한 CountryRepository 인터페이스를 참조하여 JpaRepository를 대신 구현하는 역할을 한다.
JpaRepositoryFactory는 spring-data-commons 모듈의 RepositoryFactorySupport를 확장하고 있다.
import org.springframework.data.repository.core.support.RepositoryFactorySupport;
public class JpaRepositoryFactory extends RepositoryFactorySupport {
...
RepositoryFactorySupport는 전달받은 Repository 인터페이스로 Proxy 인스턴스를 생성하는 추상 클래스이다.
<RepositoryFactorySupport.java>
package org.springframework.data.repository.core.support;
public abstract class RepositoryFactorySupport implements BeanClassLoaderAware, BeanFactoryAware {
...
RepositoryFactorySupport를 확장하여 다양한 spring-data-XXX에서 Repository를 생성한다. RepositoryFactorySupport에는 getRepository라는 Repository 인스턴스를 생성하는 메서드가 기본으로 정의돼 있다. 이 메서드는 Repository 인터페이스를 구현한 Proxy를 생성한다. JpaRepositoryFactory는 부모 클래스의 메서드인 getRepository를 상속 받는다.
<RepositoryFactorySupport.java>
public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
//첫 번째 파라미터 repositoryInterface로 CountryRepository가 전달된다.
...
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
// repositoryInterface로 전달된 CountryRepository의 인터페이스를 구현한다.
...
T repository = (T) result.getProxy(classLoader);
...
return repository; // Proxy 인스턴스를 반환한다.
}
대신 spring-data-jpa는 JpaRepositoryFactory가 추상 메서드인 getRepositoryBaseClass를 SimpleJpaRepository.class를 반환하도록 오버라이드 했다.
@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
return SimpleJpaRepository.class;
}
<RepositoryFactorySupport.java>
protected abstract Class<?> getRepositoryBaseClass(RepositoryMetadata metadata);
...
메서드 명세를 살펴보면 Base Class는 실제 Repository 인스턴스를 지원하는 클래스이다. 즉 SimpleJpaRepository는 실제로 생성된 JpaRepository 인스턴스를 지원하는(Backing) 인스턴스의 클래스(타입)이 된다.
Returns the base class backing the actual repository instance.
Make sure *getTargetRepository(RepositoryInformation) returns an instance of this class.
출처: docs.spring.io
getRepositoryBaseClass가 반환하는 Base class는 getRepositoryInformation에서 RepositoryInformation 인스턴스를 조립할 때 전달된다. RepositoryInformation은 말 그대로 Repository의 정보를 담고있다(예: 엔티티의 타입은 무엇인지, Base Class는 무엇인지 등등).
<RepositoryFactorySupport.java>
private RepositoryInformation getRepositoryInformation(RepositoryMetadata metadata,
RepositoryComposition composition) {
....
return repositoryInformationCache.computeIfAbsent(cacheKey, key -> {
Class<?> baseClass = repositoryBaseClass.orElse(getRepositoryBaseClass(metadata));
//getRepositoryBaseClass를 호출해 Base Class를 가져온다(SimpleJpaRepository.class).
return new DefaultRepositoryInformation(metadata, baseClass, composition);
});
}
...
하나 더! API 명세에서 getTargetRepository가 반환하는 인스턴스의 클래스가 Base Class와 같아야 한다고 했다.
Make sure *getTargetRepository(RepositoryInformation) returns an instance of this class.
RepositoryInformation은 getTargetRepository의 인자로 Base Class 정보를 전달해 TargetRepository 인스턴스의 Class 타입을 결정한다. JpaRepositoryFactory의 경우 TargetRepository가 SimpleJpaRepository.class가 되겠다.
Query Proxy를 지원하는 Repository 인스턴스라고 한다.
Create a repository instance as backing for the query proxy.
출처: docs.spring.io
앞서 RepositoryFactorySupport에는 실질적인 Repository 인스턴스를 생성하는 getRepository가 메서드가 정의돼 있다고 했다. 메서드를 훑어보면서 TargetRepository에 대해서 알아보자(아래 코드의 코멘트 (1), (2), (3). (4)를 읽으면서 따라가 보자)
<RepositoryFactorySupport.java>
public <T> T getRepository(Class<T> repositoryInterface, RepositoryFragments fragments) {
...
RepositoryInformation information = getRepositoryInformation(metadata, composition);
//(1) Base Class 정보가 담긴 RepositoryInformation을 가져온다.
...
Object target = getTargetRepository(information); //(2) TargetRepository를 생성한다.
...
ProxyFactory result = new ProxyFactory();
result.setTarget(target); //(3) TargetRepository는 Proxy의 target 인자로 전달된다.
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
...
T repository = (T) result.getProxy(classLoader);
...
return repository; //(4) Proxy Repository를 반환한다.
}
아하! Proxy 인스턴스의 target이 TargetRepository였구나! 즉 Proxy로 생성된 Repository 인스턴스의 target 인스턴스가 SimpleJpaRepository의 인스턴스였다는걸 알게 됐다. 그렇다면 SimpleJpaRepository가 뒤에서 JpaRepository 인터페이스의 실 구현체를 제공한다는 의미이다.
1. 최종적으로 Bean으로 생성되는 CountryRepository는 Proxy 인스턴스이다(Proxy Repository).
2. JpaRepository는 Proxy 인스턴스의 Target을 SimpleJpaRepository.class의 인스턴스로 주입한다.
3. Proxy Repository 인스턴스 내부에서 SimpleJpaRepository(target)가 실 구현체를 실행한다.