9 min read

<Spring> 스프링 핵심원리 이해 6 - 컴포넌트 스캔

컴포넌트 스캔과 의존관계 자동 주입 설정하기

  • 지금까지의 과정에선 스프링 빈을 만들 때, 대상이 되는 메서드에 @Bean을 붙여서 AppConfig에 직접 명시하는 방식을 채택했었다.
  • 하지만 이러한 방식은 등록해야 할 빈의 수가 수 백개가 될 경우, 실수를 하게 될 확률이 굉장히 높아지기 때문에 새로운 방식을 채택해야한다.
  • 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.
  • 또한 의존관계를 자동으로 주입해주는 Autowired라는 기능도 제공한다.

먼저 기존의 AppConfig 클래스는 복습을 위해 남겨두고, 컴포넌트 스캔과 Autowired를 공부하기 위해 AutoAppConfig라는 클래스를 AppConfig와 동일한 디렉토리 위치에 생성한다.

package hello.core;

@Configuration
//기존에 AppConfig 클래스의 빈들을 제외해주기 위해서 excludeFilters 사용
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION,classes = Configuration.class)
)
public class AutoAppConfig {

}
  • 컴포넌트 스캔을 사용하기 위해서 @ComponentScan를 설정 정보에 붙여주면 된다.
  • 기존의 AppConfig와 다르게 @Bean을 통해 등록한 클래스가 하나도 없다.

이제 각 클래스에 컴포넌트 스캔의 대상이 되도록 어노테이션을 설정해야한다.

  • MemoryMemberRepository, RateDiscountPolicy는 아래와 같이 @Component를 붙여준다.
   @Component
  public class MemoryMemberRepository implements MemberRepository
  • MemberServiceImpl, OrderServiceImpl은 @Component와 의존관계 주입을 위한 @Autowired도 설정해줘야한다. @Autowired는 아래와 같이 생성자 위에 어노테이션 처리해주면 된다.
	@Component
  public class MemberServiceImpl implements MemberService {
      private final MemberRepository memberRepository;
      @Autowired
      public MemberServiceImpl(MemberRepository memberRepository) {
          this.memberRepository = memberRepository;
      }
}

해당 설정정보가 기존과 동일하게 동작하는지 확인하기 위해 AutoAppConfigTest 클래스를 테스트 내의 scan 패키지 내부에 생성한다.

package hello.core.scan;

import static org.assertj.core.api.Assertions.*;

public class AutoAppConfigTest {
    @Test
    void basicTest() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService memberService = ac.getBean(MemberService.class);

        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}
  • 테스트 통과후, 로그의 일부를 가져와서 읽어보면 아래와 같이 나온다. 생성 되는 빈의 이름은 디폴트 값은 해당 타입의 첫글자를 소문자로 바꾼 형태이다. 예를 들어AutoAppConfig 클래스가 빈으로 등록 될 때는 autoAppConfig로 등록된다.
  • 또한 로그의 상단에 ClassPathBeanDefinitionScanner를 찾을 수 있는데 이는 컴포턴트 스캔이 잘 동작하고 있다고 볼 수 있는 로그이다.

이제 컴포넌트 스캔과 자동 의존관계 주입에 대해 그림으로 알아보자.

  1. @ComponentScan
  • @ComponentScan가 @Compoent가 붙은 모든 클래스를 스프링 빈으로 등록한다.
  • 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
  1. @Autowired 의존관계 자동 주입
  • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 스프링 빈을 찾아서 주입한다.
  • 이때, 해당되는 스프링 빈을 찾는 기본 방법은 타입이 같은 빈을 찾아서 주입해준다.
  • 위 그림의 경우 MemberServiceImpl의 생성자에 MemberRepository 타입의 스프링 빈이 필요했다. 그래서 등록된 빈 중 같은 타입인 memoryMemberRepository을 의존관계 주입해주었다.
  • 주입해줘야할 타입이 여러개인 경우도 전부 알아서 찾아서 주입해준다.

탐색 위치와 디폴트 스캔 대상

  • @ComponentScan의 경우 탐색을 시작할 패키지를 아래와 같이 설정할 수 있다.
  • 아래의 경우 hello.core 패키지부터 하위 패키지로 탐색을 시작한다.
@ComponentScan(
          basePackages = "hello.core",
}
  • 만약 따로 컴포넌트 스캔에 대한 설정을 위와 같이 진행해주지 않으면, 해당 어노테이션이 붙은 설정 정보(AppConfig)클래스의 패키지가 시작위치가 된다.
  • 권장되는 방법은 따로 패키지 위치를 지정하지 않고, 설정 정보 클래스(AppConfig)의 위치를 프로젝트 최상단에 두는 것이다.최근 스프링부트도 이 방법을 기본으로 제공한다.
  • 지금 프로젝트를 기준으로 설명하면, hello.core가 프로젝트 시작 루트이며, 이 패키지에 AppConfig와 같은 메인 설정 정보를 두고 @ComponentScan를 붙이고 basePackages지정은 생략한다.
  • 이렇게 설정하면 hello.core를 포함한 하위 패키지들은 모두 자동으로 컴포넌트 스캔의 대상이 된다. 그리고 프로젝트의 메인 설정정보(AppConfig)는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는것이 관례이다.
  • 추가적으로 스프링 부트를 사용하면 @SpringBootApplication을 설정하는데 이는 @ComponetScan을 포함하고 있다.그래서 위와 같은 방법으로 스프링 빈 스캔을 시작한다.

컴포넌트 스캔 기본 대상

  • 컴포넌트 스캔은 @Component뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함된다.
  • @Component(컴포넌트 스캔에서 사용),@Controller(스프링 MVC 컨트롤러에서 사용), @Service(스프링 비지니스 로직에서 사용),@Repository(스프링 데이터 접근 계층에서 사용),@Configuration(스프링 설정 정보에서 사용)
  • 위에 나열한 어노테이션들을 소스 코드를 보면 전부 @Component를 포함하고 있다.

필터

위의 코드에서 우리는 기존 AppConfig의 스프링 빈들을 중복으로 등록하지 않기 위해excludeFilters를 사용해보았다. 이처럼 필터에는 excludeFilters와 includeFilters가 존재한다. 이는 각각 컴포넌트 스캔할 대상에서 제외하거나 추가하는 기능을 수행한다.아래 테스트 예제로 빠르게 확인해보자

  • 컴포넌트 스캔 대상에 추가할 애노테이션
package hello.core.scan.filter;
  import java.lang.annotation.*;
  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface MyIncludeComponent {
  }
  • 컴포넌트 스캔 대상에서 제외할 애노테이션
package hello.core.scan.filter;
  import java.lang.annotation.*;
  @Target(ElementType.TYPE)
  @Retention(RetentionPolicy.RUNTIME)
  @Documented
  public @interface MyExcludeComponent {
  }
  • 컴포넌트 스캔 대상에서 추가할 클래스 : BeanA 클래스로 설정
package hello.core.scan.filter;

@MyIncludeComponent
public class BeanA {}
  • 컴포넌트 스캔 대상에서 제외할 클래스 : BeanB 클래스 설정
package hello.core.scan.filter;

@MyExcludeComponent
public class BeanB {}
  • 해당 필터에 대한 테스트 코드(ComponentFilerAppConfigTest)
public class ComponentFilterAppConfigTest {
    @Test
    void filterScan() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class)
        );

    }

    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION,classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION,classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig {}
}
  • 해당 내용에 대한 테스트는 이상없이 통과하는 것을 볼 수있다.
  • 애초에 BeanB에대한 스프링 빈 등록은 exclulde를 통해 제외를 했기 때문에 asserThrows가 아닌 getBean을 통해 BeanB를 찾아보면 NoSuchBeanDefinitionException 오류가 발생한다.

중복 등록과 충돌

컴포넌트 스캔에서 중복되는 스프링 빈 이름을 등록하면 어떻게 될까? 아래의 두가지 경우의 수에 대해 충돌 발생 시 어떻게 되는 알아보자.

  1. 자동 빈 등록 vs 자동 빈 등록
  • 스프링 컨테이너의 컴포넌트 스캔에 의해서 자동으로 스프링 빈 이름이 등록될때, 그 이름이 동일한 경우 아래와 같이 오류를 발생시킨다.
  1. 자동 빈 등록 vs 수동 빈 등록
  • 일단 아래의 코드를 AutoAppConfig 클래스에 추가한다. 아래의 코드는 memoryMemberRepository라는 빈을 수동으로 등록해서 중복되게 두개의 빈을 등록시킨다.
  • 스프링에서는 수동으로 빈 이름을 등록하는 것에 우선순위를 두고 수동으로 등록된 빈이 우선권을 가진다.
  • 하지만 최근 스프링 부트에서는 아래와 같이 수동으로 오버라이딩 해버리는것을 에러로 잡아버린다.
  • 만약 수동 스프링 빈이 오버라이딩을 하는것을 허용하고 싶으면 spring.main.allow-bean-definition-overriding=true설정을application.properties 파일에 추가해주면 된다.
스프링 부트는 빈 이름 충돌시 오버라이딩 금지
스프링 부트는 빈 이름 충돌시 오버라이딩 금지

소스코드 : https://github.com/brido4125/core-spring

해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.