스프링 핵심 원리 - 컴포넌트 스캔
시작
컴포넌트 스캔
지금까지는 스프링 빈을 등록할 때 자바 코드의 @Bean이나 XML의 <bean>
등을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다.
하지만 이렇게 등록해야 할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기도 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생한다.
그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
또 의존관계도 자동으로 주입하는 @Autowired
라는 기능도 제공한다.
컴포넌트 스캔(@ComponentScan
)은 이름 그대로 @Component
애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다. @Component
를 붙여야 하는 이유가 바로 이것이다.
이제 컴포넌트 스캔과 의존관계 자동 주입이 어떻게 동작하는지 자세히 알아보자.
먼저 기존 AppConfig
말고도 AutoAppConfig
클래스를 만들어보자.
- 컴포넌트 스캔을 사용하려면
@ComponentScan
을 사용해야 한다. - 기존 AppConfig 클래스는 비어있게 만들어두자.
// AppConfig, TestConfig 등 만들어 놓은 설정(@Configuration) 정보는 컴포넌트 대상에서 제외해주도록 하자.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
@Configuration 는 @Component 애노테이션을 포함하고 있기 때문에 컴포넌트 스캔의 대상이 된다.
테스트를 위해 기존 MemoryMemberRepository, RateDiscountPolicy, MemberServiceImpl, OrderServiceImpl 에 @Component 를 붙여주고, 의존성을 주입해야 하는 부분은 생성자에 @Autowired 를 붙여 자동 주입하도록 하자.
package hello.core.member;
@Component
public class MemoryMemberRepository implements MemberRepository {
// ...
}
package hello.core.discount;
@Component
public class RateDiscountPolicy implements DiscountPolicy {
// ...
}
package hello.core.member;
@Component // 컴포넌트 스캔의 대상이 되도록 지정
public class MemberServiceImpl implements MemberService {
// 회원 저장소 의존성 주입을 위한 필드
private final MemberRepository memberRepository;
@Autowired // ac.getBean(MemberRepository.class) 자동 의존관계 주입 (스프링 컨테이너에서 MemberRepository 타입의 빈을 찾아서 주입)
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// ...
}
package hello.core.order;
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
// ...
}
이제 테스트 코드를 작성해보자.
package hello.core.scan;
public class AutoAppConfigTest {
@Test
void basicScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
결과 로그는 다음과 같다.
ClassPathBeanDefinitionScanner - Identified candidate component class:
.. RateDiscountPolicy.class
.. MemberServiceImpl.class
.. MemoryMemberRepository.class
.. OrderServiceImpl.class
이 로그를 보면 컴포넌트 스캔이 잘 동작하고 있음을 알 수 있다.
정리해보면 다음과 같다.
@ComponentScan
- 컴포넌트 스캔은 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
- 빈의 기본 이름은 클래스명을 사용화되 맨 앞글자만 소문자로 변경한 이름을 사용한다. ex)
MemberServiceImpl
->memberServiceImpl
- 빈 이름을 직접 지정하고 싶으면
@Component("이름")
으로 지정할 수 있다.
- 빈 이름을 직접 지정하고 싶으면
@Autowired
- 생성자에 @Autowired 를 붙이면 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
- 기본 조회 전략은 타입이 같은 빈을 찾아 주입한다.
getBean(MemberRepository.class)
와 같은 코드를 자동으로 처리해준다.
의존관계 주입
@Configuration
과@Bean
으로 직접 등록하고 의존관계를 설정하던 부분을@Component
와@Autowired
를 이용해 자동으로 해결한다. 생성자에 파라미터가 많아도 다 찾아서 주입해준다.
탐색 위치와 기본 스캔 대상
@ComponentScan
에서 지정한 기본 패키지는 해당 패키지와 하위 패키지를 모두 탐색한다.
- basePackages 속성을 이용해 탐색할 패키지를 지정할 수 있다.
- 이 속성을 적용하면 해당 패키지와 하위 패키지를 모두 탐색한다. 범위를 지정함으로써 탐색 범위를 좁힐 수 있다.
- 중괄호를 이용해 여러 패키지를 지정할 수 있다. ex) @ComponentScan(basePackages = hello.service)
- 이 옵션을 지정하지 않으면
@ComponentScan
이 붙은 설정 정보 클래스의 패키지 위치가(package hello.core
) 기본으로 탐색 대상이 된다. - 따라서 설정 정보 파일은 프로젝트 최상단에 위치해야 한다.
사실 스프링을 사용하면 대부분 컴포넌트 스캔을 사용한다 @SpringBootApplication
애노테이션을 사용하면 스프링 부트가 자동으로 설정 정보를 읽어와서 스프링 컨테이너를 생성하고 스프링 빈을 등록한다.
내부를 보면 @ComponentScan
애노테이션을 포함하고 있기 때문이다.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
// ...
}
컴포넌트 스캔의 기본 대상
컴포넌트 스캔은 @Component
뿐 아니라 다음 내용도 추가로 대상에 포함한다.
@Component
: 컴포넌트 스캔에서 사용@Controller
: 스프링 MVC 컨트롤러로 인식 (내부를 보면@Component
애노테이션을 포함하고 있다.)@Service
: 스프링 비즈니스 로직에서 사용 (내부를 보면@Component
애노테이션을 포함하고 있다.)@Repository
: 스프링 데이터 접근 계층에서 사용 (내부를 보면@Component
애노테이션을 포함하고 있다.)@Configuration
: 스프링 설정 정보에서 사용 (내부를 보면@Component
애노테이션을 포함하고 있다.)
사실 애노테이션은 상속관계가 아니다. 그래서 이렇게 애노테이션이 특정 애노테이션을 들고 있는 것을 인식할 수 있는 것은 자바가 아닌 스프링이 지원하는 기능이다.
또한 컴포넌트 스캔 용도 뿐 아니라 다음 애노테이션이 있으면 부가 기능을 수행한다.
@Controller
스프링 MVC 컨트롤러로 인식@Repository
스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환해준다.@Configuration
스프링 설정 정보로 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.@Service
: 특별한 부가 기능을 수행하지는 않지만 개발자들이 비즈니스 계층을 인식하는데 도움을 준다.
참고
useDefaultFilters
속성을 false로 설정하면 기본 스캔 대상에서 제외할 수 있다.
필터
이번에는 includeFilters
와 excludeFilters
를 사용해서 컴포넌트 스캔 대상을 추가하거나 제외해보자.
아래와 같은 코드를 추가한다.
// MyIncludeComponent 어노테이션
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}
// MyExcludeComponent 어노테이션
package hello.core.scan.filter;
import java.lang.annotation.*;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
}
// BeanA
package hello.core.scan.filter;
@MyIncludeComponent
public class BeanA {
}
// BeanB
package hello.core.scan.filter;
@MyExcludeComponent
public class BeanB {
}
테스트 코드를 통해 커스텀으로 작성한 애노테이션이 잘 동작하는지 확인해보자.
public class ComponentFilterAppConfigTest {
@Test
void filterScan() {
// 스프링 컨테이너 생성, ComponentFilterAppConfig.class를 구성 정보로 사용
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
// BeanA는 @MyIncludeComponent가 붙어있어 스프링 빈으로 등록됨
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
// BeanB는 @MyExcludeComponent가 붙어있어 스프링 빈으로 등록되지 않음
// 따라서 NoSuchBeanDefinitionException 예외가 발생해야 함
Assertions.assertThrows(
NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class)
);
}
// 컴포넌트 스캔 설정을 위한 Config 클래스
@Configuration
@ComponentScan(
// MyIncludeComponent 애노테이션이 붙은 클래스는 컴포넌트 스캔 대상에 포함
includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
// MyExcludeComponent 애노테이션이 붙은 클래스는 컴포넌트 스캔 대상에서 제외
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {
}
}
만일 클래스를 직접 선택해 필터에서 제외하도 싶다면 excludeFilters
속성에 다음과 같은 코드를 추가한다.
@ComponentScan(
// ...
excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = BeanA.class)
)
중복 등록과 충돌
컴포넌트 스캔에서 같은 빈 이름을 중복해서 등록하면 어떻게 될까? 다음 두 가지 경우가 있다.
- 자동 빈 등록 vs 자동 빈 등록
- 수동 빈 등록 vs 자동 빈 등록
자동 빈 등록 vs 자동 빈 등록
- 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 이름이 같은 경우 예외가 발생한다.
수동 빈 등록 vs 자동 빈 등록
- 수동 빈 등록이 우선권을 가진다. Override 된다.
이미 스프링에 의해 MemoryMemberRepository
빈이 등록되어 있는데, 이를 수동으로 등록한 MemoryMemberRepository
빈이 중복되면 수동 빈 등록이 우선권을 가져서 수동 빈이 등록된다.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
MemberRepository memoryMemberRepository() {
return new MemoryMemberRepository();
}
}
로그를 보면 다음과 같다.
Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing
물론 개발자 의도적으로 이런 결과를 기대했다면 수동이 우선권을 가지는게 맞지만 현실은 개발자가 의도적으로 설정해 이런 결과가 만들어지기 보다 여러 설정이 꾜여 이런 결과가 만들어지는 경우가 대부분이다.
이런 버그의 경우 그야말로 잡기 어려운 버그
이다.
이 경우 스프링은 다음과 같은 에러 로그를 제공해준다.
2025-01-19T15:47:22.492+09:00 ERROR 12484 --- [core] [ main] o.s.b.d.LoggingFailureAnalysisReporter :
***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'memoryMemberRepository', defined in class path resource [hello/core/AutoAppConfig.class], could not be registered. A bean with that name has already been defined in file [/Users/wooglim/dev/spring/spring-general/core/build/classes/java/main/hello/core/member/MemoryMemberRepository.class] and overriding is disabled.
Action:
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
를 설정해주면 된다.
이 옵션을 활성화하면 수동 빈 등록이 우선권을 가지게 된다. 다만 이 옵션을 활성화하면 예상치 못한 문제가 발생할 수 있으므로 주의해야 하며 스프링 또한 이 옵션을 디폴트로 사용하지 않는다.