본문 바로가기

Spring Boot

@ComponentScan으로 스프링 빈 스캔하기

728x90

앞의 포스팅까지는 @Configuration과 @Bean을 이용하여 아래와 같이 자바 설정 정보에 등록할 스프링 빈을 직접 만들어 스프링 컨테이너에 빈을 등록하였습니다.


@Configuration
public class AutoAppConfig {
    @Bean
    Product getBook(){
        return new Book();
    }

    @Bean
    Order getOrder(){
        return new OnlineOrder(getBook());
    }
}

 

[Book, OnlineOrder 클래스]

더보기
public interface Order {}

public class OnlineOrder implements Order {
    Product product;

    public OnlineOrder(Product product) {
        this.product = product;
    }
}

public interface Product {}
public class Book implements Product{}

 

 

하지만 이렇게 수동으로 등록하는 방식은 개발자가 일일이 다 작성해야 한다는 번거로움이 존재합니다. 만약 등록해야 할 스프링 빈이 수십, 수백 개가 되면 수동으로 입력하는 것이 더 어려워지겠죠? 

 

그래서 스프링이 개발자가 설정 정보를 작성하지 않아도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공합니다.  이 컴포넌트 스캔을 제공하는 기술이 바로 @ComponentScan 애너테이션 입니다. 따라서, 이번 포스팅은 @ComponentScan을 이용한 스프링 빈 등록에 대해서 작성하였습니다.


@ComponentScan

@ComponentScan은 이름 그대로 @Component 애너테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록합니다. 즉, @Configuration과 @Bean이 함께 해야 스프링 빈으로 등록되듯이, @ComponentScan도 @Component와 함께해야 스프링 빈으로 등록되는 것입니다.

 

참고로, @Configuration 내부를 살펴보면 아래와 같이 @Component 애너테이션이 붙어 있기 때문에 @Configuration이 붙은 설정 정보도 컴포넌트 스캔의 대상이 되어 자동으로 스프링 빈으로 등록됩니다. 


 

이 외에도, 아래 애너테이션들도 모두 @Component를 가지고 있어 @ComponentScan의 대상이 됩니다.

  • @Controller: 스프링 MVC 컨트롤러에서 사용 
  • @Service :  스프링 비즈니스 로직에서 사용
  • @Repository : 스프링 데이터 접근 계층에서 사용
  • @Configuration: 스프링 설정 정보에서 사용

 

그럼, @ComponentScan을  적용해 보겠습니다. 우선, 제가 작성한 클래스들의 패키지 구조는 아래와 같습니다.



 

 

@ComponentScan을 메인 클래스(HelloApplication)에 아래와 같이 작성합니다.


@ComponentScan
public class HelloApplication {
    public static void main(String[] args) {}
}

 

 

여기서 주의할 점이 있습니다. @ComponentScan이 @Component를 탐색(스캔)하는 위치는 @ComponentScan이 위치한 클래스의 패키지를 포함한 하위 패키지입니다. 따라서, @ComponentScan을 작성하는 곳은 보통 프로젝트 최상단에 두곤 합니다. 

 

만약, 특정 하위 패키지 내부에서만 컴포넌트 스캔을 하고 싶다면 아래와 같이 설정할 수도 있습니다. (권장하는 방법은 아닙니다.)


@ComponentScan(basePackages = "hello.config") // @ComponentScan(basePackageClasses = AutoAppConfig.class)
public class HelloApplication {
    public static void main(String[] args) { }
}

 

  • basePackages: 탐색할 패키지의 시작 위치로, 이 패키지를 포함해서 하위 패키지를 모두 탐색합니다.
    • basePackages = {"hello.config", "hello.product"}과 같이 여러 시작 위치를 지정할 수도 있습니다.
  • basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정합니다.
  • 위 속성들을 지정하지 않으면 @ComponentScan이 붙은 클래스의 패키지가 시작 위치가 됩니다.

 

참고로, 현재 프로젝트는 Spring Boot가 아닌 Spring Framework를 사용하고 있습니다. (Spring Framwork과 Spring Boot

만약 Spring Boot를 이용하면 Spring Boot의 대표 시작 정보인 @SpringBootApplication을 이 프로젝트 시작 루트 위치(e.g. HelloApplication.java)에 두는 것이 관례입니다. (그리고 @SpringBootApplication 내부에 @ComponentScan이 들어있습니다. 따라서, Spring Boot 사용 시, @ComponentScan을 따로 작성할 필요 없습니다.)


@Component

이제 스프링 빈으로 등록하고자 하는 클래스 위에 @Component 애너테이션을 작성하여 @ComponentScan이 스캔하는 대상이 될 수 있도록 합니다. 우선 저는 아래와 같이 Book 클래스를 스프링 빈으로 등록되도록 하였습니다.


@Component
public class Book implements Product{}

 

 

테스트 코드를 통해 Book 클래스가 스프링 빈으로 등록되었는지 확인해보겠습니다.


class AutoAppConfigTest {
    @Test
    void basicScan() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(HelloApplication.class);
        Product product = ac.getBean(Product.class);
        assertThat(product).isInstanceOf(Book.class);
        
        System.out.println(product); // 출력 결과: com.example.hello.product.Book@3574e198
    }
}

 

 

테스트가 통과하였습니다. 또한 product 객체를 출력하여 어떤 스프링 빈인지도 확인하였습니다. 


@ComponentScan의 Filter

@ComponentScan의 Filter 기능을 사용하면 컴포넌트 스캔 대상을 추가하거나 제외할 수도 있습니다. Filter는 아래와 같이 두 종류가 있습니다.

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

그리고 FilterType을 지정해 줄 수 있는데, FilterType에는 5가지 옵션이 있습니다.

  • ANNOTATION: 기본값으로, 애너테이션을 인식해서 동작합니다.
    • e.g. com.example.hello.SomeAnnotation 
  • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작합니다.
    • e.g. com.example.hello.SomeClass
  • ASPECTJ: AspectJ 패턴을 사용합니다.
    • e.g. com.example.hello.*Service+
  • REGEX: 정규 표현식
    • e.g. com\.example\.hello\.Default.*
  • CUSTOM: TypeFilter라는 인터페이스를 구현해서 처리합니다.
    • e.g. com.example.hello.MyTypeFilter

대표적으로 ANNOTATION 방법과 ASSIGNABLE_TYPE을 이용해 보겠습니다.


ANNOTATION

1. 애너테이션 생성 (자바 애너테이션 정의하기)


package com.example.hello.myAnnotation;

import java.lang.annotation.*;

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

 

package com.example.hello.myAnnotation;

import java.lang.annotation.*;

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

 

 

 

2. 컴포넌트 스캔 대상에 추가할 클래스 (Food.java)


package com.example.hello.product;

import com.example.hello.myAnnotation.IncludeComponent;

@IncludeComponent
public class Food implements Product{
}

 

 

 

 

3. 컴포넌트 스캔 대상에 제외할 클래스 (Book.java)


package com.example.hello.product;

import com.example.hello.myAnnotation.ExcludeComponent;

@ExcludeComponent
public class Book implements Product{
}

 

 

위와 같이 설정 후, 테스트 코드를 작성하였습니다. 이번에는 메인 클래스가 아닌 테스트 코드 내부에 static 클래스를 만들어 설정 정보로 등록하였습니다. 주의할 점은 여기서 basePackages를 최상단으로 설정하여야 Food 클래스와 Book 클래스를 제대로 찾을 수 있습니다. (여기서 오류가 발생해서 찾는데 시간이 좀 걸렸습니다 ㅠㅠ)


class AutoAppConfigTest {
    @Test
    void filterScan() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);

        Product pr1 = ac.getBean(Food.class);
        assertThat(pr1).isNotNull(); // Food 객체 컴포넌트는 찾는다.
        
        // Book 객체 컴포넌트는 Null 이다.
        assertThrows(NoSuchBeanDefinitionException.class, () -> ac.getBean(Book.class));
        

    }

    @ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = IncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(classes = ExcludeComponent.class),  // type 생략 가능 (ANNOTATION이 default 이므로)
            basePackages = "com.example.hello")
    static class ComponentFilterAppConfig {
    }
}

 

이렇게 작성하면, 정상적으로 테스트가 잘 통과합니다.


ASSIGNABLE_TYPE

위에서 작성한 코드에 원하는 클래스만 작성하면 됩니다. 만약 Food 클래스도 컴포넌트 스캔 대상에서 제외하고 싶다면 아래와 같이 작성합니다.


  @ComponentScan(includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = IncludeComponent.class),
            excludeFilters = {
                    @ComponentScan.Filter(classes = ExcludeComponent.class),
                    @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Food.class)
            },
            basePackages = "com.example.hello")
    static class ComponentFilterAppConfig {
    }

 


중복 등록과 충돌

만약, 컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까요? 두 가지 상황으로 나눠서 생각할 수 있습니다.

 

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

 이 경우는 @Component("bean")이라는 애너테이션이 중복으로 정의했을 때 발생할 수 있겠죠? 혹은 클래스 이름이 Bean이라는 클래스가 @Component로 선언되어 빈으로 등록되어 있는데 개발자가 다른 클래스 위에 @Component("bean")이라고 선언하는 경우도 있겠습니다. (@Component("빈이름")으로 빈 이름을 설정할 수 있습니다. 설정하지 않으면 기본적으로 Spring에서 클래스명을 사용하되, 맨 앞글자만 소문자를 사용합니다.)

 

이럴 경우는 스프링은 ConflictingBeanDefinitionException 예외를 발생시킵니다.


public class ComponentScanTest {
    @ComponentScan
    static class ComponentAppConfig {
    }

    @Component("beanB") // 같은 빈 이름 설정
    static class BeanA{}
    @Component("beanB")
    static class BeanB{}

    @Test
    void duplicatedBeanTest() {
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(ComponentAppConfig.class);

    }

}

 

 


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

이 경우 예외는 발생하지 않고, 수동으로 등록된 빈이 우선권을 가지게 됩니다. 아래 코드를 통해 확인해 보겠습니다.


public class ComponentScanTest {

    @Component("beanC")
    static class beanC{
    }


    @Configuration // 수동 등록
    @ComponentScan
    static class AppConfig{
        @Bean(name="beanC")
        public BeanA beanA(){
            return new AutowireTest.BeanA();
        }
    }

    @Test
    @DisplayName("수동 빈 등록 vs 자동 빈 등록 이름 충돌 시")
    void duplicatedBeanTest2(){
        ApplicationContext ac = new
                AnnotationConfigApplicationContext(AppConfig.class);
        Object bean = ac.getBean("beanC");
        System.out.println(bean.toString()); // com.example.hello.config.AutowireTest$BeanA@63f34b70 출력
    }

}

 

 

테스트 코드가 정상적으로 동작하여 통과하였고, bean을 출력해 보니 BeanA 타입을 가지고 있습니다. 즉, AppConfig에서 직접 작성하여 수동으로 등록한 BeanA 타입의 빈이 beanC라는 이름을 가지는 빈이라는 것을 알 수 있습니다.

 

이렇게 수동 빈이 자동 빈을 오버라이딩 해버리게 되는데, 개발자가 의도적으로 이런 결과를 기대했다면 자동보다는 수동이 우선권을 가지는 것이 좋습니다. 하지만 현실은 개발자가 의도적으로 이런 결과를 만들도록 설정하기보다는 여러 설정들이 꼬여서 이렇게 결과가 만들어지는 경우가 많습니다. (이렇게 되는 경우는 정말 잡기 어려운 버그라고 합니다.)

 

이런 일들을 방지하기 위해 최근에 Spring Boot에서는 수동 빈 등록과 자동 빈 등록이 충돌 나면 오류가 발생하도록 기본 값을 바꾸었습니다. 그래서 Spring Framework이 아닌 Spring Boot로 실행하게 되면 에러가 발생합니다.

 


 

이렇게 @ComponentScan에 대해서 알아보았습니다.

@ComponentScan과 @Component를 이용하면 스프링 빈으로 Spring Framework가 자동으로 등록해 줍니다. @Configuration과 @Bean으로 자바 설정 정보를 작성했을 때는 개발자가 직접 빈으로 등록함과 동시에 의존관계도 명시하여 Spring Framework에서 의존관계를 주입할 수 있었습니다.

하지만, @ComponentScan과 @Component는 스프링 빈으로만 등록해 주고 의존관계 주입까지는 하지 않습니다. 따라서 다음 포스팅은 이렇게 등록된 스프링 빈을 이용하여 어떻게 의존관계를 주입할 수 있는지 알아보겠습니다.(@Autowired)

 


[전체 소스코드]

 

GitHub - eunhwa99/SpringBlog

Contribute to eunhwa99/SpringBlog development by creating an account on GitHub.

github.com

 

 

[참고자료]

김영한, "스프링 핵심 원리 - 기본편", 인프런

728x90
반응형