Spring Data JPA를 쓰다보면 보통 아래와 같이 코드를 작성하곤 한다.
interface UserRepository : JpaRepository<User, Long> {
fun findByEmail(email: String): User?
}
이렇게 인터페이스만 만들어두는데 userRepository.findByEmail(…)을 호출하면 쿼리가 알아서 나간다. 이게 어떻게 동작하는지 문득 궁금하게 되었다.
이 글에서는 UserRepository가 어떻게 Bean으로 등록되고, 실제 쿼리를 실행하기까지 어떤 내부 과정이 있는지를 단계별로 정리해본다.
우리가 만든 UserRepository 인터페이스에는 @Repository 나 @Component 같은 어노테이션이 붙어있지 않다. 그런데도 스프링이 실행되면 이 인터페이스가 Bean으로 등록되어 주입된다. 어디서, 누가, 어떤 시점에 이 일을 하는지부터 알아보자.
package org.springframework.data.jpa.repository.config;
@Import(JpaRepositoriesRegistrar.class)
public @interface EnableJpaRepositories {...}
Spring Data JPA는 우리가 작성한 Repository 인터페이스를 직접 Bean으로 등록하지 않는다. 대신 애플리케이션 부팅 시 @EnableJpaRepositories 어노테이션이 읽히면 해당 어노테이션에 달려있는 @Import 어노테이션에 의해 JpaRepositoriesRegistrar 클래스에 대한 처리를 하게 된다.
JpaRepositoriesRegistrar 는 ImportBeanDefinitionRegistrar 를 구현한 클래스인데 스프링은 이를 만나면 구현된 registerBeanDefinitions(…) 콜백을 호출해 직접 BeanDefinition을 등록하도록 위임한다.
JpaRepositoriesRegistrar의 해당 콜백은 아래처럼 RepositoryBeanDefinitionRegistrarSupport에 구현되어있고 결국 RepositoryConfigurationDelegate#registerRepositoriesIn(...) 를 호출하는 것을 알 수 있다.
// RepositoryBeanDefinitionRegistrarSupport.class
@Override
public void registerBeanDefinitions(
AnnotationMetadata metadata,
BeanDefinitionRegistry registry,
BeanNameGenerator generator,
) {
...생략...
RepositoryConfigurationDelegate delegate = new RepositoryConfigurationDelegate(configurationSource, resourceLoader,
environment);
delegate.registerRepositoriesIn(registry, extension);
}