스프링 핵심 원리 (9) - 컴포넌트 스캔 필터와 빈 중복 등록
1. 개요
앞 글에서는 @Component가 붙은 클래스를 자동으로 찾아 빈으로 등록하는 컴포넌트 스캔의 기본 동작을 정리했다. 이번에는 스캔 대상을 원하는 대로 걸러내는 필터 옵션과, 스캔하다 보면 반드시 한 번은 마주치는 빈 이름 충돌 문제를 정리한다.
2. includeFilters / excludeFilters
includeFilters: 스캔 대상에 포함시킬 조건을 추가로 지정한다.excludeFilters: 스캔 대상에서 제외할 조건을 지정한다.
동작을 확인하기 위해 커스텀 애노테이션 두 개를 만들어보자.
1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyIncludeComponent {
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyExcludeComponent {
}
각각을 적용한 클래스를 하나씩 준비한다.
1
2
3
4
5
6
7
@MyIncludeComponent
public class BeanA {
}
@MyExcludeComponent
public class BeanB {
}
이제 설정 클래스에서 두 필터를 함께 지정하면, BeanA는 등록되고 BeanB는 등록되지 않는다.
1
2
3
4
5
6
7
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {
}
1
2
3
4
5
6
7
8
9
10
@Test
void filterScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class));
}
여러 조건을 동시에 걸고 싶다면 배열로 넘기면 된다. 예를 들어 MyIncludeComponent가 붙은 BeanA까지 제외하고 싶다면 excludeFilters에 ASSIGNABLE_TYPE 조건을 하나 더 추가하면 된다.
1
2
3
4
5
6
7
8
9
@ComponentScan(
includeFilters = {
@Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class)
},
excludeFilters = {
@Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
@Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
}
)
3. FilterType 옵션
FilterType에는 다섯 가지 값이 있다.
| 옵션 | 설명 | 예시 |
|---|---|---|
ANNOTATION | 기본값. 지정한 애노테이션이 붙은 대상을 인식 | org.example.SomeAnnotation |
ASSIGNABLE_TYPE | 지정한 타입과 그 자식 타입을 인식 | org.example.SomeClass |
ASPECTJ | AspectJ 패턴으로 매칭 | org.example..*Service+ |
REGEX | 정규 표현식으로 매칭 | org\.example\.Default.* |
CUSTOM | TypeFilter 인터페이스를 직접 구현해서 처리 | org.example.MyTypeFilter |
다만 실무에서는 이 옵션들을 세밀하게 조정할 일이 생각보다 많지 않다. @Component만 붙여도 충분한 경우가 대부분이라 includeFilters를 쓸 일은 거의 없고, excludeFilters도 간혹 특정 클래스를 스캔에서 빼야 할 때만 사용한다. 스프링 부트가 제공하는 컴포넌트 스캔 기본 설정을 그대로 따르는 편이 대체로 무난하다.
4. 자동 빈 등록끼리 이름이 충돌하면
컴포넌트 스캔으로 자동 등록되는 빈끼리 이름이 겹치는 경우는 흔치 않다. 클래스명이 다르면 기본 빈 이름도 다르기 때문이다. 하지만 어떤 이유로든 이름이 같은 빈이 두 번 스캔되면, 스프링은 ConflictingBeanDefinitionException을 던지며 등록을 실패시킨다. 즉 자동 등록끼리는 하나가 다른 하나를 조용히 덮어쓰는 일이 없고, 문제를 바로 드러낸다.
5. 수동 등록과 자동 등록이 충돌하면
문제는 수동으로 등록한 빈과 자동으로 스캔된 빈의 이름이 같을 때다.
1
2
3
@Component
public class MemoryMemberRepository implements MemberRepository {
}
1
2
3
4
5
6
7
8
9
10
11
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
MemoryMemberRepository는 @Component로 스캔되어 memoryMemberRepository라는 이름으로 등록되는 동시에, AutoAppConfig의 @Bean(name = "memoryMemberRepository")로도 등록된다. 이렇게 자동 등록과 수동 등록의 이름이 겹치면, 이번에는 예외 없이 수동 등록이 자동 등록을 덮어쓴다. 이때 다음과 같은 로그가 남는다.
1
Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
개발자가 의도를 갖고 특정 빈을 오버라이딩한 것이라면 문제가 되지 않는다. 하지만 실제로는 여러 설정이 얽히다가 의도치 않게 이런 상황이 만들어지는 경우가 더 많다. 어떤 빈이 실제로 사용되는지 코드만 봐서는 바로 드러나지 않기 때문에, 추적하기 까다로운 버그로 이어지기 쉽다.
그래서 최근 스프링 부트는 기본 정책을 바꿔서, 수동 등록과 자동 등록이 충돌하면 애플리케이션 실행 자체를 실패시킨다. 이때 뜨는 에러 메시지는 다음과 같다.
1
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
빈 이름을 바꾸거나, 정말 오버라이딩이 필요하다면 spring.main.allow-bean-definition-overriding=true를 명시적으로 설정해야만 예전처럼 동작한다.
6. 정리
컴포넌트 스캔은 includeFilters/excludeFilters로 대상을 세밀하게 조정할 수 있지만, 스프링이 제공하는 기본 스캔 규칙을 그대로 따르는 경우가 대부분이고 필터를 직접 다룰 일은 많지 않다.
빈 이름 충돌은 성격이 다르게 처리된다. 자동 등록끼리 겹치면 ConflictingBeanDefinitionException으로 바로 실패하지만, 수동 등록과 자동 등록이 겹치면 과거에는 수동 등록이 조용히 우선권을 가져갔다. 이 조용한 오버라이딩이 잡기 어려운 버그의 원인이 되기 쉬워서, 최근 스프링 부트는 기본값을 바꿔 이 충돌도 애플리케이션 실행 실패로 처리한다.