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