JPA QueryMethod 구현해보기

chloe·2024년 7월 22일
0

Other

목록 보기
3/3
post-thumbnail
post-custom-banner

발단

이래저래 스터디를 하면서 QueryMethod는 스프링 데이터 JPA가 메소드 이름을 분석해서 JPQL을 실행한다는걸 알게되었다.
전부터 Interface에 JpaRepository를 상속하고 method를 정의만 해뒀는데 어떻게 실행되는건지 원리가 궁금했었다.

Reflection을 사용할 것 같은데 평소에 스프링이 제공하는 @Aspect 를 가져다 쓰는데만 익숙하다보니,
공부는 했었지만 직접 Reflection이나 Proxy 객체를 만들어볼 일이 없어서 이참에 연습 겸 해보면 좋을 것 같다고 생각했다.

또 최근에 네이버 부스트캠프 9기 챌린지가 시작됐다는 소식을 들으면서, 챌린지 때처럼 하나의 언어로 밑에서부터 구현하면서 그 언어를 학습하는 좋은 방식이라 생각해 나만의 챌린지를 시작했다.

구현과 더불어 부캠때는 잘 하지 못했던 TDD도 같이 진행했다.

직접 만들어보자

📎 GitHub Repository

What

구현 하는 항목은 크게 3가지로 나눴다.
1. MyJpaRepository 를 상속하는 Repository를 찾아 해당 Interface의 Proxy객체를 대신 Bean에 등록한다.
2. (1)의 Repository Interface내 Method들로 Query를 만들어낸다.
3. (1)의 Repository가 호출될 때는 Proxy 객체가 Method에 맞는 (2)의 Query를 찾아 출력한다.

How

  1. QueryMethodCache Class

    • MyJpaRepository를 상속한 Repository의 Method를 파싱하여 PrepareStatement Query 생성한다.
    • Component로 등록해 Application 실행 시 Query를 미리 인자 부분만 남겨두고 생성해둔다.
  2. Parser

    • method name을 전달받아 SELECT 문을 생성한다.
    • Parser를 의존하는 다른 객체의 단위 테스트를 위해 FakePaser를 테스트 시 주입할 수 있게했다.
    • Paser 구현이 프로젝트의 주 목적은 아니였기 때문에 Spring 문서에 나와있는 예시 수준으로 단위 테스트를 작성해둔 다음, 테스트가 성공할 때까지 구현했다. 이것도 꽤 걸림
  3. QueryMethodAdvice Class

    • Repository의 Method를 호출하면 Proxy 객체가 실행되며 Query에 동적인 값 인자를 결합하여 Query문을 완성한다.
  4. ProxyFactoryBean

    • ProxyFactoryBean을 사용하여 MyJpaRepository을 상속한 Repository Interface의 Prxoy Bean을 생성한다.

어려웠던 점

실제 구현은 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으로 등록하는 부분을 다시 공부해보고 적용하는데 오래걸렸다.

Result

Structure

실행

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 메소드 부터 따라가보면 된다.

회고

  • 실무에서는 왠만하면 @Aspect를 가져다 쓰기만 했기 때문에, 책이나 문서로만 읽고 넘어갔던 부분들을 직접 구현할 수 있어서 재미있었다.
  • 파싱하는 부분은 Copilot이나 GPT한테 시키려고 했는데, 자꾸 제대로 못해서 그냥 내가 직접 구현했다.
  • Parser를 직접 구현하면서 이전에 Next Step에서 배웠던 일급 컬렉션이나 객체 중심적으로 개발하는 과정을 다시 한번 적용해봤다.
  • TDD 연습을 하면서, 평소에 API 하나 전체 구현 후 테스트 하던 방식에서 작은 클래스 하나 틀만 만들어서 테스트를 작성해 둔 다음, 구현하면서 틀리는 부분을 계속 수정했더니 확인 과정이 더 빨라졌다.
  • 객체를 계속 나누면서 리팩토링을 하다보니 이전에 만들어둔 테스트를 빠르게 돌려보기만 하면되는 점이 좋았다.
  • 좀 더 확장해본다면, 지금은 Query 만들어서 출력까지 했는데, 실제 조회하고 이를 기반으로 Entity를 만들거나 1차 캐시도 구현해보는 것도 좋을 것 같다.
profile
삽질전문 아티스트
post-custom-banner

0개의 댓글