[Spring 핵심원리 기본편] 컴포넌트 스캔
by mini_min
끝이 보인다. 아자아자!!
컴포넌트 스캔과 의존관계 자동 주입 시작하기
지금까지는 직접 등록할 스프링 빈을 나열했다. 이렇게 등록할 빈이 수백개가 되면, 하나하나 등록하기 어렵다.
그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다. 또한, 의존관계도 자동으로 주입하는 @Autowired 기능도 제공한다.
먼저, @ComponentScan 을 설정 정보에 등록하면 된다. 기존 AppConfig 와 다르게 @Bean 으로 등록한 클래스가 없다!
@ComponentScan 은 이름 그대로 @ComponentScan 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 자동 등록한다.
@Configuration @ComponentScan( excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class) ) public class AutoAppConfig { }
이제 각 클래스가 컴포넌트 스캔의 대상이 되도록, @Component 애노테이션을 붙여주자.
이전에는 AppConfig 에서 직접 @Bean 애노테이션으로 설정 정보를 작성했고, 의존관계도 직접 명시했다. 이제는 @Autowired 로 의존관계를 자동으로 주입할 수 있다.
(밑에 MemberServiceImpl 참고!)
@Component public class MemoryMemberRepository implements MemberRepository {}
@Component public class RateDiscountPolicy implements DiscountPolicy {}
@Component public class MemberServiceImpl implements MemberService{ private final MemberRepository memberRepository; @Autowired public MemberServiceImpl(MemberRepository memberRepository) { this.memberRepository = memberRepository; }
OrderServiceImpl 에도 똑같이 @Component, @Autowired 추가하자
@Component //@RequiredArgsConstructor public class OrderServiceImpl implements OrderService{ private final MemberRepository memberRepository; private final DiscountPolicy discountPolicy; public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) { this.memberRepository = memberRepository; this.discountPolicy = discountPolicy; } }
AutoAppConfigTest.java
에서 AnnotationConfigApplicationContext 를 사용하는 것은 기존과 동일하다.
Test 코드를 실행시켜보면, 설정 정보로 AutoAppConfig 클래스들을 넘겨주고 기존과 같이 잘 동작하는 것을 볼 수 있다.
package hello.core.scan; import hello.core.AutoAppConfig; import hello.core.member.MemberService; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; public class AutoAppConfigTest { @Test void basicScan(){ AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class); MemberService memberService = ac.getBean(MemberService.class); Assertions.assertThat(memberService).isInstanceOf(MemberService.class); } }
컴포넌트 스캔 동작 원리
1. @ComponentScan 이 @Component 가 붙은 모든 클래스를 스프링 빈에 등록한다.

2. @Autowired 로 의존관계를 자동 주입한다.
생성자에 @Autowired 를 지정해주자. 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
= getBean(MemberRepository.class) 와 동일하다고 이해하면 된다.

탐색 위치와 기본 스캔 대상
탐색할 패키지의 시작 위치 지정하기는 꼭 필요한 위치부터 탐색하도록 할 수 있다.
basePackages : 탐색할 패키지의 시작 위치를 지정한다.
basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
지정이 없다면, 컴포넌트 스캔이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
@ComponentScan( basePackages = "hello.core", }
컴포넌트 스캔 기본 대상
@Component : 컴포넌트 스캔에서 사용
@Controlller : 스프링 MVC 컨트롤러에서 사용
@Service : 스프링 비즈니스 로직에서 사용
@Repository : 스프링 데이터 접근 계층에서 사용
@Configuration : 스프링 설정 정보에서 사용
📝 참고로 useDefaultFilters 옵션이 기본으로 켜져있는데, 이 옵션을 끄면 기본 스캔 대상들이 제외된다.
필터
includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
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 { }
위에서 컴포넌트 스캔 대상에서 제외할 애노테이션과 포함할 애노테이션을 만들어준다. 그리고 컴포넌트 스캔 대상에 추가할 클래스와 제외할 클래스를 만든다. (이때, 필터용으로 만든 애노테이션들을 붙여준다.)
package hello.core.scan.filter; @MyIncludeComponent public class BeanA { }
package hello.core.scan.filter; @MyExcludeComponent public class BeanB { }
includeFilters 에 MyIncludeComponent 애노테이션을 추가해서 BeanA가 스프링 빈에 등록된다. excludeFilters 에 MyExcludeComponent 애노테이션을 추가해서 BeanB는 스프링 빈에 등록되지 않는다.
package hello.core.scan.filter; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import static org.assertj.core.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*; public class ComponentFilterAppConfigTest { @Test void filterScan(){ AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class); BeanA beanA = ac.getBean("beanA", BeanA.class); assertThat(beanA).isNotNull(); } @Configuration @ComponentScan( includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class), excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class) ) static class ComponentFilterAppConfig{ } }
FilterType 옵션
ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
ex) org.example.SomeAnnotation
ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다.
ex) org.example.SomeClass
ASPECTJ: AspectJ 패턴 사용
ex) org.example..*Service+
REGEX: 정규 표현식
ex) org\.example\.Default.*
CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
ex) org.example.MyTypeFilter
📝 참고로, @Component 애노테이션만으로 충분하기에 includeFilters 는 사용할 일이 거의 없더. excludeFilters 는 여러 이유로 간~혹 사용할 때가 있지만 많지는 않다.
중복 등록과 충돌
1. 자동 빈 등록 vs 자동 빈 등록
2. 수동 빈 등록 vs 자동 빈 등록
만약 빈 등록시에 같은 빈의 이름이 동시에 등록되면 어떻게 될까....?
1번의 경우 컴포넌트 스캔에 의해 자동으로 빈이 등록될 때 ConflictingBeanDefinitionException 예외가 발생한다.
2번의 경우 자동 vs 수동에서 수동 빈 등록이 우선권을 가진다. 수동 빈이 자동 빈을 오버라이딩 해버린다.
-- 수동 빈 등록시 남는 로그 replacing [Generic bean: class [hello.core.member.MemoryMemberRepository];
자동보다는 수동이 우선권을 가지는 것이 좋지만, 보통 이런 결과가 의도적으로 만들어지는 경우는 드물기에, 수동과 자동이 충돌하지 않도록 로직을 짜는 것이 중요하다.
스프링 부트에서 실행하면,
The bean 'memoryMemberRepository', defined in class path resource [hello/core/AutoAppConfig.class], could not be registered. A bean with that name has already been defined in file [C:\Users\Back\Downloads\core\core\out\production\classes\hello\core\member\MemoryMemberRepository.class] and overriding is disabled.
친절하게 위와 같은 오류를 보내준다. 만약 오버라이딩 하고 싶으면 Application.properties 에 오버라딩=true 를 치라고 나온다.
블로그의 정보
개발자 미니민의 개발로그
mini_min