Java Spring Core

Component Scan

기존에 Spring Bean을 등록하기 위해서 AppConfig 파일을 통해 수동적으로 Bean 생성 작업을 진행했다. 하지만, 프로젝트 규모가 커지게 되면서 이러한 Bean Configuration 작업은 시간이 오래 걸리게 된다 그래서 이를 자동화 해주기 위해 Spring에서 Component Scan 방식을 제공한다.

@Component annotation이 등록된 클래스에 대해서 자동으로 이들을 탐색해서 Spring Bean으로 등록해주는 작업을 진행한다. 또한 @Autowired annotation이 등록되어 있으면 DI도 자동으로 해결해준다.

Example

AppConfig

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

여기서 excludeFilter은 Component Scan을 진행할때 제외하고자 하는 클래스를 의미하는데, @Configuration을 excludeFilter에 등록하게 되면 기존의 @Configuration이 설정된 클래스들은 Spring Bean으로 등록되지 않는다.

@Component / @Autowired추가 작업

MemoryMemberRepository

@Component
public class MemoryMemberRepository implements MemberRepository{

RateDiscountPolicy

@Component
public class RateDiscountPolicy implements DiscountPolicy{

MemberServiceImpl

@Component
public class MemberServiceImpl implements MemberService{
    private final MemberRepository memberRepository;
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository{
        this.memberRepository=memberRepository;
    }

OrderServiceImpl

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

AppConfigTest

 @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);

        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
            
            if(beanDefinition.getRole()==BeanDefinition.ROLE_APPLICATION){
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("bean = " + bean);
            }
        }
    }

Results

bean = hello.core.discount.RateDiscountPolicy@1c32386d
bean = hello.core.member.MemberServiceImpl@6399551e
bean = hello.core.member.MemoryMemberRepository@13d73fa
bean = hello.core.order.OrderServiceImpl@5023bb8b

위와 같이 Component를 등록한 다음 조회를 해보면 모두 정상적으로 Spring Bean으로 등록된 것을 확인 할 수 있다.

@Component를 등록한 클래스에 대해 Spring Bean이 등록된 것을 확인할 수 있고, @Autowired 설정을 통한 DI도 정상적으로 된것을 위의 예제를 통해 확인할 수 있다.

Options

basePackages

모든 자바 파일을 다 훑어보면서 Spring Bean으로 등록할지 말지를 결정하는 작업은 매우 시간이 많이 걸리게 된다. basePackage option을 통한 탐색 위치를 제한하면 시간을 단축 시킬 수 있다.

AppConfig

@ComponentScan(
        basePackages = "hello.core",
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes=Configuration.class)
)

basePackages=”hello.core”


bean = hello.core.discount.RateDiscountPolicy@1c32386d
bean = hello.core.member.MemberServiceImpl@6399551e
bean = hello.core.member.MemoryMemberRepository@13d73fa
bean = hello.core.order.OrderServiceImpl@5023bb8b

basePackages=”hello.core.member”

bean = hello.core.member.MemberServiceImpl@32f232a5
bean = hello.core.member.MemoryMemberRepository@43f82e78

탐색 시작 위치에 따라 등록되는 Spring Bean의 종류가 다른 것을 확인 할 수 있다.

basePackageClasses

위의 basePackages와 유사하다. 다만, 설정된 클래스의 패키지를 탐색 시작 위치를 설정한다는 점이 다른다.

AppConfig

@ComponentScan(
        basePackageClasses = OrderService.class,
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes=Configuration.class)
)

@ComponentScan의 default 설정은, basePackageClasses=”설정파일” 처럼 설정 파일의 패키지로 되어 있기 때문에, 설정(Config)파일을 프로젝트 최상단에 둬서 default으로 실행하는 것이 권장사항이다.

Component Types

스프링의 비지니스 로직에 이용되는 @Controller, @Repository, @Service, @Configuration 모두 @Component에 속하기 때문에 해당 Annotation이 등록된 클래스에 대해서는 자동으로 Component Scan이 동작한다.

Filters

FilterType Options

Filter에 지정할 수 있는 FilterType에는 아래와 같이 5가지가 있다.

Type Description Example
ANNOTATION 어노테이션 인식 org.example.SomeAnnotation
ASSIGNABLE_TYPE 타입 인식 org.example.SomeClass
ASPECTJ aspectj 패턴 인식 org.example..*Service+
REGEX 정규표현식 org.example.Default.*
CUSTOM TypeFilter 인터페이스를 구현한 클래스 org.example.MyTypeFilter

includeFilters

Component Scan에 추가적으로 등록하고자 하는 대상 정의

includeFilters = @ComponentScan.Filter(type= /*FilterType*/,classes= /*filter type에 맞는 클래스*/)

excludeFilters

Component Scan에서 제외하고자 하는 대상 정의

excludeFilters = @ComponentScan.Filter(type= /*FilterType*/,classes= /*filter type에 맞는 클래스*/)

Example

@MyIncludeComponent 정의

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

@MyIncludeComponent 등록된 BeanA 클래스 정의

@MyIncludeComponent
public class BeanA {
}

@MyExcludeComponent

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

@MyExcludeComponent 등록된 BeanB 클래스 정의

@MyExcludeComponent
public class BeanB {
}

Component Scan Options

@Configuration
@ComponentScan(
        includeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes= MyIncludeComponent.class),
        excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes= MyExcludeComponent.class)
    )

다음과 같이 includeFilters, excludeFilters을 설정해준 다음, 아래의 테스트를 진행한다.

TestConfig

public class ComponentFilterAppConfigTest {
    @Test
    void componentScanner() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

        String[] beanDefinitionNames = ac.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);

            if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
                Object bean = ac.getBean(beanDefinitionName);
                System.out.println("bean = " + bean);
            }
        }
    }

    @Configuration
    @ComponentScan(
            includeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes= MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type= FilterType.ANNOTATION,classes= MyExcludeComponent.class)
    )
    static class TestConfig{

    }
}

Results

bean = hello.core.scan.filter.BeanA@4671115f

includeFilters에 등록된 BeanA는 Spring Bean으로 등록되고, excludeFilters에 등록된 BeanB는 Spring Bean으로 등록되지 않은 것을 확인 할 수 있다.

Duplicated Bean_names

Component Scan을 통해 Spring Bean으로 등록되게 되는데, 이때 bean name을 이용해서 탐색을 진행하게 된다. 만약 중복된 이름을 가진 빈이 존재할 때 어떤 현상이 발생할까?

Automated Beans vs Automated Beans

Sample Component

@Component("member")
public class MemberServiceImpl implements MemberService{

@Component("member")
public class MemoryMemberRepository implements MemberRepository{

다음과 같이 bean name이 member인 클래스가 2개가 존재할 때, component scan을 실시하게 되면

Results

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [hello.core.AutoAppConfig]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'member' for bean class [hello.core.member.MemoryMemberRepository] conflicts with existing, non-compatible bean definition of same name and class [hello.core.member.MemberServiceImpl]

ConflictingBeanDefinitionException 에러가 발생하는 것을 확인할 수 있다.

Automated Bean vs Manual Beans

Component Scan

@Configuration
    @ComponentScan(
            basePackages = "hello.core",
            excludeFilters = @ComponentScan.Filter(type=FilterType.ANNOTATION,classes=Configuration.class)
    )
    static class AutoAppConfig{
        @Bean(name="memoryMemberRepository")
        public MemberRepository memberRepository(){
            return new MemoryMemberRepository();
        }

    }

다음과 같이 MemoryMemberRepository에 대해서 수동으로 bean을 등록해주는데, 이는 기존의 MemoryMemberRepository와 겹치게 된다.

Results

Overriding bean definition for bean 'memoryMemberRepository' with a different definition

수동으로 등록한 Bean이 자동으로 등록되는 Bean을 오버라이딩 한다. 즉, 수동을 등록한 Bean이 우선순위를 가진다.

위와 같이 중복되는 Bean name을 사용하게 되면, 프로젝트 전반적으로 꼬이게 되고, 버그 가능성도 높아진다. 이에 따라 스프링에서는 Bean name이 겹치는 Spring Bean이 발생하게 되면 아래와 같은 오류를 반환하면서 종료한다.

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

References

link: inflearn

link:springcore

link:spring_framework

댓글남기기