이 게시글은 인프런의 유료 강의 스프링 핵심 원리 - 기본편을 수강하며 공부한 내용을 담고있습니다.
저작권에 의해 강의내용에 해당하는 전체 코드는 공개할 수 없습니다.
저작권에 의해 모든 내용을 담을 수 없습니다. 중복 및 반복되는 내용은 생략했습니다.
컴포넌트 스캔과 의존관계 자동 주입
@Configuration 애노테이션과 @Bean 애노테이션이 붙은 메서드들을 일일히 작성해서 스프링 빈으로 등록하고 의존관계 주입을 했다.
만약 등록해야할 빈이 수백개이고 의존관계도 엄청 많다면 코드를 모두 작성하기 어려울 것이다.
이를 쉽게 하기 위해서 @Component 와 @CompnentScan, @Autowired 애노테이션을 사용할 수 있다.
@ComponentScan
@ComponentScan 은 이름 그대로 @Component 가 붙은 클래스들을 읽어들여 Bean으로 만들어준다.
1
2
3
4
5
6
@Configuration
@ComponentScan
public class AutoAppConfig {
// 기존의 AppConfig 를 자동으로 수행한다는 의미로 AutoAppConfig 라는 이름을 붙임
// 클래스 내부에는 아무것도 없다.
}
기존의 AppConfig 를 대체할 클래스를 만들고 @Configuration @ComponentScan 를 붙여준다.
@ComponentScan 은 @Component 를 읽어들인다.

문제는 @Configuration 에 @Component가 내장되어 있으므로 기존의 AppConfig 도 @Component로 읽혀버린다.
읽어들인 AppConfig 를 Bean 으로 등록해버리니 @Component로 Bean을 생성하는 실습을 할 수 없다.
따라서 AppConfig 를 삭제하거나 다음과 같이 필터를 걸어버린다.
1
2
3
4
5
6
7
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes =Configuration.class))
public class AutoAppConfig {
// 기존의 AppConfig 를 자동으로 수행한다는 의미로 AutoAppConfig 라는 이름을 붙임
// 클래스 내부에는 아무것도 없다.
}
위 필터는 @Configuration 을 읽지 않는다는 의미이다. 이렇게 해서 AppConfig 는 무시하고 나머지 @Component 만 읽을 수 있게 된다.
여기서는 기존 예제를 지우지 않고 @ComponentScan 을 실습하기 위해 위와같은 필터를 작성했지만 평소에는 @Configuration 을 필터로 걸 일은 없다. 대신 excludeFilters 의 사용방법을 익히면 된다.
Bean 등록

AppConfig 에서 스프링 빈으로 등록한 객체들은 다음과 같다.
- memberServiceImpl
- orderServiceImpl
- memoryMemberRepository
- rateDiscountPolicy
이번에는 @Component 를 사용해 Bean 으로 등록할 것이다.
각각의 클래스에 @Component 를 붙여준다.
1
2
3
4
@Component
public class MemberServiceImpl implements MemberService{
// (이하 생략)
}
1
2
3
4
@Component
public class OrderServiceImpl implements OrderService{
// (이하 생략)
}
1
2
3
4
@Component
public class MemoryMemberRepository implements MemberRepository{
// (이하 생략)
}
1
2
3
4
@Component("원하는 이름 작성") // Bean이름을 지정해줄 수 있다.
public class RateDiscountPolicy implements DiscountPolicy{
// (이하 생략)
}
이렇게 해주면 기존의 AppConfig 에서 @Bean 을 붙였던것과 동일하게 Bean 이 만들어진다.
생성된 Bean 의 이름은 기존의 이름 규칙을 그대로 준수한다. 기존에는 메소드명이 그대로 Bean명이 되었다.
memberService 메서드로 Bean 을 만들면 Bean명은 memberService 였다.
하지만 지금은 클래스를 Bean으로 등록한다.
MemberServiceImpl 클래스로 Bean 을 만들면 이름은 그대로 가져오되 memberServiceImpl 로 첫글자를 소문자로 바꾼다.
만약 이름을 직접 지정해야하면 @Component(“이름작성”) 으로 작성하면 된다.
Bean 은 생성되었지만 의존관계는 아직 없는 상태이다.
의존관계 주입
의존관계는 @Autowired 를 작성하면 된다.
Auto = 자동, wired = 연결
1
2
3
4
5
6
7
8
9
10
@Component
public class MemberServiceImpl implements MemberService{
private final MemberRepository memberRepository;
@Autowired
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// (이하 생략)
}
생성자 위에 @Autowired 를 작성하면 의존관계 주입이 자동으로 된다.
생성자의 매개변수를 보면 MemberRepository 클래스 객체가 필요한 것을 알 수 있다.
그럼 Spring 은 Spring Container 에서 MemberRepository 클래스인 Bean 을 찾아서 주입해준다.
위에서 @Component 를 이용해 만든 Bean 중 MemberRepository 를 상속받고 있는 MemoryMemberRepository 가 주입된다.
그럼 이런 의문이 생긴다. MemberRepository 에 해당되는 Bean 이 여러개라면?? 당연히 충돌이 발생한다.
하지만 여기서는 다루지 않고 뒤에서 배운다.
테스트 코드 작성
@Component 로 등록한 Bean과 @Autowired 로 주입된 객체를 비교하여 제대로 작동하는지 테스트해보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class AutoAppConfigTest {
@Test
void basicScan() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);
MemberRepository memberRepository1 = ac.getBean(MemberRepository.class);
MemberServiceImpl memberServiceImpl = ac.getBean(MemberServiceImpl.class);
MemberRepository memberRepository2 = memberServiceImpl.getMemberRepository();
System.out.println("memberRepository1 = " + memberRepository1);
System.out.println("memberRepository2 = " + memberRepository2);
Assertions.assertThat(memberRepository1).isSameAs(memberRepository2);
}
}
1
2
3
4
5
6
7
8
9
Identified candidate component class: ...(중략).../RateDiscountPolicy.class
-> 번역: 컴포넌트 후보 클래스가 식별되다: ...(중략).../RateDiscountPolicy.class
-> RateDiscountPolicy 에 @Component 를 작성했기 때문에 컴포넌트 후보가 됨
Creating shared instance of singleton bean 'rateDiscountPolicy'
-> 번역: rateDiscountPolicy 가 Singleton Bean 으로 만들어졌다.
Autowiring by type from bean name 'orderServiceImpl' via constructor to bean named 'rateDiscountPolicy'
-> 번역: orderServiceImpl 이라는 Bean에 생성자를 통해 rateDiscountPolicy 가 Autowired(자동주입)되었다.
테스트 코드를 작성하고 실행해보면, 테스트 성공은 물론이고 위와 같은 로그가 출력된다.
탐색 위치와 기본 스캔 대상
탐색 위치
@ComponentScan 사용 시 어떤 경로를 탐색해서 @Component 를 찾을까?
기본적으로 가져오는 라이브러리만 해도 수십개인데 그걸 다 탐색해버리는걸까?
ComponentScan 이 탐색하는 경로를 지정하는 방법과 default 탐색 경로에 대해 알아본다.
1
2
3
4
@ComponentScan(
basePackages = "hello.core"
)
위 코드처럼 basePackages 에 컴포넌트 스캔 경로를 작성할 수 있다. 해당 경로부터 하위 경로까지 전부 ComponentScan 대상이 된다.
그럼 만약 basePackages 를 작성하지 않으면 어떻게 될까??
@ComponentScan 이 있는 패키지가 basePackages 로 지정이된다.
즉 위 코드는 경로를 작성한 것과 작성하지 않은 것의 결과가 동일하다는 것이다.
또 알아두어야 할 것은 스프링 프로젝트를 만들면 기본적으로 @SpringBootApplication 이 있는데, 이걸 cmd + 클릭 해보면 내부에 @ComponentScan이 있는 것을 확인할 수 있다.
즉, 기본적으로 우리는 @ComponentScan을 사용할 일이 없다.
기본 스캔 대상
@ComponentScan 의 스캔 대상은 다음과 같다.
- @Component
- @Controller
- @Service
- @Repository
- @Configuration
스캔이 되는 이유는 해당 애노테이션들이 @Component 를 포함하고 있기 때문이다.
만약 intellij IDE 를 사용하고 있다면 애노테이션을 cmd + 클릭하여 코드를 직접확인할 수 있다.
@Controller 의 코드를 직접 확인해보면 아래와 같다.
1
2
3
4
5
6
7
8
9
10
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
해당 애노테이션들이 하는 역할도 겸사겸사 알아보자.
- @Controller
- Spring MVC 에서 Controller 로 인식되는 역할을 해준다.
- @Repository
- 스프링 데이터 접근 계층으로 인식하게 해준다. 그리고 데이터 계층 예외를 스프링 예외로 변환해준다.
- @Configuration
- 스프링 설정 정보로 인식하고, @Bean 들이 싱글톤을 유지하도록 해준다.
- @Service
- 특별한 처리를 하지 않는다. 개발자들이 비즈니스 계층임을 인식할 수 있게 한다.
@Repository 의 설명 중에 데이터 계층 예외를 스프링 예외로 변환해준다는 의미는 다음과 같다.
이해하기 쉬운 예시로 개발할 때는 MySQL 에 연동을 했다가 이후에 Oracle DB 로 전환을 했다고 가정하자.
그럼 개발과정에서 DB 관련 예외가 발생하면 MySQL 에서 발생한 예외가 있을텐데, 이를 서비스 계층에서 예외를 처리하는 코드를 작성했다.
하지만 나중에 Oracle DB 로 바꾸면서 동일한 원인에 의해 예외가 발생했지만 처리하는 방법이 다르게 될 수가 있다.
기존에 MySQL 을 기준으로 작성된 예외처리는 다시작성해야할 수 있게된다.
이를 방지하기위해서 Spring 에서 데이터 계층에서 발생한 예외를 스프링 예외로 변환해주면, 개발자는 스프링 예외만 예외처리를 해주면된다.
추후 DB를 바꾸게 되는 일이 있어도 예외처리에 대한 코드를 바꾸게 되는 일을 줄일 수 있다.
필터
@ComponentScan 에 사용할 수 있는 Filter 에 대해 알아본다.
- includeFilter : 스캔 대상을 추가로 지정한다.
- excludeFilter : 스캔 대상에서 제외한다.
1
2
3
@ComponentScan(
includeFilter = @ComponentScan.Filter(type = FilterType.Annotation, classes = MyIncludeComponent.class)
)
사용법은 위와 같다. classes 에는 스캔대상에 포함될 클래스를 작성한다. 여기서는 MyIncludeComponent 라는 클래스를 작성했다.
type 에는 classes 에 작성한 클래스의 타입을 작성한다. MyIncludeComponent 는 애노테이션이므로 타입도 애노테이션이다.
이렇게 해주면 @MyIncludeComponent 가 붙은 클래스는 전부 Bean 으로 등록된다.
type 은 FilterType.Annotation 이 Default 값이라 생략해도 된다.
예제 코드
1
2
3
4
5
6
7
8
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
// include filter 를 적용시킬 예제이다.
// MyExcludeComponent 는 클래스명만 다르다.
// @Target, @Retention, @Documented 에 대해서는 일단 pass 한다.
}
나만의 애노테이션을 하나 만든다. 해당 애노테이션은 @Component 가 없으므로 @ComponentScan 대상에서 제외된다.
1
2
3
4
5
6
7
@Configuration
@ComponentScan(
includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class))
static class ComponentFilterAppConfig {
// 따로 코드 없음.
}
@ComponentScan 에 필터를 작성한다. @MyIncludeComponent 가 붙어있으면 Bean 으로 등록하고 @MyExcludeComponent 가 붙어있으면 Bean으로 등록하지 않는다.
1
2
3
@MyIncludeComponent
public class BeanA {
}
1
2
3
@MyExcludeComponent
public class BeanB {
}
1
2
3
4
5
6
7
8
9
10
@Test
void myTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
BeanA beanA = ac.getBean("beanA", BeanA.class);
// BeanB beanB = ac.getBean("beanB", BeanB.class); // 예외발생 why? beanB 는 스캔이 안되서 Bean 등록이 안됨.
assertThat(beanA).isNotNull();
assertThrows(NoSuchBeanDefinitionException.class, () -> {
BeanB beanB = ac.getBean("beanB", BeanB.class);});
}
사실 beanB 는 @MyExcludeComponent 내부에 @Component 가 없기 때문에 Exclude Filter 로 설정하지 않아도 NoSuchBeanDefinitionException 예외가 발생한다. 그래도 그냥 컴포넌트 스캔이 되었다고 상상하고 Exclude Filter 로 인해 예외가 발생했다고 생각하자.
타입 옵션
- FilterType.ANNOTATION : Default 값. 애노테이션을 인식한다.
- FilterType.ASSIGNABLE_TYPE : 지정한 클래스와 자식 클래스를 인식한다.
- FilterType.ASPECTJ : AspectJ 패턴 사용
- FilterType.REGEX : 정규식
- FilterType.CUSTOM : 필터 타입을 직접 커스텀해서 지정.
1
2
3
4
5
// 예시
excludeFilter = {
@Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class),
@Filter(type = FilterType.ASSIGNBAL_TYPE, classes = BeanA.class)
}
이렇게 필터를 걸면 BeanA 클래스는 excludeFilter 에 걸러져서 빈 등록이 되지않는다.
마지막으로
Filter 를 공부하다가 includeFilter 가 필요한가 싶을수도 있다.
왜냐하면 @SpringBootApplication 내부에서 기본적으로 @ComponentScan 을 제공하기 때문에 내가 직접 @ComponentScan을 작성할 일이 없다. 그러면 자연스래 Filter 를 작성하지 않는 방향으로 코딩을 하게된다.
애초에 includeFilter 을 이용해 Bean을 직접 등록하기보다 @Component 를 붙여서 Bean으로 등록하면 되는 것도 있다.
결국 필요하다면 excludeFilter 정도만 이용하게 된다.
그래도 알아두어야 사용할 수 있다.
컴포넌트 중복 등록 및 충돌
@Component 가 붙어있거나 @Bean 거나 등등 여러가지 방법으로 Bean 을 등록할 수 있다.
만약 중복으로 Bean(같은 이름의 Bean) 이 등록되면 어떻게 될까??
- 자동 Bean 등록 vs 자동 Bean 등록
1
2
3
4
5
6
7
8
9
@Component
public class MemoryMemberRepository implements MemberRepository {
// 이하 생략
}
@Component("memoryMemberRepository")
public class MemberServiceImpl implements MemberService{
// 이하 생략
}
자동 Bean 등록 방식으로 memoryMemberRepository 이름을 가진 Bean 을 두 개 만들었다.
이렇게 되면 memoryMemberRepository 빈이 두 개 만들어지기 때문에 충돌이 발생한다.
충돌이 발생하면 에러 메세지가 나와서 확인이 가능하다.
- 자동 Bean 등록 vs 수동 Bean 등록
1
2
3
4
5
6
7
8
9
10
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
// 수동 Bean 등록. @Component("memoryMemberRepository") 와 충돌이 발생한다.
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
이 경우네는 에러 메세지가 안나오고 수동 Bean 등록이 Overriding 하게 된다.
이렇게 되면 나중에 서비스에 문제가 발생했을 때 원인을 파악하기 어렵다.
그래서 Spring 프레임워크 내부적으로는 수동 Bean 이 Overriding 하는 정책으로 되어 있지만, 최신 Spring boot 에서 자동 Bean 과 수동 Bean 이 충동하는 예외가 발생하도록 수정하였다.
Spring Boot 설정 파일에서 수동 Bean 으로 Overriding 할 수 있게 설정할 수 있지만, 여러 사람이 함께 코딩을 하기 때문에 어디서 Overriding 될 지 알 수 없으므로 안하는게 좋다.
이전 포스팅 : 스프링과 싱글톤 기초
다음 포스팅 : 의존관계 자동주입