Chapter 5. 컴포넌트 스캔

Posted by yunki kim on May 2, 2022

  컴포넌트 스캔은 스프링이 직접 클래스를 검색해서 빈으로 등록하는 기능이다. 이 기능을 사용하면 설정 클래스를 사용하지 않아도 되므로 설정 코드가 줄어든다. @Componenet 어노테이션을 클래스에 사용해 해당 클래스를 스캔 대상으로 설정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// @Component 어노테이션만 사용하면
// 클래스 이름에서 첫 글자를 소문자로 바꾼 이름이
// 빈 이름으로 사용된다.
@Component
public class Bean {
    public void printHello() {
        System.out.println("hello");
    }
}
 
// 속성 값을 주면 해당 속성 값이 빈 이름으로 사용된다.
@Component("bean1")
public class Bean {
    public void printHello() {
        System.out.println("hello");
    }
}
cs

@ComponentScan 어노테이션으로 스캔 설정

  @Compoenent 어노테이션을 붙인 클래스를 스캔해 스프링 빈으로 등록하기 위해서는 @ComponentScan 어노테이션을 사용해야 한다. 

1
2
3
// setter 라는 패키지와 그 하위 패키지에 속한 클래스를 스캔 대상으로 설정한다.
// 스캔을 하면서 @Component가 붙은 클래스의 객체를 생성해 빈으로 등록한다.
@ComponentScan(basePackages = {"setter"})
cs

  만약 스캔시 특정 대상을 자동 등록 대상에서 제외하고 싶다면 @ComponentScan 어노테이션의 excludeFilters 속성을 사용하면  된다.

  excludeFilters 속성은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Repeatable(ComponentScans.class)
public @interface ComponentScan {
    ...
 
    /**
     * Specifies which types are not eligible for component scanning.
     * @see #resourcePattern
     */
    Filter[] excludeFilters() default {};
 
    ...
}
cs

  excludeFilters 속성은 다음과 같이 사용할 수 있다.

1
2
3
4
5
6
// setter 패키지 부터 스캔을 한다.
// filter 방식을 정규식으로 한다.
// "spring." 으로 시작하고 Dao로 끝나는 컴포넌트를 스캔 대상에서 제외한다.
 
@ComponentScan(basePackages = {"setter"}, 
        excludeFilters = @Filter(type = FilterType.REGEX, pattern="spring\\..*Dao"))
cs

  FilterType은 다음과 같은 종류가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public enum FilterType {
 
    /**
     * Filter candidates marked with a given annotation.
     * @see org.springframework.core.type.filter.AnnotationTypeFilter
     */
    ANNOTATION,
 
    /**
     * Filter candidates assignable to a given type.
     * @see org.springframework.core.type.filter.AssignableTypeFilter
     */
    ASSIGNABLE_TYPE,
 
    /**
     * Filter candidates matching a given AspectJ type pattern expression.
     * @see org.springframework.core.type.filter.AspectJTypeFilter
     */
    ASPECTJ,
 
    /**
     * Filter candidates matching a given regex pattern.
     * @see org.springframework.core.type.filter.RegexPatternTypeFilter
     */
    REGEX,
 
    /** Filter candidates using a given custom
     * {@link org.springframework.core.type.filter.TypeFilter} implementation.
     */
    CUSTOM
 
}
cs

  이 중 AspectJ 패턴을 사용하기 위해서는 aspectjweaver 모듈을 추가해야 한다.

1
implementation 'org.aspectj:aspectjweaver:1.9.9.1'
cs

  스프링 부트에서 기본으로 사용되는, @SpringBootApplication 내부에 존재하는 @ComponentScan은 다음과 같은 속성을 가지고 있다.

1
2
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
cs

기본 스캔 대상

  @Component 어노테이션 뿐만 아니라 다음 어노테이션들 역시 컴포넌트 스캔 대상에 포함된다.

    1. @Compoenent (org.springframework.stereotype)

    2. @Controller (org.springframework.stereotype)

    3. @Service (org.springframework.stereotype)

    4. @Repository (org.springframework.stereotype)

    5. @Aspect (org.aspectj.lang.annotation)

    6. @Configuration (org.springframework.context.annotation)

  이 어노테이션들 중 @Controller, @Service, @Repository는 내부적으로 @Component 어노테이션을 사용한다. @Aspect에 대한 내용은 추후 AOP에서 다루자.

컴포넌트 스캔에 따른 충돌 처리

    컴포넌트 스캔 기능을 사용할 때 빈 이름 충돌과 수동 등록에 따른 충돌을 조심해야 한다.

빈 이름 충돌

  빈 이름 충돌은 빈 스캔 대상이 되는 두 개 이상의 클래스가 같은 이름을 가질 때 발생한다. 예를 들어 conflict 패키지 내부에 Conflict 클래스가 존재하고 conflict2 패키지에도 Conclict 클래스가 존재한다 해보자. 이 두 클래스 모두 @Component로 빈 스캔 대상으로 등록하면 다음과 같은 에러가 발생한다.

1
2
3
4
5
org.springframework.beans.factory.BeanDefinitionStoreException: 
Failed to parse configuration class [setter.Main]; nested exception 
is org.springframework.context.annotation.ConflictingBeanDefinitionException: 
Annotation-specified bean name 'conflict' for bean class [setter.conflict.Conflict] 
conflicts with existing, non-compatible bean definition of same name and class [setter.Conflict2.Conflict]
cs

  이 경우 @Component에 속성값을 부여해 둘 중 하나를 명시적으로 빈 이름을 지정해야 하낟.

수동 등록한 빈과 충돌

  만약 다음과 같이 자동 등록 빈과 수동 등록 빈의 이름이 같다면 수동 등록한 빈이 우선권을 갖는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 수동 등록
@Configuration
public class Config {
 
    @Bean
    public setter.Bean bean() {
        return new setter.Bean();
    }
}
 
// 자동 등록
// @Component에 속성으로 이름을 명시하지 않으면
// 클래스 이름에서 첫 글자가 소문자로 바뀐 문자열이
// 빈의 이름이 된다.
@Component
public class Bean {
    public void printHello() {
        System.out.println("hello");
    }
}
cs

  만약 여기서 수동 등록 빈의 이름을 다음과 같이 수정한다면.

1
2
3
4
@Bean
public setter.Bean bean2() {
    return new setter.Bean();
}
cs

  수동 등록 빈의 이름과 자동 등록 빈의 이름이 다르므로 같은 Bean 타입의 빈이 두 개가 생성된다. 이 때는 @Qualifier 어노테이션을 사용해 알맞은 빈을 선택해야 한다.

 

출처 - 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문