컴포넌트 스캔과 의존관계 자동 주입하기
지금까지는 스프링 빈을 등록할때 자바 코드를 사용해 @Bean을 사용해 설정 정보에 직접 등록할 스프링 빈을 나열했다.
(AppConfig)
예제에선 몇개 다루지 않았지만, 스프링 빈이 많아지게 된다면 일일이 등록하기 힘들고, 설정 정보도 커지고 누락될 위험이 있다.
그래서 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능이 있다.
또 의존관계도 자동으로 주입하는 @Autowired 기능도 있다.
@Configuration
@ComponentScan(
excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class)
)
public class AutoAppConfig {
}
컴포넌트 스캔을 사용하려면 @ComponentScan 어노테이션을 사용하면 된다.
기존의 AppConfig와는 다르게 @Bean으로 등록한 클래스가 하나도 없는것을 확인할 수 있다.
*위 코드의 excludeFilters는 Configuration 어노테이션이 적용된 컴포넌트를 제외하는 코드이다. 예제를 새로 만드는게 아니기 때문에
수동으로 설정한 스프링 빈을 제외하는 코드이므로 그냥 넘어가자.
컴포넌트 스캔은 @Component 어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
@Component
public class MemoryMemberRepository implements MemberRepository{}
@Component
public class RateDiscountPolicy implements DiscountPolicy {}
@Component 추가
@Component
public class MemberServiceImpl implements MemberService {
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
@Component
public class OrderServiceImpl implements OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
}
@Autowired 어노테이션으로 의존관계를 자동으로 주입할 수 있다.
public class AutoAppConfigTest {
@Test
void basicScan() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberService memberService = ac.getBean(MemberService.class);
Assertions.assertThat(memberService).isInstanceOf(MemberService.class);
}
}
AnnotationConfigApplicationContext를 사용하는것은 기존과 동일하다.
설정 정보로 AutoAppConfig를 넘겨주었다.
실행 결과 아래와 같은 로그가 출력되는것을 확인할 수 있다.
ClassPathBeanDefinitionScanner - Identified candidate component class:
.. RateDiscountPolicy.class
.. MemberServiceImpl.class
.. MemoryMemberRepository.class
.. OrderServiceImpl.class
컴포넌트 스캔과 자동 의존관계 주입 동작방법
@ComponentScan

@ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
빈 이름 기본 전략은 앞 대문자를 소문자로 변환해 사용한다.
@Autowired 의존관계 자동 주입

생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다. getBean(MemberRepository.class)와 같다고 생각하면 된다.
orderServiceImpl처럼 생성자에 파라미터가 많아도 다 자동으로 찾아서 주입한다.
위의 예시로 스프링 빈으로 등록된 MemberRepository를 찾을 때, @Component가 붙어 자동으로 빈이 등록된 경우 또는 @Bean을 사용해 수동으로 등록된 경우 MemberRepository 빈을 찾아서 반환하는 것이다.
탐색 위치와 기본 스캔 대상
모든 자바 클래스를 다 컴포넌트 스캔하면 시간이 오래걸린다. 그래서 꼭 필요한 위치부터 탐색하도록 시작위치를 지정할 수 있다.
- basePackages : 탐색할 시작 위치를 지정한다. 이 패키지를 포함해 하위 패키지 모두 탐색
- basePackageClasses : 지정한 클래스의 패키지를 탐색 위치로 지정한다.
만약 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.
권장하는 방법
강사가 추천하는 방법은 별도로 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는것이다.
hello.core에 설정 정보 클래스를 위치한 이유가 위와 같은것이다.
참고로 스프링 부트를 사용하면 @SpringBootApplication를 이 프로젝트 시작 루트 위치에 두는것이 관례이다.
컴포넌트 스캔 대상
- @Component
- @Controller
- @Service
- @Repository
- @Configuration
위의 클래스의 소스코드를 보면 @Component를 포함하고 있으므로 별도로 @Component 어노테이션을 지정하지 않아도 된다.
필터
- includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다.
- excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
@Test
void filterScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
Assertions.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 {
}
}
BeanA는 MyIncludeComponent 어노테이션을 추가해 스프링 빈에 등록되고, BeanB는 MyExcludeComponent 어노테이션을 추가해 스프링 빈에 등록되지 않게 한다.
FilterType 옵션은 5가지가 있는데, 기본값은 ANNOTATION이다. 나머지는 필요할 때 검색해보자
(사실 @Component 만으로도 충분하기 때문에, 필터를 사용할일은 많지 않다. 옵션을 변경하여 사용하기보다 기본 설정에 최대한 맞춰 사용하는것을 권장하고 선호한다고 함)
중복과 충돌
컴포넌트 스캔에서 같은 빈 이름을 등록하게 된다면 어떻게 될까?
- 자동 빈 등록 vs 자동 빈 등록
- 수동 빈 등록 vs 수동 빈 등록
자동빈 등록끼리는 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은경우 예외를 발생시킨다.
- ConflictingBeanDefinitionException
수동 빈 vs 자동 빈 충돌
이 경우, 수동 빈 등록이 우선권을 가지게 된다. (수동 빈이 자동 빈을 오버라이딩 해버림)
수동 빈 등록시 남는 로그 :
Overriding bean definition for bean 'memoryMemberRepository' with a different
definition: replacing
수동 빈은 개발자가 직접 정의했기 때문에 어떻게 보면 수동 빈이 우선권을 가지는게 당연하다. 그러나 보통의 상황에선
개발자가 의도해서 오버라이딩이 되기보단 여러 설정들이 꼬여서 충돌하는 경우가 대부분인게 현실이다..
그래서 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충ㅇ돌하면 오류가 발생하도록 기본 값을 바꿨다.
(오류발생을 하지 않으려면 application.properties에서 설정값을 true로 변경하면 된다.)
요약
1. 컴포넌트 스캔 (@ComponentScan)
• @Component가 붙은 클래스를 자동으로 스프링 빈으로 등록
• 기본적으로 클래스명을 소문자로 변환한 이름으로 빈 등록
• basePackages나 basePackageClasses로 스캔할 위치 지정 가능
• @Controller, @Service, @Repository, @Configuration도 자동 등록됨
2. 의존관계 자동 주입 (@Autowired)
• 생성자에 @Autowired를 붙이면 스프링이 자동으로 타입이 일치하는 빈을 주입
• getBean(MemberRepository.class)와 동일한 방식으로 빈 조회 후 주입
• 여러 개의 빈이 존재하면 충돌 발생
3. 필터 (includeFilters, excludeFilters)
• 특정 어노테이션이나 조건을 기준으로 스캔 대상 추가/제외 가능
• 하지만 기본적으로 @Component 기반의 자동 스캔을 사용하는 것이 권장됨
4. 빈 등록 충돌 해결
• 자동 빈 vs 자동 빈: 동일한 이름의 빈이 있으면 예외 발생
• 수동 빈 vs 자동 빈: 수동 빈이 우선권을 가져 자동 빈을 덮어씀
• 스프링 부트에서는 자동 vs 수동 충돌 시 예외를 발생하도록 기본 설정 변경
핵심 : @ComponentScan을 사용하면 빈을 자동으로 등록할 수 있고, @Autowired로 의존관계를 자동 주입할 수 있다.
자동 / 수동 실무 운영 관점에서?
대부분의 경우엔 편리한 자동 기능을 기본으로 사용하는것을 권장한다.
스프링은 점점 자동을 선호하는 추세이다. @Component 뿐 아니라 @Controller, @Service, @Repostiory 처럼 계층을 맞추어 일반적인 애플리케이션 로직을 자동으로 스캔할 수 있도록 지원한다. 거기에 스프링 부트는 컴포넌트 스캔을 기본으로 사용하고, 다양한 빈들도 조건이 맞는다면 자동으로 등록하도록 설계했다.
설정 정보를 기반으로 구성하는 부분과 실제 동작하는 부분을 명확하게 분리하는것이 이상적이다. 하지만 이것을 모두 다 지키긴 현실적으로 어렵다. @Component 하나만 넣어주면 끝나는 일을 @Configuration 설정 정보에 일일이 @Bean을 적고, 객체 생성, 주입 등등 과정이 너무 번거로워지게 된다. 관리할 빈이 많아지면 더욱 번거롭다.
그리고 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다!
그럼 수동 빈 등록은 언제해야되는데?
비즈니스 로직에 관련된 빈은 자동 등록을 권장
- 컨트롤러, 서비스, 리포지토리 등 보통 비즈니스 요구사항을 개발할 때 추가하거나 변경된다. 이 경우엔 숫자도 매우 많고, 한번 개발해야하면 어느정도 유사한 패턴이 있기 때문에 자동 등록 빈 사용을 권장한다. 문제가 발생해도 비즈니스 로직은 어디서 문제가 발생했는지 명확하게 파악하기 쉽다.
기술 지원 빈은 수동 등록 권장
기술 지원빈은 기술적인 문제나 AOP를 처리할 때 사용된다. DB 연결이나 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다. 기술 지원 로직은 비즈니스 로직보다 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐 광범위하게 영향을 준다. 비즈니스 로직은 문제 발생 시 명확하게 잘 드러나지만, 기술 지원 로직은 적용이 잘 되고 있는지 안되고 있는지 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는것을 권장한다.
비즈니스 로직 중에서 다형성을 적극 활용할 때
DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies) {
this.policyMap = policyMap;
this.policies = policies;
System.out.println("policyMap = " + policyMap);
System.out.println("policies = " + policies);
}
위 코드에서, Map의 DiscountPolicy는 내가 코드를 직접 작성했기 때문에 rateDiscountPolicy, fixDiscountPolicy가 들어온다는 것을 알고 있지만, 다른 사람이 봤을 때 DiscountPolicy에 어떤 값이 들어오는지 한눈에 확인할 수 없다. DiscountPolicy에 직접 접근을 해서 하나씩 봐야하는 경우이다.
@Configuration
public class DiscountPolicyConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
이런경우에 수동 빈으로 등록해서 한눈에 딱 들어오게 별도의 설정 정보를 만들고 수동으로 관리하면 좋다.
핵심은 한눈에 딱 보고 이해가 되어야 한다는 것.
자동 등록으로 관리하게 된다면 특정 패키지에 묶어두는게 좋다.
'Spring' 카테고리의 다른 글
빈 생명주기 콜백 (0) | 2025.04.02 |
---|---|
의존관계 자동 주입 (0) | 2025.03.27 |
싱글톤 컨테이너 (0) | 2025.02.27 |
스프링 컨테이너와 스프링 빈 (1) | 2025.02.24 |
스프링 핵심 원리 (2) (0) | 2025.02.20 |