본문 바로가기

메모

extends JpaRepository<Cookie, String>

728x90

날 것의 SQL을 작성하며, 개발하던 개발자가 처음스프링에서 spring-data-jpa를 접한 순간 가장 신기한 부분은 다름 아닌 인터페이스로 repository를 만드는 것이다. 단순히 XXXRepository를 하는 것 만으로 기본적으로 필요한 메서드가 자동으로 만들어주기 때문이다.

 

하지만 인생사가 그러하듯 득이 있으면 실이 있고, 그 사이를 저울질하며 어떤 것이 더 이득인지 결정하듯 이 또한 비슷한 것이 아닌가 생각이 든다.

Wingardium Leviosa

spring-data-jpa는 어떻게 아무것도 없는 구현부를 마법처럼 만들어줄까? 그 시작은 Bean 생성에서 시작된다. Spring은 시작하며 관련된 bean을 생성하게 된다. 이때 JpaRepository를 하위 클래스들은 @NoRepositoryBean에 의해 Spring에 의해 즉시 bean이 생성되지 않고, RepositoryFactoryBeanSupport에 의해 초기화된다. 이렇게 함으로써 우리가 만든 중간 인터페이스, 예를 들어 CookieRepository extends JpaRepository<T, ID>에서 CookieRepository에 대한 구현 클래스를 bean 등록 없이 우리가 CookieRepository를 주입받을 수 있는 마법 중 하나이다.

this.repositoryMetadata = this.factory.getRepositoryMetadata(repositoryInterface);
...
this.repository = Lazy.of(() -> this.factory.getRepository(repositoryInterface, repositoryFragmentsToUse));

RepositoryFactoryBeanSupport.java에서는 생성할 bean의 metadata를 통해 구체적인 repository를 주입받게 된다. 단, 지금까지 과정을 거치면 실제로 객체가 생성되는 것이 아니라 주입받은 bean을 직접적으로 호출하면 실제 repository 구현체가 생성된다.

StartupStep repositoryProxyStep = onEvent(applicationStartup, "spring.data.repository.proxy", repositoryInterface);
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);

if (MethodInvocationValidator.supports(repositoryInterface)) {
    result.addAdvice(new MethodInvocationValidator());
}

result.addAdvisor(ExposeInvocationInterceptor.ADVISOR);

if (!postProcessors.isEmpty()) {
    StartupStep repositoryPostprocessorsStep = onEvent(applicationStartup, "spring.data.repository.postprocessors",
            repositoryInterface);
    postProcessors.forEach(processor -> {

        StartupStep singlePostProcessor = onEvent(applicationStartup, "spring.data.repository.postprocessor",
                repositoryInterface);
        singlePostProcessor.tag("type", processor.getClass().getName());
        processor.postProcess(result, information);
        singlePostProcessor.end();
    });
    repositoryPostprocessorsStep.end();
}

if (DefaultMethodInvokingMethodInterceptor.hasDefaultMethods(repositoryInterface)) {
    result.addAdvice(new DefaultMethodInvokingMethodInterceptor());
}

Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
        evaluationContextProvider);
result.addAdvice(new QueryExecutorMethodInterceptor(information, getProjectionFactory(), queryLookupStrategy,
        namedQueries, queryPostProcessors, methodInvocationListeners));

result.addAdvice(
        new ImplementationMethodExecutionInterceptor(information, compositionToUse, methodInvocationListeners));

T repository = (T) result.getProxy(classLoader);

메서드를 호출할 때 위 코드가 동작하며 포록시로 감싼 SimpleRepository를 반환하게 된다.

여담

소스 코드를 구석구석 보고 다니면서 몇 가지 인사이트(?)를 얻은 점을 공유해 보자. 다소 억지스러운 면이 있지만 알쓸신잡 같은 지식 정도로 봐주면 좋을 것 같다.

매개변수는 3개를 초과하지 말자.

    @Hidden
    @ForceInline
    private Object invokeImpl(Object obj, Object[] args) throws Throwable {
        return switch (paramCount) {
            case 0 -> target.invokeExact(obj);
            case 1 -> target.invokeExact(obj, args[0]);
            case 2 -> target.invokeExact(obj, args[0], args[1]);
            case 3 -> target.invokeExact(obj, args[0], args[1], args[2]);
            default -> target.invokeExact(obj, args);
        };
    }

reflect 된 메서드를 호출하는 경우 메소드 형태가 동적이기 때문에 위 코드처럼 인자의 길이를 기준으로 분기한다. 이펙티브 자바에서도 언급하듯 가변 인자는 신중하게 사용하고, 복사 비용을 절약하기 위해 많이 사용되는 인터페이스는 위 코드처럼 작성해 불필요한 복사를 피하도록 권장한다. 이 말에 이어지듯 3개 이상이 되면 불필요한 배열 복사가 일어나기 때문에 피하도록 하자.