컴포넌트 스캔(ComponentScan)
이전 글을 통해 @Configuration 애노테이션을 통한 스프링 빈 등록에 대해 알아보았다.
하지만 꼭 빈을 등록할 때, @Configuration, @Bean 애노테이션을 선언해야만 할까?
그렇지 않다.
우리는 스프링이 지원하는 컴포넌트 스캔이라는 기능을 통해 아주 간편하게 스프링 컨테이너에 빈 등록이 가능하다.
컴포넌트 스캔 (Component Scan) 이란?
컴포넌트 스캔은 스캔 범위 내에 존재하는 @Component 애노테이션이 붙은 클래스를 스프링 빈으로 등록하기 위한 스캔 과정이다.
컴포넌트 스캔은 @ComponentScan 애노테이션을 통해 설정이 가능하다.
@SpringBootApplication
스프링부트 프로젝트 생성 시, Main 클래스에 설정되는 @SpringBootApplication 애노테이션은
내부에 @ComponentScan 애노테이션을 기본으로 가지고 있다.
덕분에 우리는 따로 @ComponentScan 애노테이션을 선언하지 않고도 @Component 애노테이션이 붙어있는 클래스들을 스프링 컨테이너에 빈으로 등록할 수 있던 것이다.
컴포넌트 스캔이 작동하는 애노테이션 종류
컴포넌트 스캔은 @Component 애노테이션이 붙어있는 클래스에만 해당되는 이야기라고 하였다.
하지만 @Component 애노테이션 외에도 몇 가지 애노테이션도 컴포넌트 스캔 범위에 해당된다.
그 종류는 바로 @Controller, @RestController, @Service, @Configuration, @Repository 이다.
이 애노테이션들은 왜 컴포넌트 스캔에 포함될까?
@Repository 살펴보기
예시로 사진을 가져와본 Repository 애노테이션의 코드를 보면 알 수 있듯이
해당 애노테이션들은 내부에 @Component 애노테이션을 포함하고 있다.
때문에 컴포넌트 스캔 범위에 해당이 되는 것이며, 우리는 이들을 스프링 컨테이너의 가호 아래에서 싱글톤으로 사용할 수 있는 것이다.
컴포넌트 스캔의 탐색 범위
그렇다면 컴포넌트 스캔의 탐색 범위는 어떻게 될까?
대규모 프로젝트라면 컴포넌트 스캔을 위해 모든 자바 클래스를 탐색하는 것은 비용 낭비일 것이다.
컴포넌트 스캔 basePackage 설정
@Configuration
@ComponentScan(
basePackages = "com.example"
)
public class AutoAppConfig {
}
컴포넌트 스캔은 위와 같이 basePackage 설정을 통해 컴포넌트 스캔의 기반 패키지를 설정할 수 있다.
basePackage를 설정하게 되면, 설정된 패키지와 그 하위의 패키지의 클래스들을 모두 탐색하며 컴포넌트 스캔을 수행한다.
이를 통해 프로젝트의 모든 클래스를 찾지 않고 필요한 부분만 컴포넌트 스캔을 수행할 수 있다.
@SpringBootApplication의 basePackage 설정
@SpringBootApplication(
scanBasePackages = ""
)
SpringBootApplication 또한 내부에 @ComponentScan 애노테이션을 가지고 있기 때문에,
scanBasePackages 이라는 속성을 통하여 컴포넌트 스캔 범위를 설정할 수 있도록 되어있다.
컴포넌트 스캔의 Default basePackage
만약 위와 같이 컴포넌트 스캔 범위를 지정해주지 않는다면, @ComponentScan 애노테이션을 선언해준 클래스의 패키지를 basePackage로 설정한다.
때문에 해당 클래스가 존재하는 패키지를 포함하여, 그 하위의 패키지에 있는 클래스들에 대해서 컴포넌트 스캔이 이루어진다.
스프링 신 김영한 선생님의 권장 방식
김영한님께서 개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것 이라고 한다.
basePackageClasses 옵션
@Configuration
@ComponentScan(
basePackageClasses = {
AutoAppConfig.class
}
)
public class AutoAppConfig {
}
basePackages 외에 basePackageClasses 라는 속성도 존재하는데,
이 속성은 특정 클래스를 설정하면 그 클래스가 위치한 패키지와 그 하위 패키지에 대해 컴포넌트 스캔을 수행하여 빈을 등록한다.
컴포넌트 스캔 탐색 제외(exclude) / 포함(include) 속성
컴포넌트 스캔은 basePackages와 basePackageClasses 속성 외에도
특정 부분에 대해 컴포넌트 스캔을 제외(exclude) 시킬 수 있는 excludeFilters와
특정 부분에 대해 컴포넌트 스캔을 포함(include)시킬 수 있는 includeFilters 속성이 존재한다.
includeFilters
먼저 includeFilters에 대해 살펴보자
@Configuration
@ComponentScan(
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
}
)
public class AutoAppConfig {
}
includeFilters 속성을 @ComponentScan 애노테이션의 속성으로 추가하게 되면,
해당 속성에 정의한 type, classes를 바탕으로 컴포넌트 스캔에서 포함(include) 시킬 클래스를 정의할 수 있다.
위 코드와 같이 includeFilters 옵션을 사용하게 되면,
컴포넌트 스캔 범위에 해당하는 클래스 + @Configuration 애노테이션이 붙은 클래스를 스프링 빈으로 등록하게 된다.
excludeFilters
@Configuration
@ComponentScan(
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
}
)
public class AutoAppConfig {
}
excludeFilters 속성을 @ComponentScan 애노테이션의 속성으로 추가하게 되면,
해당 속성에 정의한 type, classes를 바탕으로 컴포넌트 스캔에서 제외(exclude) 시킬 클래스를 정의할 수 있다.
위 코드와 같이 excludeFilters 옵션을 사용하게 되면,
컴포넌트 스캔 범위에 해당하는 클래스를 탐색하되, @Configuration 애노테이션이 붙은 클래스를 발견하면 스캔에서 제외하고
이외의 것들만 스프링 빈으로 등록한다.
FilterType의 종류
FilterType이 Enum으로 되어있기에, 어떤 종류가 있는 지 궁금해서 정리해보았다.
FilterType Enum 클래스는 다음과 같은 열거 상수들을 가지고 있다.
- ANNOTATION : 지정된 애노테이션을 가지고 있는 클래스를 탐색
- ASSIGNABLE_TYPE : 지정된 클래스 또는 그 클래스를 상속하는 클래스들을 탐색
- ASPECTJ : AspectJ 패턴을 기반으로 빈을 탐색
- REGEX : 정규 표현식을 사용하여, 정규 표현식 패턴에 일치하는 이름의 클래스들을 탐색
- CUSTOM : 사용자 정의 필터 로직을 구현하여 원하는 방식으로 빈을 선택적으로 탐색
(TypeFilter 인터페이스를 구현해서 처리)
컴포넌트 스캔 중복
컴포넌트 스캔 시에, 동일한 이름의 빈 등록이 발생하면 어떻게 될까?
1. 자동 빈 등록 vs 자동 빈 등록
컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 만약 같은 이름으로 설정한 빈이 존재하면
스프링은 ConflictingBeanDefinitionException 예외를 발생시킨다.
2. 자동 빈 등록 vs 수동 빈 등록
그렇다면 만약 자동 빈 등록과 수동 빈 등록을 함께 사용할 때 빈 이름이 겹치면 어떻게 될까?
이전 스프링은 수동 빈이 자동 빈 보다 우선권을 가지도록 하였었다.
하지만 지금의 스프링은 자동 빈 vs 자동 빈 등록 케이스와 마찬가지로 예외를 발생시킨다.
@Configuration
@ComponentScan(
excludeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
}
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
다음과 같이 memoryMemberRepository를 @Configuration 클래스 내부에서 Bean으로 등록하고
@Repository
public class MemoryMemberRepository implements MemberRepository {
}
이와 동일한 MemoryMemberRepository를 @Repository 애노테이션을 통해 컴포넌트 스캔 범위로 잡아 스프링 빈으로 등록한다.
Bean의 기본 네이밍 패턴은 클래스명에서 맨 앞자만 소문자로 변경하는 형태이다.
때문에 위 Config에 수동으로 등록한 빈의 이름("memoryMemberRepository")과 동일함을 인지할 수 있다.
과거 스프링은 이러한 경우에
아래와 같은 로그를 출력하고 수동 빈 등록이 우선권을 가지게 하였다.
(수동 빈이 자동 빈을 오버라이드 해버렸다.)
Overriding bean definition for bean 'memoryMemberRepository' with a different
definition: replacing
하지만 현재 스프링은 스프링 빈의 이름이 같다면 예외를 발생시킨다.
그 이유는 대부분의 빈 네임 중복은 개발자의 의도가 아닌 휴먼 에러에 의해서 발생했던 것이었는데,
이로 인하여 나도 모르는 사이에 빈 네임 중복으로 인해 스프링 빈이 2개 중 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`로 설정하면 Bean 오버라이딩이 가능해질 것이라고 한다.
실제로 해당 옵션을 yaml 또는 properties 파일에 추가해주면
수동 빈 등록 vs 자동 빈 등록 상황에서 빈 네이밍이 중복되어도 예외가 발생하지 않고
수동 빈이 자동 빈을 오버라이드 하도록 설정이 가능하다.