Spring

SpringBoot Component Scan 에 대하여

구티맨 2022. 3. 30. 13:54

목차

    ComponentScan 이란?

    @ComponentScan은 스프링부트에서 제공하는 어노테이션으로써, 스프링 빈으로 만들기 위한 컴포넌트를 스캔할 곳을 설정하는 역할을 합니다.

    스프링에서는 여러 빈들을 관리하면서 의존성을 주입해 주는데 이때 전체 코드에서 컴포넌트를 찾아 빈을 생성하는 것이 아니라

    범위를 지정하여 그 범위 안에서 어노테이션 된 클래스들을 찾아 빈을 생성 및 관리합니다.

    빈으로 등록하기 위해 스캔하는 어노테이션은 @Component, @Repository, @Service, @Controller, @Configuration 가 있습니다.

    package 구조

    com.example.component.SlideComponent와 com.example.component2.BarComponent가 있는데 둘은 서로 다른 패키지에 위치하고 있습니다.

    아래 코드에서는 SlideComponent, BarComponent의 빈을 주입받아 print함수를 호출하고 있습니다.( 생성자 주입 )

    package com.example.component;
    
    import com.example.component2.BarComponent;
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.context.annotation.ComponentScan;
    import org.springframework.stereotype.Component;
    
    //@ComponentScan(basePackages = "com.example.component;com.example.component2")
    @Component
    public class MyApplicationRunner implements ApplicationRunner {
    
        SlideComponent slideComponent;
        BarComponent barComponent;
    
        public MyApplicationRunner(SlideComponent slideComponent, BarComponent barComponent) {
            this.slideComponent = slideComponent;
            this.barComponent = barComponent;
        }
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            slideComponent.print();
            barComponent.print();
        }
    }

    실행하면 아래와 같이 실패를 하고, BarComponent 빈 타입을 찾을 수 없다고 합니다.

    ***************************
    APPLICATION FAILED TO START
    ***************************
    
    Description:
    
    Parameter 1 of constructor in com.example.component.MyApplicationRunner required a bean of type 'com.example.component2.BarComponent' that could not be found.

    왜냐하면 SpringBoot 애플리케이션에 붙어 있는 SpringBootApplication 어노테이션에서 ComponentScan으로 어노테이션이 있는 패키지의 위치를 basePackage로 설정하기 때문입니다.

    그래서 com.example.component 패키지에서 컴포넌트를 찾아 빈으로 등록을 하니 SlideComponent 빈만 존재하고 있습니다.

    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
    public @interface SpringBootApplication {

    basePackages vs basePacakgeClasses

    ComponentScan 어노테이션에서는 basePackages나 basePackageClasses를 사용하여 패키지 범위를 정합니다.

    basePackages는 스캔할 base 패키지를 설정할 수 있고,

    basePackageClasses는 클래스 설정하면 설정된 클래스의 패키지를 base 패지지로 하여 범위를 설정합니다.

     

    ApplicationRunner 상단에 있는 주석 처리된 @ComponentScan의 주석을 해제하고 다시 실행을 하면 정상적으로 실행이 되면서 print() 함수가 실행되면 정상적으로 값이 출력되고 있습니다.

    @ComponentScan(basePackages = "com.example.component;com.example.component2")
    
    [출력]
    SlideComponent ---
    BarComponent ---

    또는 아래와 같이 값을 설정할 수 있습니다.

    @ComponentScan(basePackages = {"com.example.component","com.example.component2"})
    @ComponentScan(basePackages = {"com.example"})

     

    개인적으로 String을 인자로 받는 basePacakges보다는 class를 인자로 받아 type safe 한  basePackageClasses를 사용하는 것을 추천합니다.

    @ComponentScan(basePackageClasses = {SlideComponent.class, BarComponent.class} )

    ComponentScan Filter

    ComponentScan.Filter 에는 아래의 다섯 가지 유형이 있습니다.

    • ANNOTATION : 주어진 어노테이션이 명시된 컴포넌트를 필터
    • ASSIGNABLE_TYPE : 주어진 타입을 필터( 주어진 타입을 상속 또는 구현한 타입도 필터 )
    • ASPECTJ : AspectJ 패턴을 사용하여 필터
    • REGEX : 정규식을 이용한 필터
    • CUSTOM : 직접 만든 필터를 이용한 필터

    이 필터를 ComponentScan의 includeFilters, excludeFilters에 정의하여 필터를 적용할 수 있습니다.

    includeFilters는 스캔에 포함할 필터를 적용, excludeFilters는 스캔에 포함하지 않을 필터를 적용합니다.

    아래의 예제 코드에 필터를 적용하여 동작을 확인해보겠습니다.

    package com.example.springapp.animal;
    
    @Component
    public class Cat implements Walk {
    }
    
    @Component
    public class Dog implements Walk{
    }
    
    public interface Walk {
    }
    package com.example.springapp;
    ...
    
    public class SpringApplication {
        private static ApplicationContext applicationContext;
    
        public static void main(String[] args){
            applicationContext = new AnnotationConfigApplicationContext(SpringApplication.class);
    
            for(String name : applicationContext.getBeanDefinitionNames()){
                System.out.println(name);
            }
        }
    }

    ANNOTATION 필터 타입

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    public @interface Ignore {
    }
    
    @Ignore
    @Component
    public class Dog implements Walk{
    }
    
    @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Ignore.class))
    public class SpringApplication {
        private static ApplicationContext applicationContext;
        ...
    }
    
    [출력]
    springApplication
    cat

    Ignore 어노테이션을 정의하고, Dog 컴포넌트에 Ignore 어노테이션을 작성합니다.

    그리고 Ignore 어노테이션을 필터에 넘겨주면, Ignore 어노테이션이 붙은 빈들은 필터가 되어 dog 빈은 제외되었습니다.

    ASSIGNABLE_TYPE 필터 타입

    @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = Walk.class))
    
    [출력]
    springApplication

    Walk 클래스 타입은 제외를 하여, cat, dog 빈은 제외되었습니다.

    ASPECTJ 필터 타입

     

    REGEX 필터 타입

    @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.REGEX, pattern = "com\\.example\\.springapp\\.animal\\..*"))
    
    [출력]
    springApplication

    com.example.springapp.animal. 이하는 제외하여 Cat, Dog 빈은 제외되었습니다.

    CUSTOM 필터 타입

    public class MyCustomFilter implements TypeFilter {
        @Override
        public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
            ClassMetadata classMetadata = metadataReader.getClassMetadata();
            String fullyQualifiedName = classMetadata.getClassName();
            String className = fullyQualifiedName.substring(fullyQualifiedName.lastIndexOf(".") + 1);
            return className.length() == 3 ? true : false;
        }
    }
    
    @ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.CUSTOM, classes = MyCustomFilter.class))
    
    [출력]
    springApplication

    TypeFilterf를 구현하여 match 함수를 오버라이드 하여 true가 리턴되면 필터 적용, false를 리턴하면 필터 미적용이 됩니다.

    dog, cat 빈은 필터에 매칭이 되어 excludeFilters에 적용이 되어 scan에서 제외되었습니다.

     

    이와 같은 여러 필터를 통하여 원하는 조건에 따라 프로젝트 내의 특정 빈을 제외 또는 포함을 하거나

    외부 라이브러리를 제외, 포함하도록 하여 유연하게 빈을 관리할 수 있습니다.