날 것의 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개 이상이 되면 불필요한 배열 복사가 일어나기 때문에 피하도록 하자.
'메모' 카테고리의 다른 글
예외에 대한 새로운 시각에 대한 내 생각 (0) | 2023.12.25 |
---|---|
알림 기능 고찰 🤔 (0) | 2023.12.21 |
🚀 데이터베이스 이전 대작전! 📊 클라우드로의 여정 🌐 (0) | 2023.12.15 |
GeoIP를 활용한 Nginx 국가별 접근 차단: 당신의 웹사이트를 지키는 글로벌 관문 🌍 🗝️ (1) | 2023.12.12 |
N+1 발생을 쉽게 파악할 수 없을까? (0) | 2023.12.08 |