본문 바로가기

Spring 프레임워크/이론

Spring의 컴포넌트 스캔(Component Scan)

이전까지는 스프링 설정 클래스(ApplicationContext 등)에서 Bean 객체를 생성, 컨테이너에 직접 등록하여 사용했다. 그러나 컴포넌트 스캔 기능을 사용하면 설정 클래스에서 일일이 등록하지 않아도 여러 클래스의 Bean 객체를 컨테이너에 자동으로 등록할 수 있다.

@Component

@Component
public class MemberRegisterService {
    ...

이 방법은 Bean 객체를 컴포넌트 클래스에 구현하면 애플리케이션 실행 시 스프링이 컴포넌트 클래스들을 탐색, Bean 객체들을 컨테이너에 등록하는 방법이다. 이를 컴포넌트 스캔(Component Scan)이라 한다. 컴포넌트 클래스는 위처럼 @Component 어노테이션이 붙은 클래스며 이 클래스의 객체가 스프링 컨테이너에 Bean 객체로 자동으로 등록되는 것이다.

@Component("realMemberRegisterService")
public class MemberRegisterService {
    ...
MemberRegisterService memberRegisterService = applicationContext.getBean("memberRegisterService", MemberRegisterService.class);

이때 @Component 어노테이션에 문자열을 전달할 수 있는데 이는 컨테이너에 등록하는 Bean 객체의 이름을 지정하는 것이다. 이 이름은 설정 클래스에서 Bean 객체의 이름을 이용해 객체를 얻어오는 getBean 메서드에 사용할 수 있는데 만약 위처럼 Bean 객체의 이름이 다르다면 빈을 찾을 수 없다는 예외가 발생한다.

@Component
public class MemberRegisterService {
    ...

만약 아무런 이름도 지정하지 않는다면 어떻게 될까? 이 경우 맨 처음 글자를 소문자로 바꾼 클래스의 이름을 Bean 객체의 이름으로 등록한다. 즉 MemberRegisterService는 memberRegisterService란 이름의 Bean 객체로 등록되는 것이다. 이 경우 위의 getBean 메서드는 아무런 예외를 발생시키지 않을 것이다.

 

이는 지난 포스트에서 언급했던 의존 자동 주입에서 @Bean 메서드가 반환하는 Bean 객체들은 메서드의 이름을 객체의 이름으로 하여 컨테이너에 등록된다고 했던 것과 유사하다. 단지 이번에는 명명 규칙에 따라 맨 처음 글자를 소문자로 변환하여 등록한다는 차이가 있다.

@ComponentScan

이렇게 등록한 컴포넌트들은 그 자체로는 아무런 의미가 없다. 대신 스프링 프레임워크에서 이 컴포넌트들을 탐색할 수 있는 지시어가 필요하며 그것이 바로 @ComponentScan 어노테이션이다. 이 어노테이션은 @Configuration 어노테이션과 같이 사용되며 설정 클래스에 비슷하게 붙이면 된다.

@Configuration
@ComponentScan(basePackages = {"springdemo"})
public class AppContextMember {
    ...

어노테이션을 사용할 때는 basePackages나 basePackagesClasses 옵션 값을 설정할 수 있다. 이는 컴포넌트들을 탐색할 패키지 디렉터리를 지정하는 옵션인데 만약 아무 옵션도 지정하지 않는다면 이 어노테이션이 붙은 클래스가 포함된 패키지만 기준으로 탐색한다. 위의 코드는 springdemo 패키지와 그 하위 패키지에 포함된 컴포넌트 클래스를 탐색하여 Bean 객체를 등록하라는 의미가 된다.

 

이런 컴포넌트를 사용하지 않고 설정 클래스에서 직접 Bean 객체를 등록했을 때 코드는 다음과 같았다.

@Configuration
public class AppContextMember {
    @Bean
    public MemberDAO memberDAO(){
        MemberDAO memberDAO = new MemberDAO();
        memberDAO.setUnique("Manual-injected dependencies");
        return memberDAO;
    }

    @Bean
    public String memberDAOMessage(){
        return "Auto-injected dependencies";
    }

    @Bean
    public MemberRegisterService memberRegisterService(){
        return new MemberRegisterService();
    }

    @Bean
    public ChangePasswordService changePasswordService(){
        return new ChangePasswordService();
    }
}

MemberDAO, MemberRegisterService, ChangePasswordService 객체를 직접 Bean 객체로 등록하고 있는 것을 볼 수 있다. 하지만 각 클래스에 @Component 어노테이션을 붙여 컴포넌트로 등록한 후 설정 클래스에서 @ComponentScan 어노테이션을 이용하여 컴포넌트 스캔을 수행하면 다음처럼 코드를 줄일 수 있다.

@Configuration
@ComponentScan(basePackages = {"springdemo"})
public class AppContextMember {

    @Bean
    public String memberDAOMessage(){
        return "Auto-injected dependencies";
    }
}

물론 이 경우 객체로 등록할 클래스들이 springdemo 패키지 내에 컴포넌트로 구현되어 있어야 할 것이다.

 

@ComponentScan(basePackages = {"spring"},
    excludeFilters=@Filter(type=FilterType.REGEX, pattern="spring\\..*Dao"))
    
@ComponentScan(basePackages = {"spring"},
    excludeFilters=@Filter(type=FilterType.ASPECTJ, pattern="spring.*Dao"))
    
@ComponentScan(basePackages = {"spring"},
    excludeFilters=@Filter(type=FilterType.ANNOTATION, classes={NoProduct.class, ManualBean.class}))

컴포넌트 스캔으로 탐색할 때 모든 걸 스캔하지 않고 스캔 대상에서 제외할 클래스를 설정하려면 excludeFilters 옵션을 사용할 수 있다. 이 옵션은 @Filters 어노테이션과 같이 사용되는데 옵션에 따라 위처럼 정규표현식이나 AspectJ 패턴, 특정 어노테이션 등으로 제외할 클래스들을 지정할 수 있다.

기본 탐색 대상

이렇게 @Component 어노테이션이 붙어있으면 컴포넌트 스캔의 대상이 되는데 이 어노테이션을 포함하고 있는 어노테이션도 컴포넌트 스캔의 대상이 된다. 대표적으로 @Controller, @Service, @Repository가 있다.

@Target(value=TYPE)
 @Retention(value=RUNTIME)
 @Documented
 @Component
public @interface Controller { ... }
@Target(value=TYPE)
 @Retention(value=RUNTIME)
 @Documented
 @Component
public @interface Repository { ... }
@Target(value=TYPE)
 @Retention(value=RUNTIME)
 @Documented
 @Component
public @interface Service { ... }

이 어노테이션을 살펴보면 모두 @Component 어노테이션을 내부적으로 사용하고 있는 것을 볼 수 있다. 그래서 이 어노테이션이 붙은 클래스들은 따로 @Component 어노테이션을 붙이지 않아도 컴포넌트 스캔의 대상이 된다. 이 @Controller, @Service, @Repository 등은 컴포넌트 스캔 대상이 될 뿐 아니라 현재 스프링 프레임워크에서 특별한 역할을 수행하고 있기 때문에 이런저런 어노테이션이 조합된 것이다.

컴포넌트 충돌

컴포넌트 스캔을 통해 컴포넌트를 탐색, Bean 객체들을 등록하다 보면 실수로 중복된 Bean 객체를 등록할 수도 있을 것이다. 이 경우 스프링에서는 ConflictingBeanDefinitionException 예외를 발생시킨다.

예를 들어 위처럼 MemberDAO 클래스가 두 패키지에 같이 존재하고 있을 때 두 클래스 모두 컴포넌트로 등록됐다면 추후 컴포넌트 스캔에서 두 컴포넌트 모두 MemberDAO 클래스의 Bean 객체로 탐색된다. 위에서 언급했듯이 @Component 어노테이션에 별다른 이름을 지정하지 않으면 첫 번째 글자를 소문자로 한 클래스의 이름으로 Bean 객체를 등록하기 때문에 memberDAO라는 이름의 MemberDAO 객체가 두 개 등록되는 것이다.

package springdemo.useless;

import org.springframework.stereotype.Component;

@Component("fakeMemberDAO")
public class MemberDAO {
    ...

그래서 컴포넌트 충돌이 발생하기 때문에 위의 코드처럼 두 클래스 중 하나에 명시적으로 이름을 지정하는 방법으로 중복을 피해야 한다. 그럼 만약에 컴포넌트 스캔이 아니라 수동으로 컨테이너에 등록된 객체와 충돌할 때는 어떻게 될까?

@Configuration
@ComponentScan(basePackages = {"springdemo"})
public class AppContextMember {
    @Bean
    public MemberDAO memberDAO(){
        MemberDAO memberDAO = new MemberDAO();
        memberDAO.setUnique("Manual-injected dependencies");
        return memberDAO;
    }
    ...
@Component
public class MemberDAO {
    private static long nextId = 0;
    private String unique = "DEFAULT";
    ...

위의 코드에서는 "DEFAULT" 문자열을 가지는 컴포넌트로 등록한 MemberDAO 객체와 "Manual-injected dependencies" 문자열을 가지는 수동으로 등록한 MemberDAO 객체가 중복으로 컨테이너에 등록되고 있다. 하지만 중복된 컴포넌트가 있을 때처럼 예외는 발생하지 않고 대신 수동으로 등록한 객체가 사용되는 것을 볼 수 있다. 

 

 

[참고 | docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/stereotype/Component.html]

[참고 | docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/context/annotation/ComponentScan.html]