6. 컴포넌트 스캔

jinhxxxxkim·2023년 2월 1일
0

Spring핵심

목록 보기
6/9
post-thumbnail

6. 컴포넌트 스캔

1. 컴포넌트 스캔과 의존관계 자동 주입

  • 지금까지의 방식은 스프링 빈을 직접 스프링 컨테이너에 등록하는 방식이다
  • 자바 코드의 @Bean, XML의 <bean>을 사용하여 등록하였다.
  • 만일 등록해야 하는 빈의 수가 많다면 여러가지 문제가 발생할 것이다.
  • 따라서 스프링은 설정정보 없이 스프링 빈을 등록하는 컴포넌트 스캔이라는 방식을 지원한다.
  • 또한 의존관계도 자동으로 주입해주는 @Autowired 기능을 제공한다.

코드

AutoAppConfig 클래스

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {}
  • 기존의 AppConfig 클래스를 수정하지 않고 새로운 클래스를 선언하였다.
  • 컴포넌트 스캔을 사용하기 위해 @ComponentScan을 설정정보에 추가한다.
  • 기존의 AppConfig와 달리 @Bean이 존재하지 않는다.
  • 컴포넌트 스캔은 @Component 어노테이션이 붙은 클래스를 스캔하여 스프링 빈으로 등록한다.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration { ... }
  • @Configuration의 내부를 보면, @Component 어노테이션이 존재하는 것을 확인할 수 있다.
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
  • 이전에 사용하던 설정정보들에 @Configuration 어노테이션이 붙어있다.
  • 따라서 해당 예제에서 컴포넌트 스캔 시 설정정보는 컴포넌트 스캔에서 제외시키기 위해 excludeFilters를 사용하였다.
@Component
public class MemoryMemberRepository implements MemberRepository {...}
@Component
public class RateDiscountPolicy implements DiscountPolicy {...}
  • 각 클래스가 컴포넌트 스캔의 대상이 되도록 @Component를 추가해준다.
@Component
public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    ...
}
@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;
    }
	...
}
  • 이전에 AppConfig에서는 @Bean 으로 직접 설정 정보를 작성했고, 의존관계도 직접 명시했다.
  • 하지만 컴포넌트 스캔에서는 이러한 설정 정보 자체가 없기 때문에, 의존관계 주입도 이 클래스 안에서 해결해야 한다.
  • 의존관계 주입은 @Autowired를 사용해 자동으로 주입해준다.
  • @Autowired 를 사용하여 생성자에서 여러 의존관계도 한번에 주입받는다.

컴포넌트 스캔 Test

@Test
void basicScan(){
	AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
    MemberService memberService = ac.getBean(MemberService.class);
   
    Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
  • 기존과 동일하게 AnnotationConfigApplicationContext를 사용하여 스프링 컨테이너를 생성한다.
  • 설정 정보로 AutoAppConfig 클래스를 넘겨준다.

로그

16:22:04.759 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\SpringStudy\core\out\production\classes\hello\core\discount\RateDiscountPolicy.class]
16:22:04.765 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\SpringStudy\core\out\production\classes\hello\core\member\MemberServiceImpl.class]
16:22:04.767 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\SpringStudy\core\out\production\classes\hello\core\member\MemoryMemberRepository.class]
16:22:04.770 [main] DEBUG org.springframework.context.annotation.ClassPathBeanDefinitionScanner - Identified candidate component class: file [C:\SpringStudy\core\out\production\classes\hello\core\order\OrderServiceImpl.class]
...
16:22:04.994 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'autoAppConfig'
16:22:05.004 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'rateDiscountPolicy'
16:22:05.006 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memberServiceImpl'
16:22:05.034 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'memoryMemberRepository'
ClassPathBeanDefinitionScanner - Identified candidate component class
  • ClassPathBeanDefinitionScanner가 컴포넌트 클래스를 식별함을 띄워주며, 해당 클래스의 위치를 알려준다.
DefaultListableBeanFactory - Creating shared instance of singleton bean XXX
  • 해당 빈"XXX"을 싱글톤으로 생성했다고 알려준다.

동작 원리

@ComponentScan

  • @ComponentScan@Component가 붙은 모든 클래스를 스프링 빈으로 등록시킨다.
  • 스프링 빈의 기본 이름은 클래스 명을 사용하되, 맨 앞글자만 소문자를 사용한다.
  • 직접 이름을 부여할 수 있다.

@Autowired 의존관계 자동주입

  • 생성자에 @Autowired 를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
  • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
  • getBean(MemberRepository.class) 와 동일하다고 이해하면 된다.

  • 생성자에 파라미터가 많아도 다 찾아서 자동으로 주입한다.

2. 탐색 위치와 기본 스캔 대상

  • 모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래 걸린다.
  • 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.
@ComponentScan(
 	basePackages = "hello.core",
}
  • basePackages : 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
basePackages = {"hello.core", "hello.service"}
  • 여러 패키지를 지정할 수 있다
  • basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
  • defaul: @ComponentScan 이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다
  • 설정 정보 클래스의 위치를 프로젝트 최상단에 두어 패키지 위치를 지정하지 않는 방법을 권장한다.

컴포넌트 스캔 기본 대상

  • @Component : 컴포넌트 스캔에서 사용
  • @Controlller : 스프링 MVC 컨트롤러에서 사용
  • @Service : 스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration : 스프링 설정 정보에서 사용
  • 위의 어노테이션은 @Component를 포함한다.

부가기능

  • @Controlller : 스프링 MVC 컨트롤러로 인식
  • @Service : 개발자들이 핵심 비즈니스 로직이 여기에 있겠구나 라고 비즈니스 계층을 인식하는데 도움이 된다
  • @Repository : 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
    • DB 변경 시 다른 예외가 발생하면 상위 계층까지 흔들리는 것을 방지
  • @Configuration : 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.

3. 필터

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다

어노테이션 추가

컴포넌트 스캔 대상에 추가할 어노테이션

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {}

컴포넌트 스캔 대상에서 제외할 어노테이션

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {}

클래스 추가

컴포넌트 스캔 대상에 추가할 클래스

@MyIncludeComponent
public class BeanA {}
  • @MyIncludeComponent를 적용한다

컴포넌트 스캔 대상에서 제외할 클래스

@MyExcludeComponent
public class BeanB {}
  • @MyExcludeComponent를 적용한다

Test

@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)
	);
}

@Configuration
@ComponentScan(
	includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
    excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig{}
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
  • ComponentFilterAppConfig클래스를 설정 정보로 전달한다.
@Configuration
@ComponentScan(
	includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
    excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
)
static class ComponentFilterAppConfig{}
  • ComponentFilterAppConfig클래스는 컴포넌트 스캔 방식으로 빈을 등록한다.
  • @ComponentScan의 속성으로 includeFiltersexcludeFilters를 지정하였다.
  • includeFiltersMyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.
  • excludeFiltersMyExcludeComponent 애노테이션을 추가해서 BeanB가 스프링 빈에 등록되지 않는다.
  • 원래 컴포넌트 스캔 시 @Component어노테이션이 포함된 클래스를 빈으로 등록하는데, includeFilters속성을 통해 빈으로 등록시켰다.

FilterType 옵션

  1. ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다
  2. ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
  3. ASPECTJ: AspectJ 패턴 사용
  4. REGEX: 정규 표현식
  5. CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
  • @Component 면 충분하기 때문에, includeFilters를 사용할 일은 거의 없다. excludeFilters는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다

4. 중복 등록과 충돌

컴포넌트 스캔에서 같은 이름을 가진 빈을 등록할 경우 문제가 발생한다.

  1. 자동 빈 등록 (@Component) vs 자동 빈 등록 (@Component)
  2. 수동 빈 등록 (@Bean) vs 자동 빈 등록 (@Component)

자동 빈 등록 (@Component) vs 자동 빈 등록 (@Component)

@Component("service")
public class OrderServiceImpl implements OrderService{...}

@Component("service")
public class MemberServiceImpl implements MemberService{...}
  • OrderServiceImpl을 스프링 빈으로 등록할 때 service라는 이름으로 등록
  • MemberServiceImpl을 스프링 빈으로 등록할 때 service라는 이름으로 등록
  • 즉, 동일한 이름의 빈을 등록하게 된다.
... org.springframework.context.annotation.ConflictingBeanDefinitionException ...
  • 실행 시켜보면, ConflictingBeanDefinitionException 예외가 발생하게 된다.

수동 빈 등록 (@Bean) vs 자동 빈 등록 (@Component)

...
@ComponentScan
public class AutoAppConfig {
    @Bean(name = "memoryMemberRepository")
    MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • 컴포넌트 스캔 방식의 설정정보에 수동으로 @Bean을 사용하여 빈을 등록할 경우, @Component에 의해 자동으로 등록된 빈과 중복이 발생한다.
  • 이 경우, 수동 등록 빈(@Bean)이 우선권을 갖는다
    • = 수동 빈이 자동빈을 오버라이딩한다.
Overriding bean definition for bean 'memoryMemberRepository' with a different definition: ...
  • 개발자가 이러한 상황을 의도적으로 기대했기보다 여러 설정 정보가 얽혀 발생되는 경우가 대부분이다.
  • 따라서 스프링 부트에서는 수동 빈과 자동 빈의 충돌이 일어나면 오류가 발생되게 기본 값을 변경하였다.
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 and overriding is disabled.

출처: 인프런 스프링 핵심 원리 - 기본편 (김영한)
인프런 스프링 핵심 원리

0개의 댓글