지금까지는 AppConfig.class 를 통해서 컨테이너에 스프링 빈을 등록해왔습니다. 예제에서는 3,4개만 주입하여
활용할 수 있었지만, 실무에서는 수도없이 많은 빈들이 컨테이너에 등록되어 동작을 해야합니다. 수많은 엔티티 도
메인을 위한 Repository 들, 수많은 Controller, Service 들 등 많은 관리가 필요한데, 이 모든 것들을 다 Config 에
등재를 하려면 꽤나 복잡한 작업이 될 것이고, Config 도 종류별로 나누어야 하기 때문에 또다른 일들이 추가될 것
입니다.
ComponentScan의 등장
따라서 Spring은 굳이 Config 파일들을 만들어서 읽게 해주지 말고, 각 클래스를 만들 때마다 빈 등록이 필요하면
Annotation 을 통해서빈 등록이 될 수 있게끔 하는 방식으로 변경을 하는데, 이를 ComponentScan 이라고 합니
다.
@Configuration
@ComponentScan
public class AutoAppConfig {
}
이와 같은 Config Class 를 생성해준 후 @ComponentScan 을 등록해주면, 앱 내에서 @Component 가 부착되
어 있는 Class 들은 모두 Bean으로 등록해 Spring Container 에서 관리될 수 있게 해줍니다. 따라서 Spring 등록
이 필요한 지금까지의 구현체들, (Bean 으로 등록되는 것은 구현체들이였습니다) MemoryMemberRepository,
Fix/RateDiscountPolicy, 그리고 각 ServiceImpl Class 들에 @Component 를 달아서 Bean으로 등록할 수 있도
록 해주겠습니다.
AppSpringConfig 와 같은 Config Class 를 구성할 때는, 역할체에 특정 구현체를 생성자에 넣어서 의존 관계를 주
입하였는데, 이를 생성자를 통한 의존관계 주입으로 저희는 알고 있습니다.
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
이와 같이 MemberService 역할의 구현체인 MemberServiceImpl을 Bean으로 등록하는 와중에,
MemberRepository에 의존함을 생성자를 통해 주입 해줬습니다. 그리고 MemberRepository의 역할은
MemoryMemberRepository를 구현체로 Bean 등록을 해준 모습도 확인할 수 있습니다.
Autowired의 등장
하지만 @ComponentScan을 사용하게 되면 해당 클래스에 아무것도 명시하지 않게 되므로, 의존 관계를 주입하지
못하는 문제가 발생합니다. 따라서 Bean 등록을 해주는 구현체 중 의존관계 주입(DI)이 필요한 객체에는 다음과 같
이 해당 클래스 생성자에 @Autowired 를 통해서 의존관계 자동 주입을 해주게 됩니다. 결론적으로 다음과 같은 모
습을 하게 됩니다.
@Component
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository; // 구현체 주입 필요
@Autowired
public MemberServiceImpl(MemberRepository memberRepository){
this.memberRepository = memberRepository;
}
//... 로직들
}
즉, 해당 생성자에 필요한 변수 객체들은 모두 Spring Container에서 싱글톤 빈으로 찾아와서 넣어주라는 뜻
으로 보시면 됩니다. 이 Autowire의 전략에 대해서는 다음 포스트에서 더 자세히 살펴보겠습니다.
Component Scan의 동작 방식
[컴포넌트 스캔과 의존관계 자동 주입 시작하기 15분]
Component Scan은 말 그대로 @Component 가 붙은 모든 클래스를 스프링 컨테이너에 모두 스프링 빈으로 등
록합니다. 가령, 위 MemberServiceImpl.class는 @Component가 붙었으므로 Bean 등록의 대상이 됩니다. Bean
이름은 기본적으로 클래스 이름을 사용하지만, memberServiceImpl의 이름으로 Bean이 등록되게 됩니다.
생성자에 @Autowired 지정을 하면 스프링 컨테이너는 자신의 빈 저장소에 저장되어 있는 같은 타입의 빈 (당연히
자식도 포함)을 찾아서 주입을 합니다. 기본적으로 같은 Instance의 빈을 찾아서 주입하는 전략이며, 같은 타입이
여러 개일 경우는 바로 아래서 살펴보겠습니다.
* 참고) NoSuchBeanDefinitionException
만약 저처럼 Config Class 들을 따로 Directory 를 만들어서 실행중이셨던 분들은 해당 Config 파일로 Test시
NoSuchBeanDefinitionException 에러가 발생하는 모습을 보실 수 있으실 겁니다. (Test 돌려서 Bean 을 확인해
보시면 @Component 가 달린 어떤 클래스도 Bean 등록이 안되어 있는걸 확인하실 수 있습니다)
그 이유는 ComponentScan 조회 전략이 바로 Scan 대상 Class와 동일한 Package 만 기준으로 탐색을 하기 때문
입니다. 따라서 해당 Config Class 를 최상위 디렉토리로 변경하거나, 아니면 다음과 같이 지정해주면 됩니다.
@Configuration
@ComponentScan (
basePackages = "{탐색 패키지 경로}" // ex: com.example.mypackage 시 전체 탐색
)
public class AutoAppConfig {
}
굳이 지정해주고 싶지 않으면, Config Class 를 최상위 디렉토리에 두면 됩니다. Default 로 해당 Class 패키지부터
하위 패키지들을 탐색하기 때문입니다. (강의에서는 이 방식을 권장, 앱을 대표하는 내용이기 때문. 저는 개인적으
로 Config 디렉토리를 두는 것을 선호합니다. Config 클래스가 하나가 아닌 경우가 많았기 때문입니다! 어디까지
나 개별 의사 차이)
(그렇다면 그냥 이 ComponentSCan 클래스는 왜 있는걸까요??) ___ 질문 답변 확인 필요
Component 기본 스캔 대상
@Component 뿐만 아니라 다음과 같은 Annotation 들도 기본 탐색 대상이 됩니다.
@Controller / @Service / @Repository / @Configuration
웹 백엔드 실무 중 비즈니스 로직에서 자주 사용하는 Annotation 들입니다. 해당 Annotation 들의 클래스로 들어
가보면 모두 기본적으로 @Component로 지정이 되어 있기 때문에 모두 기본 탐색 대상에 포함됩니다. (참고로
Annotation 연장 기능은 Spring 한정 지원 기술입니다)
* 참고) ConflictingBeanDefinitionException
해당 Exception 이 발생할 경우 생성되고 있는 Bean 중 이름 (Definition 항목 중 methodName)이 중복되는
Bean 들을 감지하여 throw 시키는 예외입니다. 실무에서 사용되는 패키지 내 클래스들은 정말 많고, Bean 항목들
도 정말 많기 때문에 ComponenetScan 을 쓰기도 하고, 수동 등록을 쓰기도 하고 합니다. 이 때 깔끔함을 추구하
기 위해 Bean (Component) Name 을 등록하다보면 충돌이 생각보다 빈번하게 발생하기도 합니다.
자동 등록 Bean (ComponentScan, Configuration , Autowired 등을 사용하는 경우) 끼리 충돌이 나는 경우는
위와 같은 Exception 처리가 발생합니다. 하지만 수동 등록 Bean 과 자동 등록 Bean 끼리 같은 이름을 갖는 경우
는 어떨까요? 우리 앱 내에 MemoryMemberRepository.class 는 @Component 로 인해서 현재 자동으로 등록이
되므로, 현재 @ComponentScan 클래스에 동일한 이름의 @Bean 을 추가해서 동일한 이름으로 수동 등록 시켜보
겠습니다.
@Configuration
@ComponentScan (
basePackages = "com.example.basicprinciple",
)
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
}
아무 테스트를 실행 시켜보면 별다른 Error 가 발생하지 않고, 다음과 같은 log 를 확인할 수 있습니다.
"Overriding bean definition for bean 'memoryMemberRepository' with a different definition: replacing ~"
전자에서 나오는 memoryMemberRepository 가 수동으로 등록한 Bean 이며, 자동 vs 수동인 경우 수동 등록
Bean이 우선권을 가지게 되어 있습니다. 하지만 이렇게 자동으로 뭔가를 시스템이 해버리면, 나중에 개발자의 의도
와는 다르게 꼬여버리는 문제가 발생할 수도 있기 때문에, 최근 스프링 버전에서는 그냥 빈 충돌이 나면 오류가 발
생하도록 처리되는 추세입니다. (위 코드도 Test 로 쓰면 에러는 발생시키지 않는데, 앱 실행을 해보면 에러가 발생
합니다)
* 참고) UnsatisfiedDependencyException: expected single matching bean but found 2
이정도면 참고가 아닌것 같긴하네요. 위에서 설명하였듯이, 같은 Type Bean의 두 가지 (혹은 이상) 구현체가 같이
등록이 지정되었을 때 발생하는 에러입니다. 이름이 다르더라도 역할이 같은 구현 객체 두가지 이상을 등록하려 하
였을 경우 발생한다고 보시면 됩니다.
@Component
public class FixDiscountPolicy implements DiscountPolicy{
... Override Methods
}
@Component
public class RateDiscountPolicy implements DiscountPolicy{
... Override Methods
}
첫 포스트에서 만들었던 두 가지 할인 정책 Fix Discount Policy 와 Rate Discount Policy 두 가지 클래스들을 기억
하실 겁니다. 둘다 DiscountPolicy 라는 역할 객체의 구현 객체입니다. 둘 다 쓰이기 때문에 둘 다 @Component
를 달아서 Bean 등록을 하게 했다면, 해당 에러가 발생하는 것을 보실 수 있습니다. 만약 두 구현체를 번갈아 가면
서 써야 하는 경우가 실제로 있다면, 두 가지 Annotation 을 통해서 구현해주시면 됩니다. (실제로 두 구현체가 쓰
여야 하는 경우가 많다고 합니다)
@Primary
@Component
public class FixDiscountPolicy implements DiscountPolicy{
... Override Methods
}
@Primary
@Component
public class RateDiscountPolicy implements DiscountPolicy{
... Override Methods
}
Bean 초기 생성시 우선시하여 구현객체로 등록할 객체에 달아줄 annotation 입니다. 이렇게 되면 DiscountPolicy
역할의 구현 객체는 RateDiscountPolicy 객체로 Bean 등록이 되며, DiscountPolicy의 의존성 주입이 필요한 객체
들 역시 RateDiscountPolicy 로 주입이 되게 됩니다.
@Qualifier(name)
같은 Bean Type 내에서 구분을 둘 수 있는 이름을 지정해주는 Annotation 입니다. method name 을 바꾸는 것이
아니며 기준이 더 추가되어 복잡해질 수도 있기 때문에, 많은 구현 객체들을 다뤄야 하는 것이 아닌 이상 권장하진
않는 방법입니다. (빈 method name 으로 우선 구분 가능합니다)
이렇게 같은 Type 의 Bean 을 여러 개 등록해 놓았을 때, 어떤식으로 관리하는게 좋을까요? Spring 에서는 각
bean 의 이름을 통해서 자동 주입을 받아 관리할 수 있도록 지원해줍니다.
@Test
@DisplayName("이 빈이 저 빈이냐?")
void typeEquallBean(){
DiscountPolicy dp = ac.getBean(DiscountPolicy.class);
Map<String, DiscountPolicy> bMap = ac.getBeansOfType(DiscountPolicy.class);
System.out.println("bMap = " + beansOfType);
}
Spring Container 에 등록된 같은 타입의 객체를 ac.getBeansOfType() 으로 조회를 하게 되면 Key 를 Bean
Method Name, Value 를 Bean 객체로 저장시킨 HashMap 을 반환해줍니다. 해당 Map 을 조회시 두 객체가 모
두 Bean 으로 등록되어 Map에 추가된 모습을 확인할 수 있습니다.
bMap = {fixDiscountPolicy=com.~~FixDiscountPolicy@1xx,
rateDiscountPolicy=com.~~RateDiscountPolicy@7xx}
또한, 만약 Service 가 클라이언트로서 DiscountPolicy에 의존하여 주입을 받아야 한다면, 다음과 같이 주입을 받아
서 Service 내에서 필요에 따라 변환시켜주며 구현 객체를 바꿔 끼워줄 수 있습니다.
@Component
class DiscountService {
private final Map<String, DiscountPolicy> policyMap;
public DiscountService(Map<String, DiscountPolicy> policyMap) {
this.policyMap = policyMap;
}
public int giveDiscount(String policyName){
DiscountPolicy dp = policyMap.get(policyName); // 원하는 dp 를 꺼낼 수 있음
...
}
}
이렇게 ComponentScan을 통해서 빈을 관리하면 일일이 Configuration 파일을 만들어 빈 등록을 안해줘도 되
고, 앱 실행시 사용할 Configuration 파일을 지정해주지 않아도 됩니다. 하지만 모든 것에는 단점이 있듯이, 발
생하는 Error들만 봐도 아실 수 있겠지만 Bean들의 관리가 다소 어려울 수도 있습니다. 실제 운영 서비스 같
은 경우에는 그 내부가 매우 복잡하기 때문에, 필요한 Bean이 어디에 설정되어 있고, 무슨 일을 하고 있는지
정리가 안될 때가 많습니다.
따라서 Configuration 등록과 ComponentScan을 둘다 잘 활용하여 Bean들을 정확하게 관리하는 것이 유지
보수에도 중요하고, 객체지향 관점에서도 잘 설계되었다 할 수 있겠습니다. (말은 쉽죠... ㅎㅎ 계속 하면서 발
전해 나가야 합니다 :])

포스트 요약
- 해당 클래스 패키지 기준으로 하위 클래스들 내에서 @Component를 탐색하여 Bean 등록을 해주는 기능을 @ComponentScan이라 한다
- ComponentScan을 사용하면 일일이 Bean 등록을 하지 않아도 되는 장점이 있으나, 비즈니스 로직 외의 Bean 들이 어디에서 무슨 역할을 하고 있는지 한번에 알아보기 어렵다
- ComponentScan을 등록하면 디렉토리, 동일한 빈 객체 instance, 동일한 빈 이름에 대한 오류가 발생할 수 있다.
- 빈들을 잘 관리하여 사용하는 것이 ComponentScan 사용시 중요한 점이다
출처
[스프링 기본]으로 엮인 모든 포스트들은 교육 사이트 인프런의 지식공유자이신 김영한님의 [스프링 핵심 원리] 강의를 기반으로 작성되었습니다. 열심히 정리하고 스스로 공부하기 위해 만든 포스트이지만, 제대로 공부하고 싶으시면 해당 강의를 꼭 들으시는 것을 추천드립니다.
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...
www.inflearn.com
'Spring > Spring 기본' 카테고리의 다른 글
| [Spring 기본] Bean 생명주기 콜백 (0) | 2022.10.18 |
|---|---|
| [Spring 기본] 의존 관계 주입 전략 (DI Strategy) (0) | 2022.10.17 |
| [Spring 기본] Spring Container의 Singleton 전략 (0) | 2022.10.14 |
| [Spring 기본] Container와 Bean과 조금 더 친해져보자 (0) | 2022.10.13 |
| [Spring 기본] Spring의 객체 지향 IoC, DI, Bean, Container (0) | 2022.10.13 |