-> 블로그 이전

[Spring - 기본] 컴포넌트 스캔

2022. 5. 15. 12:33Language`/Spring

컴포넌트 스캔

@Configuration
public class AppConfig {

    @Bean
    public MemberRepository memberRepository(){
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }

    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
}

이전까지는 따로 "스프링 설정 클래스"인 Config를 통해서 모든 빈을 수동으로 등록했고 @Configuration을 지정함에 따라서 스프링 컨테이너가 AppConfig 클래스를 읽어서 스프링 빈을 생성하고 의존관계도 주입해주었다

 

하지만 스프링 빈이 수십, 수백개가 된다면 일일이 다 등록을 하고 DI를 해주는 것은 개발자 입장에서 굉장히 귀찮은 일이 될 것이다

 

따라서 Spring은 따로 설정정보가 존재하지 않아도 스프링 빈으로 자동 등록해주는 "컴포넌트 스캔"이라는 기능을 제공한다

  • 그에 따라서 의존관계도 자동으로 주입해주는 @Autowired 기능도 제공해준다

 

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                classes = Configuration.class
        )
)
public class AutoAppConfig {

}

@ComponentScan을 스프링 설정 클래스에 붙여주면 알아서 "지정한 위치"에서부터 스캔을 시작한다

컴포넌트 스캔이 시작되면 @Component가 달린 모든 클래스에 대해서 스프링 빈으로 등록을 해준다

@Configuration도 @Component가 달려있고 위의 코드를 보게되면 "AppConfig"라는 스프링 설정 클래스에 현재 @Configuraion이 달려있는 것을 볼 수 있다

 

이러한 문제 때문에 AutoAppConfig에서는 @ComponentScan 파라미터에 "excludeFilters"라는 설정을 해줌에 따라서 @Configuration class는 필터링 안하겠다고 지정하였다

이렇게 스프링 빈으로 등록할 클래스에 대해서 @Component를 붙여주었다

 

이러고 나서 스프링 빈이 잘 등록이 되었나 테스트를 해보자

등록된 MemberService를 가져오려고 했으나 MemberService가 빈으로 등록이 되지 않아서 오류가 발생하였다

>> 여기서 우리가 파악해야 할 것은 스프링 설정 클래스의 위치이다

현재 내가 만든 AutoAppConfig라는 스프링 설정 클래스는 위치가 "Spring.Core.config"에 존재한다

 

@ComponentScan에서 "basePackages"에 아무것도 지정하지 않는다면 컴포넌트 스캔은 스프링 설정 클래스 파일이 존재하는 위치의 하위만 뒤지게 된다

따라서 "Spring.Core.config" 하위에는 우리가 @Component로 지정한 여러 클래스들이 존재하지 않는 것을 볼 수 있다

>> 따라서 "basePacakges"를 지정해줘야 한다

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                classes = Configuration.class
        ),
        basePackages = "Spring.Core"
)
public class AutoAppConfig {

}

이렇게 지정하게 된다면 "Spring.Core" 하위에 존재하는 패키지{discount, member, order}를 뒤질 수 있기 때문에 위에서 @Component로 지정한 클래스들에 대한 스프링 빈이 등록이 된다

 

"basePackages"를 지정하니까 이제 스프링 빈 등록이 제대로 완료되었다

 

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(
                type = FilterType.ANNOTATION,
                classes = Configuration.class
        ),
        basePackages = {"Spring.Core.discount", "Spring.Core.member", "Spring.Core.order"}
)
public class AutoAppConfig {

}

이렇게 {}로 묶어서 여러개의 패키지로 지정할 수 있고 스프링 빈이 제대로 등록이 된다

 

최근 스프링이나 스프링부트에서 권장하는 방식은 아예 @ComponentScan이 붙은 스프링 설정 클래스를 "프로젝트 최상단"에 두는 방법이다

그리고 여기서 또 하나 확인해야할 점이 "등록된 스프링 빈의 이름"이다

이러한 클래스들에 대해서 @Component를 붙여줬고 그에 따라서 등록된 스프링 빈의 이름들은 다음과 같다

RateDiscountPolicy → rateDiscountPolicy
MemberServiceImpl → memberServiceImpl
MemoryMemberRepository → memoryMemberRepository
OrderServiceImpl → orderServiceImpl

전부 "맨 앞이 소문자로 변경"된 것을 볼 수 있다

 

이렇게 클래스에 @Component를 붙임에 따라서 스프링 빈으로 등록이 되면 그에 따른 이름은 맨 앞만 소문자로 바뀐 채 등록이 된다

물론 @Component(~~~~)방식으로 직접 이름을 지정할 수는 있지만 굳이 우리가 따로 이름을 지정해줄 필요는 없다

 


컴포넌트 스캔의 기본 대상은 다음과 같다

  • @Component
  • @Controller
  • @Service
  • @Repository
  • @Configuraion

위의 Annotation들을 들어가서 보게되면 전부 @Component가 붙어있는 것을 확인할 수 있다


중복 등록 & 충돌

스프링 빈 이름이 중복 등록되어서 충돌이 발생하는 경우는 다음 2가지 경우이다

  1. 자동 빈 등록 vs 자동 빈 등록
  2. 수동 빈 등록 vs 자동 빈 등록

 

자동 vs 자동

@Component("Bean")
public class BeanFirst {
}
@Component("Bean")
public class BeanSecond {
}
public class ScanTest {

    @Configuration
    @ComponentScan(
            excludeFilters = @ComponentScan.Filter(
                    type = FilterType.ANNOTATION,
                    classes = Configuration.class
            ),
            basePackages = "Spring.Core.scan"
    )
    static class TestConfig{

    }

    @Test
    void test(){
        ApplicationContext ac =
                new AnnotationConfigApplicationContext(TestConfig.class);

        BeanFirst b = ac.getBean(BeanFirst.class);
        Assertions.assertThat(b).isInstanceOf(BeanFirst.class);
    }
}

현재 BeanFirst, BeanSecond 클래스 둘 다 @Component에 의해서 자동 스캔 대상이고, 둘다 빈 이름을 "Bean"으로 등록되게 설정을 하였다

 

그리고나서 @ComponentScan을 통해서 빈으로 등록을 하려고 했고, 테스트해보니 다음과 같은 결과가 도출되었다

결국 "ConflictingBeanDefinitionException" 오류가 발생하게 되었고 "자동 vs 자동"의 경우 스프링 빈 이름이 중복되는 것들은 스프링 빈으로 등록이 되지 않는 것을 알 수 있다

 

 

수동 vs 자동

@Component("Bean")
public class BeanFirst implements BeanTest {
}
public class ScanTest {

    @Configuration
    @ComponentScan(
            excludeFilters = @ComponentScan.Filter(
                    type = FilterType.ANNOTATION,
                    classes = Configuration.class
            ),
            basePackages = "Spring.Core.scan"
    )
    static class TestConfig{
        @Bean(name = "Bean")
        public BeanTest test(){
            return new BeanFirst();
        }
    }

    @org.junit.jupiter.api.Test
    void test(){
        ApplicationContext ac =
                new AnnotationConfigApplicationContext(TestConfig.class);

        BeanTest b = (BeanTest) ac.getBean("Bean");
        Assertions.assertThat(b).isInstanceOf(BeanFirst.class);
    }
}

"beanFirst"는 @Component에 의해 자동 스캔 대상이 되고, "beanTest"는 수동으로 빈 등록을 한 상태이다

 

이렇게 수동 빈 등록과 자동 빈 등록간에 스프링 빈 이름이 동일할 경우 다음과 같은 결과가 나온다

빈 이름이 동일한데도 불구하고 스프링 빈 등록이 정상적으로 이루어졌다

 

이 때 "Overriding bean definition"이 중요하다

결국 수동 빈 등록 & 자동 빈 등록간에 등록될 스프링 빈의 이름이 동일할 경우 "수동 빈 등록이 자동 빈 등록을 Overriding하게 된다"

따라서 수동 빈 등록이 우선권을 가진다고 볼 수 있다

 

하지만 이렇게 중복이 여러번 계속 발생하게 되면 여러 설정들이 꼬여서 원하는 결과를 도출해내지 못하거나 예외가 발생할 수 있다
따라서 최근 스프링 부트에서는 수동 빈 & 자동 빈 간의 충돌이 발생하면 default로 오류가 발생하도록 설정해두었다