이래저래 스터디를 하면서 QueryMethod는 스프링 데이터 JPA가 메소드 이름을 분석해서 JPQL을 실행한다는걸 알게되었다.
전부터 Interface에 JpaRepository를 상속하고 method를 정의만 해뒀는데 어떻게 실행되는건지 원리가 궁금했었다.
Reflection을 사용할 것 같은데 평소에 스프링이 제공하는 @Aspect
를 가져다 쓰는데만 익숙하다보니,
공부는 했었지만 직접 Reflection이나 Proxy 객체를 만들어볼 일이 없어서 이참에 연습 겸 해보면 좋을 것 같다고 생각했다.
또 최근에 네이버 부스트캠프 9기 챌린지가 시작됐다는 소식을 들으면서, 챌린지 때처럼 하나의 언어로 밑에서부터 구현하면서 그 언어를 학습하는 좋은 방식이라 생각해 나만의 챌린지를 시작했다.
구현과 더불어 부캠때는 잘 하지 못했던 TDD도 같이 진행했다.
구현 하는 항목은 크게 3가지로 나눴다.
1. MyJpaRepository
를 상속하는 Repository를 찾아 해당 Interface의 Proxy객체를 대신 Bean에 등록한다.
2. (1)의 Repository Interface내 Method들로 Query를 만들어낸다.
3. (1)의 Repository가 호출될 때는 Proxy 객체가 Method에 맞는 (2)의 Query를 찾아 출력한다.
QueryMethodCache
Class
Parser
QueryMethodAdvice
Class
ProxyFactoryBean
실제 구현은 1 -> 3 -> 4 -> 2 순으로 진행했다.
JDK Dynamic Proxy를 적용하려고 InvocationHandler를 상속해서 구현하고, Test에서 Proxy 객체 생성하는건 성공했는데, 결국 Spring에서 사용하려면 Bean으로 등록해야해서 이부분이 제일 고민이었다.
처음 설계할 때는 BeanPostProcessor를 정의해서 갈아 끼울 생각이었지만 생각해보니 Repository를 Interface로 만들어뒀다 보니 구체 클래스가 없어서 바꿔치기고 뭐고 할 수가 없었다.
최대한 원시적(?)으로 구현하고 싶었는데 결국 ProxyFactory를 써야겠다고 방향을 틀었다.
ProxyFactoryBean known example들은 다 구체 클래스를 target으로 지정했으나,
proxyFactory.setTarget(target);
문서에서 함수들을 찾다보니 target Interface를 지정하는 setInterface 함수가 있었다.
@Bean
public ProxyFactoryBean proxyFactoryBean() throws ClassNotFoundException {
QueryMethodCache queryMethodCache = new QueryMethodCache(new MethodParser());
ProxyFactoryBean pfBean = new ProxyFactoryBean();
Class[] array = new Reflections("com.example.demo").getSubTypesOf(MyJpaRepository.class).toArray(new Class[0]);
pfBean.setProxyInterfaces(array);
pfBean.addAdvice(new QueryMethodAdvice(queryMethodCache));
return pfBean;
}
아무튼 Proxy 객체를 Bean으로 등록하는 부분을 다시 공부해보고 적용하는데 오래걸렸다.
repository를 실행하면 Entity를 반환하고 Query가 로그로 남게된다.
RepositoryFactoryBeanSupport 클래스의 afterPropertiesSet 메소드는 Spring Framework에서 Bean이 초기화되는 과정에서 호출된다.
이 때 JpaRepositoryFactory 클래스의 getRepository 메소드를 호출하면서 Proxy 객체를 만드는 과정을 수행한다.
getRepository 메소드를 살펴보면 ProxyFactory를 사용해서 targetInterface와 Advice를 지정한다.
// RepositoryFactorySupport.getRepository() 발췌
Object target = getTargetRepository(information);
repositoryTargetStep.tag("target", target.getClass().getName());
repositoryTargetStep.end();
RepositoryComposition compositionToUse = composition.append(RepositoryFragment.implemented(target));
validate(information, compositionToUse);
// Create proxy
StartupStep repositoryProxyStep = onEvent(applicationStartup, "spring.data.repository.proxy", repositoryInterface);
ProxyFactory result = new ProxyFactory();
result.setTarget(target);
result.setInterfaces(repositoryInterface, Repository.class, TransactionalProxy.class);
...
Optional<QueryLookupStrategy> queryLookupStrategy = getQueryLookupStrategy(queryLookupStrategyKey,
evaluationContextProvider);
result.addAdvice(new QueryExecutorMethodInterceptor(information, projectionFactory, queryLookupStrategy,
namedQueries, queryPostProcessors, methodInvocationListeners));
파싱하는 부분이 궁금하면 AbstractQueryLookupStrategy.resolveQuery 메소드 부터 따라가보면 된다.