Spring Data JPA를 사용하다보면, JPARepository<T, ID>
를 implement하는 인터페이스는 @Repository
어노테이션이 필요하지 않습니다.
예를들어 다음과 같은 인터페이스는 @Repository
어노테이션을 사용하지 않아도 빈으로 등록되며, DI가 가능합니다.
interface PostRepository : JpaRepository<Post, Long>
이게 왜 가능할까요?
Spring Boot를 사용하면 디폴트로 설정되어 있는 어노테이션으로 다음과 같이 설정되어있습니다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(JpaRepositoriesRegistrar.class)
public @interface EnableJpaRepositories
여기서 중요한 것은 JpaRepositoriesRegistrar.class
로, 이 클래스는 JpaRepository
들을 이름과 같이 Register하는 역할을 합니다.
즉! JpaRepository
또는 JpaRepository
를 implement하는 인터페이스 혹은 클래스가 자동으로 등록되는 것입니다.
기본적으로 Spring Boot에서는 EnableJpaRepository의 컴포넌트 스캔의 범위는 Spring Boot Application의 base class 부터입니다.
근데 갑자기 든 생각!
이 설정이 자동으로 JpaRepository interface 그 자체까지 빈으로 등록하면 안될 것 같지 않나요?
빈으로 등록하면 안될 것 같은데.. 궁금하니 찾아봅시다!
@NoRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExecutor<T> {
일단 인터페이스에 들어가보니 @NoRepositoryBean
이라는 어노테이션이 있는데, 얘가 이름부터 의심이 갑니다.
직접 클래스 안으로 들어가서 찾아봅시다.
Annotation to exclude repository interfaces from being picked up and thus in consequence getting an instance being created.
Javadoc을 읽어보니 역시나 의심을 했던대로였습니다.
이 @NoRepositoryBean
은 repository interface가 instance로 생성되지 않게 스캔에서 제외되도록하는 어노테이션이라는 설명이 있습니다.
@NoRepositoryBean
살펴보기이제 위에 @NoRepositoryBean
의 Javadoc 까지 읽었으니.. 그만 살펴볼 때도 됐지만 그래도 궁금합니다.
어떻게 도대체 이게 가능한지, 더 살펴봅시다.
인텔리제이에 아무생각없이 프로젝트 하나를 켜고 @NoRepositoryBean
을 검색해봅시다.
뭔가 의심이 가는 이름의 메소드를 발견했습니다.
addExcludeFilter
딱 봐도 제외를 위한 메소드입니다.
이 메소드가 있는 RepositoryComponentProvider
클래스의 Javadoc에서 얘기하고 있습니다.
scanning for interfaces extending the given base interface Skips interfaces annotated with {@link NoRepositoryBean}.
NoRepositoryBean
이 붙어있는 인터페이스를 스킵한다고 되어있습니다!!
public void addExcludeFilter(TypeFilter excludeFilter) {
this.excludeFilters.add(0, excludeFilter);
}
그래서 addExcludeFilter
를 살펴보면.. excludeFilters
에 filter를 추가해주고 있습니다.
이제 excludeFilters에 NoRepositoryBean을 Filtering하는 Filter를 넣는 것을 보았으니, 어디에 어떻게 쓰이는지 알아봅시다!
/**
* Determine whether the given class does not match any exclude filter
* and does match at least one include filter.
* @param metadataReader the ASM ClassReader for the class
* @return whether the class qualifies as a candidate component
*/
protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
for (TypeFilter tf : this.excludeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return false;
}
}
for (TypeFilter tf : this.includeFilters) {
if (tf.match(metadataReader, getMetadataReaderFactory())) {
return isConditionMatch(metadataReader);
}
}
return false;
}
excludeFilters를 사용하는 것을 찾아보니, isCandidateComponent
라는 이름의 메소드가 있습니다.
후보가 될 수 있는 컴포넌트냐 묻는 함수라니.. 누가봐도 의심스럽습니다.
코드를 보니 excludeFilters의 TypeFilter들을 순회하면서 metadata의 정보와 타입이 일치하면 false를 return하는 것 같습니다.
또한 includeFilters에 있는 애들은 condition 까지 맞춰서 match하는지 체크하는 것 같네요.
즉, excludeFilters에 있으면 어쨌든 후보가 될 수 없다! 하는 겁니다.
그래서 결국 컴포넌트 스캔을 위해 addCandidateComponents
라는 함수에서 제외되게 되는 것이구요!
이렇게 이번 글에서는 왜 @Repository 라는 어노테이션 없이 JpaRepository를 impl하는 인터페이스가 빈으로 등록되는지.. 그리고, JpaRepository 자체는 왜 빈으로 등록되지 않는지를 알아봤습니다.
생각보다 딥하게 온 것 같은데, 나름 재밌어서 비슷하게 딥다이브 해보는 글을 꾸준히 써보려고합니다.
어쨌든 정리하면 다음과 같습니다.
JpaRepositry 를 impl 하는 클래스는 EnableJpaRepositories + JpaRepositoriesRegisterar에 의해 자동으로 빈으로 등록된다.
JpaRepository 인터페이스는 @NoRepositoryBean
어노테이션에 의해 인터페이스 그 자체가 빈으로 등록될 수 없게 설정해준다.
NoRepositoryBean 을 설정하면, 여러 과정을 거쳐 excludeFilters를 이용하여 NoRepositoryBean 어노테이션이 달린 애들을 알아서 걸러준다.