스프링 빈을 등록할 때는 자바 코드의 @Bean
또는 XML의 <bean>
을 통해 직접 등록할 스프링 빈을 나열했다.
예제에선 4개 정도로 적었지만 실제 서비스에서는 몇 배로 늘어나게 될 것이고, 이는 누락 또는 반복 문제가 발생한다.
그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.
또 의존관계도 자동으로 주입하는 @Autowired
기능도 제공한다.
@Configuration
@ComponentScan( // @Component가 붙은 모든 클래스들을 스캔한다.
// basePackages = "hello.core.member", // 해당 패키지 하위만 찾아서 컴포넌트 스캔한다.
// basePackageClasses = AutoAppConfig.class, // 지정한 클래스의 패키지를 탐색 시작 위치로 지정
// 위 2가지 다 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
// excludeFilters == 내가 스캔하고 싶지 않은것들을 제거
)
public class AutoAppConfig {
}
@Configuration
을 통해 설정 파일로 설정하고, @ComponentScan
을 작성하면 탐색 위치에 있는 클래스들 중 @Component
가 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
@Comfiguration
이 컴포넌트의 스캔 대상이 된다.@Comfiguration
안에 @Component
가 붙어 있기 때문!이전 AppCofig
에서는 @Bean
으로 직접 설정 정보를 작성한 뒤, 의존관계도 직접 명시했다.
/**
* 의존관계 직접 명시
*/
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Component
를 사용하면 의존관계를 직접 명시하지 않고, @Autowired
를 사용해 의존관계를 자동으로 주입할 수 있다.
@Component
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Autowired // == ac.getBean(MemberService.class)
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Autowired
를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다!제대로 의존관계가 주입되는지 직접 확인해보자.
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
로그를 확인하니 컴포넌트 스캔과 의존관계 주입이 제대로 된 것을 확인할 수 있다.
@ComponentScan
은 @Component
가 붙은 모든 클래스를 스프링 빈으로 등록한다.
@Component("원하는 이름")
처럼 작성하면 된다.생성자에 @Autowired
를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
모든 자바 클래스와 부가 라이브러리를 다 컴포넌트 스캔하는 일을 상상해보자.
얼마나 오래 걸리고 끔찍한가? 그래서 꼭 필요한 위치만 탐색하도록 시작 위치를 지정할 수 있다.
@Configuration
@ComponentScan( // @Component가 붙은 모든 클래스들을 스캔한다.
basePackages = "hello.core.member", // 해당 패키지 하위만 찾아서 컴포넌트 스캔한다.
basePackageClasses = AutoAppConfig.class, // 지정한 클래스의 패키지를 탐색 시작 위치로 지정
//만약 위 2가지 다 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
)
public class AutoAppConfig {
...
}
basePackages
: 탐색할 패키지의 시작 위치를 지정한다. 해당 위치 ~ 하위 패키지까지 모두 탐색한다.basePackageClasses
: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.@ComponentScan
이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다. 최근 스프링 부트는 설정 정보 클래스(@Comfiguration
)의 위치를 프로젝트 최상단에 두는 방법을 권장한다.
컴포넌트의스캔은 @Component
뿐만 아니라 다음과 같은 애노테이션도 추가로 대상에 포함한다.
@Configuration
: 스프링 설정 정보에서 사용@Controller
: 스프링 MVC 컨트롤러에서 사용@Service
: 스프링 비즈니스 로직에서 사용@Repository
: 스프링 데이터 접근 계층에서 사용참고로 애노테이션에는 상속관계는 없다.
한 애노테이션이 특정 애노테이션을 들고 있는 것이 인식되는 것은 자바 언어가 지원하는 기능이 아니라,
스프링이 지원하는 기능이다.
컴포넌트 스캔 대상을 추가로 지정하거나, 제외할 수 있는 기능이다.
@MyIncludeComponent
public class BeanA {
}
@MyExcludeComponent
public class BeanB {
}
두 Component가 존재하고, 설정 정보 클래스의 @ComponentScan
은 아래와 같다.
public class ComponentFilterAppConfigTest {
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig {
}
}
위 컴포넌트와 설정 정보가 담긴 파일 경로는 다음과 같을 때, 이때 기본 탐색 전략을 통해 결과를 유추해보면 ComponentFilterAppConfigTest.class
와 같은 경로인 BeanA
, BeanB
모두 탐색 될 것이다.
실제 테스트 코드를 통해 확인해보자
@Test
void basicScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
BeanB beanB = ac.getBean("beanB", BeanB.class);
assertThat(beanA).isNotNull();
assertThat(beanB).isNotNull();
}
테스트 실패다. 이유는 간단하다.
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
설정 파일 클래스에서 위 필터 조건을 걸었기 때문에 해당 컴포넌트 클래스는 제외하고 컴포넌트 스캔이 이루어졌다.
그렇다면 BeanA
는 빈으로 등록되고, BeanB
는 등록되지 않는다는 가정으로 테스트를 진행하자.
@Test
void filterScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
assertThat(beanA).isNotNull();
org.junit.jupiter.api.Assertions.assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("beanB", BeanB.class));
}
시원하게 성공이다.
위 조건 코드는 FilterType으로 5가지 옵션이 존재한다.
주로 ANNOTATION를 사용하고 가끔 ASSIGNABLE_TYPE을 사용한다니 간단하게 2가지만 알아두면 될 것 같다.
컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?
다음 두가지 상황이 있다.
1. 자동 빈 등록 vs 자동 빈 등록
2. 수동 빈 등록 vs 자동 빈 등록
컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우에는 스프링은 오류를 발생시킨다.
ConflictingBeanDefinitionException
예외 발생수동으로 빈을 등록시켰다는건 개발자의 의도가 더 들어가 있다고 판단하여, 수동 빈 등록이 우선권을 가져 자동 빈을 오버라이딩 한다.
@Configuration
은 @Component
를 포함하고 있으며, 이때 애노테이션 간의 상속관계는 존재하지 않고 스프링이 제공하는 기능이다.@ComponentScan
는 별도의 설정 정보 없이 자동으로 스프링 빈을 등록시켜주는 기능이며, @Autowired
을 통해 의존관계를 자동으로 주입하는 기능과 함께 사용할 수 있다.@ComponetScan
는 @Componet
를 탐색해 싱글톤 패턴을 유지하면서 스프링 빈으로 등록하는데 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자로 변경해 사용한다.이 부분도 마찬가지로 지난 해커톤과 사이드 프로젝트를 진행할 때 아무 생각없이 @Component
와 @Controller
를 함께 작성한 적이 있었다. 아마 오류는 발생하지 않았던걸로 기억하는데... 참.. 컴포넌트 스캔을 공부한 오늘 뒤돌아 생각하면 말도 안되는 코드라고 생각한다..
김영한 강사님의 스프링 기본편을 듣기 전에 너무 어려운 개념들이면 어쩌지.. 라는 생각이 있는데 아직까지는 너무 어렵지 않고, 글로 다시 정리하며 차근차근 생각해보니 잘 이해되고 연결이 잘 된다.
더 열심히 공부해서 과거의 이상한 코드를 작성하지 않고 알맞게 작성할 수 있도록 하자!