4-5. 스프링 빈 상속 관계
스프링 빈을 조회할 때 중요한 점이 있다.
바로, 타입으로 스프링 빈을 조회할 경우, 해당 타입의 자식 타입들까지 모두 함께 조회된다는 점이다.
이를테면, 최상위 자바 클래스 타입인 Object 타입으로 조회하면 모든 스프링 빈이 조회된다.
아래 그림은 주어진 클래스 계층 구조에서, 각 타입으로 빈을 조회하면 함께 조회되는 빈 타입들을 나타낸다.
1번 타입을 조회하면 자신을 포함한 모든 하위 타입인 1~7 타입이 조회되고,
2, 3번 타입을 조회하면 각각 (2, 4, 5), (3, 6, 7) 타입이 조회되는 식이다.
예제 코드와 함께 좀 더 자세히 알아보자.
설정 클래스
이번 예제 코드에서도 원활한 테스트를 위해 임의의 설정 클래스를 선언하여 사용한다.
테스트 클래스 내부에 static으로 선언한다.
TestConfig.java
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixsDiscountPolicy() {
return new FixDiscountPolicy();
}
}
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
중복 타입 오류
부모 타입으로 조회 시 자식 타입이 함께 조회되므로, 자식 타입이 둘 이상이면 예외가 발생한다.
같은 타입의 빈이 여러개 있는 것과 같은 현상이다.
findBeanByParentTypeDuplicate()
@Test
@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면, 중복 예외가 발생한다.")
void findBeanByParentTypeDuplicate() {
// ac.getBean(DiscountPolicy.class);
assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(DiscountPolicy.class));
}
+) assertThrows()는 jupyter의 Assertions를 static import했다. 즉 원형은 Assertions.assertThrows()
빈 이름 지정, 부모 타입 모두 조회
자식 타입의 빈이 여러개 있을 경우, 빈 이름을 지정해 조회하거나 getBeansByType()을 이용한다.
findBeanByParentTypeByBeanName()
@Test
@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면, 빈 이름을 지정한다.")
void findBeanByParentTypeByBeanName() {
DiscountPolicy bean = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
findAllBeanByParentType()
@Test
@DisplayName("부모 타입으로 모두 조회하기")
void findAllBeanByParentType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
assertThat(beansOfType.size()).isEqualTo(2);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + ", value = " + beansOfType.get(key));
}
}
Object 타입으로 조회
모든 자바 클래스는 Object의 자식 타입이므로, Object 타입으로 빈을 조회하면 모든 빈이 조회된다.
findAllBeanByObjectType()
@Test
@DisplayName("부모 타입으로 모두 조회하기 - Object")
void findAllBeansByObjectType() {
// 모든 자바 객체는 Object의 자식 타입이므로 컨테이너에 등록된 모든 빈이 조회된다.
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + ", value = " + beansOfType.get(key));
}
}
테스트를 실행해보면 다음과 같이 모든 빈이 조회된다.
전체 테스트클래스 코드 (ApplicaionContextExtendsFindTest.java)
package hdxian.hdxianspringcore.beanfind;
import hdxian.hdxianspringcore.AppConfig;
import hdxian.hdxianspringcore.discount.DiscountPolicy;
import hdxian.hdxianspringcore.discount.FixDiscountPolicy;
import hdxian.hdxianspringcore.discount.RateDiscountPolicy;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
public class ApplicationContextExtendsFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
@Test
@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면, 중복 예외가 발생한다.")
void findBeanByParentTypeDuplicate() {
// ac.getBean(DiscountPolicy.class);
assertThrows(NoUniqueBeanDefinitionException.class,
() -> ac.getBean(DiscountPolicy.class));
}
@Test
@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면, 빈 이름을 지정한다.")
void findBeanByParentTypeByBeanName() {
DiscountPolicy bean = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("특정 하위 타입으로 조회")
void findBeanBySubType() {
RateDiscountPolicy bean = ac.getBean(RateDiscountPolicy.class);
assertThat(bean).isInstanceOf(RateDiscountPolicy.class);
}
@Test
@DisplayName("부모 타입으로 모두 조회하기")
void findAllBeanByParentType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
assertThat(beansOfType.size()).isEqualTo(2);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + ", value = " + beansOfType.get(key));
}
}
@Test
@DisplayName("부모 타입으로 모두 조회하기 - Object")
void findAllBeansByObjectType() {
// 모든 자바 객체는 Object의 자식 타입이므로 컨테이너에 등록된 모든 빈이 조회된다.
Map<String, Object> beansOfType = ac.getBeansOfType(Object.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + ", value = " + beansOfType.get(key));
}
}
@Configuration
static class TestConfig {
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
@Bean
public DiscountPolicy fixsDiscountPolicy() {
return new FixDiscountPolicy();
}
}
}
정리
타입이 중복될 경우를 신경써야 된다면, 차라리 모든 빈을 각각 다른 구현체 타입으로 지정하면 안될까 싶기도 하다.
하지만, 빈을 그렇게 등록하면 역할과 구현이 분리되지 않기 때문에 프로그램 유지보수가 어려워지고, 변경에도 유연하게 대응할 수 없게 된다.
스프링 빈에 의존하는 다른 클래스들도 추상화에 의존하고 있으므로, 스프링 빈을 구현체 타입으로 등록하는 것은 좋은 선택지가 아니다.
그리고, 사실 개발하는 과정에서 getBean() 등으로 스프링 빈을 조회할 일이 거의 없다.
이전까지 작성한 클라이언트 프로그램 코드만 봐도, 스프링 빈을 직접 갖다 쓸 일이 없다.
하지만 그럼에도 스프링 빈 조회에 대해서 알아야 하는 이유는, 이후에 의존관계 주입(DI)에서 이 개념이 요긴하게 쓰이기 때문이다.
따라서, 스프링 빈을 직접 조회할 일이 없더라도 스프링 빈 타입과 상속관계, 그리고 이를 이용한 조회 방법 등에 대해 숙지하고 있어야 한다.