[스프링 핵심 원리] 컴포넌트 스캔
자동으로 빈을 등록하는 컴포넌트 스캔 기능
※ 코드는 강의에서 사용된 것과 다릅니다.
<a href = https://github.com/jaewon-ju/Learning_Spring>jaewon-ju Github Address</a>
새로운 문제 스프링 컨테이너에 빈을 등록하기 위해서는 @Bean 어노테이션을 사용했다. 하지만, 프로젝트가 커지면 AppConfig 클래스에 일일이 빈 코드를 작성하고 @Bean 어노테이션을 반복해서 작성해야한다.
해결 방법 스프링은 설정 정보가 비어있어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔 기능을 제공한다.
✏️ 컴포넌트 스캔
컴포넌트 스캔은 @Component 어노테이션이 붙은 클래스를 빈으로 자동 등록하는 기능이다.
- 빈의 이름은 클래스의 제일 앞 대문자를 소문자로 바꿔서 저장한다. ex) MemberRepository 클래스 -> memberRepository 빈
수동
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class AppConfig {
@Bean
public MemberRepository memberRepository(){
return new MemoryMemberRepository();
}
@Bean
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
@Bean
public BoardPolicy boardPolicy(){
return new ReadOnlyPolicy();
}
@Bean
public BoardService boardService(){
return new BoardServiceImpl(memberRepository(), boardPolicy());
}
}
- 설정 정보 클래스를 비움
- 객체(빈)로 등록할 클래스에 @Component 어노테이션을 붙임
1 2 3 4
@Configuration public class AutoAppConfig { // 비어있다 }
1
2
@Component
public class MemoryMemberRepository implements MemberRepository{ ... }
1
2
@Component
public class MemberServiceImpl implements MemberService { ... }
1
2
@Component
public class BoardServiceImpl implements BoardService{ ... }
1
2
@Component
public class ReadOnlyPolicy implements BoardPolicy { ... }
⚠️ 다수의 @Configuration
하나의 프로그램에 여러개 @Configuration 어노테이션이 존재하는 경우
@Configuration 어노테이션은 @Component 어노테이션을 포함하고 있다. ➜ @Configuration이 붙은 클래스도 빈으로 등록된다. ➜ @Configuration 어노테이션이 여러개 존재하면 충돌이 발생할 수 있다. ∴ 필터를 사용해서 스캔 대상을 선택한다.
1
2
3
4
5
@Configuration
@ComponentScan(
excludeFilters = @Filter(type = FilterType.ANNOTATION,
classes = Configuration.class))
public class AutoAppConfig {}
⚠️ @Autowired
@Component 어노테이션이 붙은 클래스는 빈으로 자동 등록된다. 하지만, 스프링의 핵심 기술인 의존관계 주입은 추가로 설정 해줘야한다.
다음의 컴포넌트를 한번 보자.
1
2
3
4
5
6
7
8
9
10
@Component
public class BoardServiceImpl implements BoardService{
private final MemberRepository memberRepository;
private final BoardPolicy boardPolicy;
public BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
}
(MemberRepository memberRepository, BoardPolicy boardPolicy)
해당 파라미터를 전달해줘야 스프링 컨테이너의 핵심 원리인 의존관계 주입 기능이 작동할 것이다.
이 때 생성자에 @Autowired 어노테이션을 붙여주면 의존관계 주입이 자동으로 처리된다.
@Autowired 어노테이션은 컨테이너에서 타입이 같은 빈을 찾아서 매개 변수에 전달한다.
ac.getBean(타입)과 같은 메커니즘으로 동작한다.
1
2
3
4
5
@Autowired
public BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
}
✏️ 탐색 위치와 스캔 대상
■ 탐색 위치 지정
basePackages 옵션을 사용하여 탐색 시작 위치를 지정할 수 있다.
1
2
3
4
@ComponentScan(
basePackages = "boardProject.demo"
// boardProject.demo 패키지에서 탐색 시작
}
탐색 위치를 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지부터 탐색이 시작된다.
탐색 위치를 지정하는 것보단, 설정 정보 클래스를 패키지 최상단에 두는 방법을 사용하자.
■ 컴포넌트 스캔 대상
컴포넌트 스캔은 @Component뿐만 아니라 다음 어노테이션도 스캔 대상으로 본다.
| Annotation | 기능 |
|---|---|
| @Configuration | 설정 정보로 인식 |
| @Controller | MVC Controller로 인식 |
| @Repository | 데이터 접근 계층으로 인식, 데이터 계층의 예외를 추상화 시켜줌 |
| @Service | 기능은 없음. @Service가 붙은 빈은 핵심 비즈니스 로직을 갖고 있는 컴포넌트임을 알리는데 의의를 둠 |
✏️ 중복 등록
등록하려는 빈의 이름이 같은 경우, 충돌이 발생한다.
■ 수동 빈 VS 자동 빈
수동으로 등록된 빈과 자동으로 등록된 빈이 충돌하면 수동 빈이 우선권을 가진다.
1
2
3
4
5
6
7
8
// 수동 빈
@Configuration
public class AutoAppConfig {
@Bean(name = "memoryMemberRepository")
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
}
1
2
3
// 자동 빈
@Component
public class MemoryMemberRepository implements MemberRepository { ... }
위의 예시처럼 이름이 같은 수동 빈과 자동 빈이 충돌하면, 수동 빈이 자동 빈을 오버라이딩 한다.
하지만, 최근 스프링 부트에서는 수동 빈과 자동 빈이 충돌하면 오류를 발생하도록 기본 값이 바뀌었다.
■ 자동 빈 VS 자동 빈
등록하려는 빈의 이름이 같은 경우, ConflictingBeanDefinitionException이 발생한다.
✏️ 중복 주입
주입하려는 빈의 타입이 같은 경우, 충돌이 발생한다. BoardServiceImpl 클래스를 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Component
public class BoardServiceImpl implements BoardService{
private final MemberRepository memberRepository;
private final BoardPolicy boardPolicy;
@Autowired
public BoardServiceImpl(MemberRepository memberRepository, BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
// MemoryMemberRepository 클래스만 @Component가 붙어있음
this.boardPolicy = boardPolicy;
// ReadOnlyPolicy, ReadWritePolicy 클래스 모두 @Component가 붙어있음
}
@Autowired는 생성자를 호출할 때 각 타입에 맞는 빈을 찾아서 의존관계를 주입한다.
ac.getBean(BoardPolicy.class)로 매개변수 타입에 맞는 빈을 찾을 때 문제가 발생한다.
현재, ReadOnlyPolicy 클래스와 ReadWritePolicy 클래스가 모두 Component로 등록되어 있다.
두 클래스 모두 BoardPolicy 인터페이스의 구현체이므로, ac.getBean(BoardPolicy.class)에 잡힌다.
➜ NoUniqueBeanDefinitionException 발생
해결 방법
@Primary사용@Autowired필드/파라미터 명 매칭@Qualifier사용
1. @Primary
@Autowired가 작동할 때 여러빈이 매칭되면 @Primary가 붙은 빈이 우선권을 가진다.
1
2
3
@Component
@Primary
public class ReadOnlyPolicy implements BoardPolicy { ... }
ReadOnlyPolicy 클래스가 ReadWritePolicy 보다 우선권을 가진다.
2. @Autowired 필드/파라미터 명 매칭
@Autowired는 생성자를 호출할 때 각 타입에 맞는 빈을 찾아서 의존관계를 주입한다.
이때, 중복되는 빈이 발견되면 파라미터 명을 추가적으로 비교한다.
다음 강의에서 배울 @Autowired 필드에도 적용된다.
3. @Qualifier
@Qualifier는 추가 구분자를 붙여주는 방법이다.
⚠️ 이름을 변경하는 것은 아니다!
1
2
3
@Component
@Qualifier("mainPolicy")
public class ReadOnlyPolicy implements BoardPolicy { ... }
1
2
3
@Component
@Qualifier("subPolicy")
public class ReadWritePolicy implements BoardPolicy { ... }
생성자 파라미터에도 @Qualifier를 붙여줘야 한다.
1
2
3
4
5
6
@Autowired
public BoardServiceImpl(MemberRepository memberRepository,
@Qualifier("mainPolicy") BoardPolicy boardPolicy) {
this.memberRepository = memberRepository;
this.boardPolicy = boardPolicy;
}
@Primary와 @Qualifier 중 뭐가 더 나을까?
95%의 상황에서 ReadOnlyPolicy 클래스를 사용하고 나머지 상황에서 ReadWritePolicy를 사용한다고 가정해보자. 이 경우에는 ReadOnlyPolicy에 @Primary를 붙이고, ReadWritePolicy에 @Qualifier를 붙여서 명시적으로 호출하는 것이 코드를 깔끔하게 유지할 수 있다.
실제로 중복된 두 빈중에 선택하고 싶을 때는 어떻게 할까?
클라이언트가 주입할 빈을 선택하도록 만들고 싶다면, 중복된 빈을 모두 가져와서 선택된 빈을 주입해야 한다. 이 경우에는 Map을 사용해서 해당 타입의 빈을 모두 주입받을 수 있다. 다음 포스팅에서 자세히 설명하겠다.