1. 들어가며


Spring Data JPA를 쓰다보면 보통 아래와 같이 코드를 작성하곤 한다.

interface UserRepository : JpaRepository<User, Long> {
    fun findByEmail(email: String): User?
}

이렇게 인터페이스만 만들어두는데 userRepository.findByEmail(…)을 호출하면 쿼리가 알아서 나간다. 이게 어떻게 동작하는지 문득 궁금하게 되었다.

이 글에서는 UserRepository가 어떻게 Bean으로 등록되고, 실제 쿼리를 실행하기까지 어떤 내부 과정이 있는지를 단계별로 정리해본다.

2. Repository는 언제 Bean이 될까?


우리가 만든 UserRepository 인터페이스에는 @Repository 나 @Component 같은 어노테이션이 붙어있지 않다. 그런데도 스프링이 실행되면 이 인터페이스가 Bean으로 등록되어 주입된다. 어디서, 누가, 어떤 시점에 이 일을 하는지부터 알아보자.

@EnableJpaRepositories

package org.springframework.data.jpa.repository.config;

@Import(JpaRepositoriesRegistrar.class)
public @interface EnableJpaRepositories {...}

Spring Data JPA는 우리가 작성한 Repository 인터페이스를 직접 Bean으로 등록하지 않는다. 대신 애플리케이션 부팅 시 @EnableJpaRepositories 어노테이션이 읽히면 해당 어노테이션에 달려있는 @Import 어노테이션에 의해 JpaRepositoriesRegistrar 클래스에 대한 처리를 하게 된다.

JpaRepositoriesRegistrarImportBeanDefinitionRegistrar 를 구현한 클래스인데 스프링은 이를 만나면 구현된 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);
}