스프링 핵심 원리 (5) - 스프링 빈 조회와 BeanFactory
1. 개요
앞선 글에서 스프링 컨테이너를 만들고 그 안에 빈이 등록되는 과정을 살펴봤다. 이번에는 컨테이너에 등록된 빈을 확인하고 실제로 꺼내 쓰는 다양한 방법을 정리한다. 등록된 빈을 전체 조회하는 방법부터, 이름으로 찾는 가장 단순한 방법, 같은 타입의 빈이 여러 개일 때, 상속 관계에 있는 빈을 조회할 때 각각 어떻게 동작하는지 확인하고, 마지막으로 스프링 컨테이너의 뼈대인 BeanFactory와 ApplicationContext의 관계도 함께 정리한다.
2. 컨테이너에 등록된 모든 빈 조회
스프링 컨테이너에 실제로 어떤 빈들이 등록되어 있는지 확인하고 싶을 때 사용하는 방법이다.
ac.getBeanDefinitionNames(): 스프링에 등록된 모든 빈 이름을 조회한다.ac.getBean(빈이름): 그 이름으로 빈 객체(인스턴스)를 조회한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기")
void findAllBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name=" + beanDefinitionName + " object=" + bean);
}
}
}
이렇게 실행하면 내가 등록한 빈뿐만 아니라 스프링이 내부적으로 사용하는 빈까지 전부 출력된다. 둘을 구분하고 싶다면 BeanDefinition의 getRole()을 확인하면 된다.
ROLE_APPLICATION: 사용자가 직접 등록한 애플리케이션 빈ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
@DisplayName("애플리케이션 빈 출력하기")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
// Role ROLE_APPLICATION: 직접 등록한 애플리케이션 빈
// Role ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("name=" + beanDefinitionName + " object=" + bean);
}
}
}
이렇게 ROLE_APPLICATION만 걸러내면 내가 등록한 memberService, orderService 같은 빈만 깔끔하게 확인할 수 있다.
3. 스프링 빈 조회 - 기본
스프링 컨테이너에서 빈을 찾는 가장 기본적인 방법은 다음 두 가지다.
ac.getBean(빈이름, 타입)ac.getBean(타입)
조회하려는 빈이 컨테이너에 없으면 NoSuchBeanDefinitionException이 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ApplicationContextBasicFindTest {
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 이름으로 조회")
void findBeanByName() {
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("이름 없이 타입만으로 조회")
void findBeanByType() {
MemberService memberService = ac.getBean(MemberService.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("구체 타입으로 조회")
void findBeanByName2() {
MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("빈 이름으로 조회X")
void findBeanByNameX() {
assertThrows(NoSuchBeanDefinitionException.class,
() -> ac.getBean("xxxxx", MemberService.class));
}
}
참고:
MemberServiceImpl처럼 구체 타입으로 조회할 수도 있지만, 구현체를 직접 명시하는 순간 나중에 구현체를 바꾸기 어려워진다. 가급적 인터페이스 타입으로 조회하는 편이 유연하다.
4. 스프링 빈 조회 - 동일한 타입이 둘 이상
타입만으로 조회할 때 같은 타입의 빈이 두 개 이상이면 어떤 빈을 반환해야 할지 컨테이너가 판단할 수 없어서 오류가 난다. 이럴 때는 빈 이름을 함께 지정해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
static class SameBeanConfig {
@Bean
public MemberRepository memberRepository1() {
return new MemoryMemberRepository();
}
@Bean
public MemberRepository memberRepository2() {
return new MemoryMemberRepository();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면 중복 오류가 발생한다")
void findBeanByTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(MemberRepository.class));
}
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면 빈 이름을 지정하면 된다")
void findBeanByName() {
MemberRepository memberRepository =
ac.getBean("memberRepository1", MemberRepository.class);
assertThat(memberRepository).isInstanceOf(MemberRepository.class);
}
특정 타입의 빈을 모두 조회하고 싶다면 ac.getBeansOfType()을 쓰면 된다. 이름을 key로, 빈 객체를 value로 갖는 Map이 반환된다.
1
2
3
4
5
6
@Test
@DisplayName("특정 타입을 모두 조회하기")
void findAllBeanByType() {
Map<String, MemberRepository> beansOfType = ac.getBeansOfType(MemberRepository.class);
assertThat(beansOfType.size()).isEqualTo(2);
}
5. 스프링 빈 조회 - 상속 관계
부모 타입으로 조회하면 그 타입을 상속받은 자식 타입의 빈까지 함께 조회 대상이 된다. 그래서 모든 자바 객체의 최상위 부모인 Object 타입으로 조회하면 스프링에 등록된 모든 빈이 걸린다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
}
DiscountPolicy타입으로 조회 →rateDiscountPolicy,fixDiscountPolicy둘 다 걸려서NoUniqueBeanDefinitionException발생- 빈 이름을 함께 지정하면 그 빈 하나만 정확히 조회된다.
RateDiscountPolicy처럼 더 구체적인 하위 타입으로 조회하면 처음부터 하나만 걸린다.getBeansOfType(DiscountPolicy.class)로 부모 타입 기준 전체 조회도 가능하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
@DisplayName("부모 타입으로 조회시 자식이 둘 이상 있으면 중복 오류가 발생한다")
void findBeanByParentTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("특정 하위 타입으로 조회")
void findBeanBySubType() {
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
참고: 구체적인 하위 타입으로 조회하는 방식은 편리해 보이지만, 결국 특정 구현체에 의존하게 되는 것이므로 좋은 설계에서는 잘 사용하지 않는다.
6. BeanFactory와 ApplicationContext
지금까지 계속 사용해온 ApplicationContext는 사실 BeanFactory를 상속받아 확장한 인터페이스다.
| 구분 | 역할 |
|---|---|
BeanFactory | 스프링 컨테이너의 최상위 인터페이스. 빈을 등록·관리·조회하는 역할을 담당하며 getBean()을 제공한다. |
ApplicationContext | BeanFactory의 기능을 모두 상속받고, 애플리케이션 개발에 필요한 다양한 부가 기능까지 추가로 제공한다. |
ApplicationContext가 제공하는 대표적인 부가 기능은 다음과 같다.
- 메시지소스를 활용한 국제화 기능: 접속 지역에 따라 한국어, 영어 등 다른 언어로 메시지를 출력할 수 있다.
- 환경변수: 로컬, 개발, 운영 환경을 구분해서 처리할 수 있다.
- 애플리케이션 이벤트: 이벤트를 발행하고 구독하는 모델을 편리하게 지원한다.
- 편리한 리소스 조회: 파일, 클래스패스, 외부 리소스 등을 편리하게 조회할 수 있다.
지금까지 써온 대부분의 기능(getBean()으로 빈을 조회하는 것)은 사실 BeanFactory가 제공하는 기능이고, ApplicationContext는 여기에 위와 같은 부가 기능을 얹은 것이다. 실무에서 BeanFactory를 직접 사용할 일은 거의 없고, 부가 기능이 포함된 ApplicationContext를 스프링 컨테이너로 사용하는 것이 일반적이다.
7. 정리
빈을 조회할 때는 이름과 타입을 함께 지정하는 것이 가장 안전하다. 타입만으로 조회하면 같은 타입의 빈이 여러 개이거나 부모-자식 관계의 빈이 여러 개일 때 NoUniqueBeanDefinitionException이 발생할 수 있으므로, 이런 상황에서는 빈 이름을 지정하거나 getBeansOfType()으로 전체를 조회해서 다뤄야 한다.
구조적으로 보면 BeanFactory는 빈 관리라는 핵심 기능만 담당하는 최상위 인터페이스이고, ApplicationContext는 여기에 국제화·환경변수·이벤트·리소스 조회 같은 부가 기능을 더한 확장판이다.