brunch

You can make anything
by writing

C.S.Lewis

by anonymDev Apr 27. 2022

JPA Repository 생성의 비밀

진짜는 프록시(Proxy)다

아래 소스코드들의 출처는 spring-project임을 밝힙니다. 

spring-data-jpa는 2.6.x를 기준으로 작성했습니다.


spring-data-jpa를 사용하면 JPA Repository를 인터페이스로 선언해서 사용한다.


엔티티의 타입이 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를 확장하고 있다.


<JpaRepositoryFactory.java>

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 반환하도록 오버라이드 했다.


<JpaRepositoryFactory.class>

@Override
protected Class<?> getRepositoryBaseClass(RepositoryMetadata metadata) {
   return SimpleJpaRepository.class;
}


<RepositoryFactorySupport.java>

protected abstract Class<?> getRepositoryBaseClass(RepositoryMetadata metadata);

...


Repository Base Class 클래스는 무엇일까?


메서드 명세를 살펴보면 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 되겠다.


TargetRepository는 무엇일까?

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)가 실 구현체를 실행한다.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari