ApplicationContext
를 스프링 컨테이너라 부른다고 했다.
이번 포스팅에서는 스프링 컨테이너가 생성되는 과정에 대해 알아보자.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class)
✅ 더 정확히는 스프링 컨테이너는 BeanFactory
, ApplicationContext
로 구분해서 이야기한다. 하지만, BeanFactory
를 직접 사용하는 경우는 거의 없기에 일반적으로 ApplicationContext
를 스프링 컨테이너라 한다.
new AnnotationConfigApplicationContext() 생성자 호출을 통해 스프링 컨테이너를 생성한다. 여기서 해당 생성자의 인자 값으로 구성 정보를 지정해 줘야 하는데, 여기서는 예제 코드로 작성한 구성 정보 클래스인 AppConfig.class를 전달해 준다.
스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록한다.
여기서 빈 이름
은 구성 정보에 있는 메서드 이름을 사용하며, 임의로 애노테이션 속성(name)을 사용해 지정해 줄 수도 있다. 다만, 여기서 빈 이름은 항상 다른 이름을 부여해야 하는데 그 이유는 빈 이름이 중복될 경우 다른 빈이 무시되거나 기존 빈을 덮어버리거나 설정에 따라 에러가 발생할 수 있기 때문이다. 이럴 경우 애노테이션 속성으로 빈 이름을 바꿔줄 수는 있지만, 가장 좋은 건 빈 이름을 별개로 해놓는 거다.
AppConfig의 빈 정보를 읽어와 등록해 준다.
이제 생성된 스프링 빈에 의존관계를 설정해 줘야 한다. 기존 AppConfig에는 MemberService와 OrderServicer가 있는데 이 두 클래스를 생성하기 위해서는 각각 할인정책, 회원 리포지토리를 의존관계를 주입해 줘야 한다. 이런 정보가 AppConfig라는 구성 정보 클래스에 담겨있기에 이를 베이스로 스프링 빈 의존관계를 설정해 준다.
스프링 빈 의존관계 설정
스프링 컨테이너는 위와 같이 설정 정보를 참고해 의존관계를 주입(DI) 해 준다.
이처럼 스프링은 빈을 생성하고 의존관계를 주입하는 단계가 있는데 이처럼 자바 코드를 통해 스프링 빈을 등록하면 생성자를 호출하며 의존관계 주입도 처리가 된다.
스프링 컨테이너에 실제로 등록된 스프링 빈들이 제대로 등록되어 있는지 확인해 보자.
public class ApplicationContextInfoTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("모든 빈 출력하기")
void findAllBeans() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
Object bean = ac.getBean(beanDefinitionName);
System.out.println("beanDefinitionName = " + beanDefinitionName + " bean = " + bean);
}
}
}
실행하면 모든 빈 정보를 출력할 수 있다.
ac.getBeanDefinitionNames()
메서드에서 스프링에 등록된 모든 빈 이름을 조회하고 ac.getBean()
에 해당 beanName을 인자로 넘겨 빈 객체(인스턴스)를 조회한다.
실행 결과를 보면 직접 만들어준 빈 말고도 많은 빈들이 출력되는 걸 볼 수 있다. 이는 스프링의 기능을 위해 생성되는 빈이다. 직접 등록한 빈들만 조회하고 싶다면 아래와 같이 사용을 할 수 있다.
@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("beanDefinitionName = " + beanDefinitionName + " bean = " + bean);
}
}
}
이제 내가 만들어준 빈들만 출력되는 것을 볼 수 있다.
코드 중 getRole()을 통해 빈을 구분할 수 있는데, 여기서는 직접 만든 빈(혹은 외부 라이브러리에서 등록한 빈)을 구분하기 위해 BeanDefinitio.ROLE_APPLICATION
을 사용해 준다.
스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 조회 방법
getBean(빈이름, 타입)
getBean(타입)
만약 조회 대상 스프링 빈이 존재하지 않는다면 NoSuchBeanDefinitionException
이 발생한다.
public class ApplicationContextBasicFindFirst {
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 findBeanByImplements() {
MemberService memberService = ac.getBean("memberService", MemberServiceImpl.class);
assertThat(memberService).isInstanceOf(MemberServiceImpl.class);
}
@Test
@DisplayName("등록되지 않은 빈 조회")
void findBeanByWithoutBean() {
assertThatThrownBy(()->{
ac.getBean("xxxx", MemberService.class);
}).isInstanceOf(NoSuchBeanDefinitionException.class);
}
}
✅ 구체 타입으로 조회하면 변경 시 유연성이 떨어진다.
getBean을 통해 타입으로 빈을 조회할 수 있다고 하였는데, 여기서 해당 타입의 빈이 하나가 아니라 둘 이상이라면 어떻게 될까? 한번 실행해 보자.
public class ApplicationContextSameBeanFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SameBeanConfig.class);
@Test
@DisplayName("동일한 타입이 둘 이상인 스프링 빈을 타입으로 조회할 경우 에러가 발생한다.")
void findBeanByTypeDuplicate() {
assertThatThrownBy(()->{
DiscountPolicy bean = ac.getBean(DiscountPolicy.class);
}).isInstanceOf(NoUniqueBeanDefinitionException.class);
}
@Configuration
static class SameBeanConfig {
@Bean
public DiscountPolicy fixDiscountPolicy() {
return new FixDiscountPolicy();
}
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
}
}
기존 AppConfig에는 중복 타입의 빈 등록이 없다. 하지만 테스트를 위해 프로덕션 코드를 수정하는 건 지양해야 하기에 테스트 클래스 내에 테스트용 구성 정보 객체인 SameBeanConfig 클래스를 만들어준다.
실행 결과는 NoUniqueBeanDefinitionException이 발생한다.
그럼 동일한 타입의 빈이 여러 개 등록되어 있다면, 혹은 에러 없이 이 빈들을 다 꺼내고 싶다면 어떻게 해야 할까? 지정해서 꺼내는 건 이름을 지정해서 조회하는 방법과, 모두 조회하는 방법 두 가지를 모두 사용해 보자.
@Test
@DisplayName("동일한 타입이 둘 이상인 스프링 빈을 타입으로 조회할 빈 이름을 지정한다.")
void findBeanByName() {
DiscountPolicy discountPolicy = ac.getBean("fixDiscountPolicy", DiscountPolicy.class);
assertThat(discountPolicy).isInstanceOf(FixDiscountPolicy.class);
}
@Test
@DisplayName("특정 타입을 모두 조회한다.")
void findAllBeanType() {
Map<String, DiscountPolicy> beansOfType = ac.getBeansOfType(DiscountPolicy.class);
for (String key : beansOfType.keySet()) {
System.out.println("key = " + key + "value = "+ beansOfType.get(key));
}
System.out.println("beansOfType = " + beansOfType);
assertThat(beansOfType).hasSize(2);
}
할인 정책 타입 DiscountPolicy
이라는 인터페이스로 빈을 조회해도 구현체들이 모두 조회된다. 이는 부모 타입으로 조회할 경우 자식 타입도 함께 조회되기 때문인데, 그렇기의 자바의 최고 조상 객체인 Object
타입으로 조회하면, 모든 스프링 빈을 조회하게 된다.
public class ApplicationContextExtendsFindTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ApplicationContextSameBeanFindTest.SameBeanConfig.class);
@Test
@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 중복 오류가 발생한다. ")
void findBeanByParentTypeDuplicate() {
assertThatThrownBy(()->{
ac.getBean(DiscountPolicy.class);
}).isInstanceOf(NoUniqueBeanDefinitionException.class);
}
@Test
@DisplayName("부모 타입으로 조회 시 자식이 둘 이상 있으면 빈 이름을 지정하면 정상 동작한다.")
void findBeanByParentTypeBeanName() {
DiscountPolicy rateDiscountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy.class);
assertThat(rateDiscountPolicy).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).hasSize(2);
}
@Test
@DisplayName("부모 타입으로 모두 조회하기 - Object")
void findAllBeanByObjectType() {
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 fixDiscountPolicy() {
return new FixDiscountPolicy();
}
@Bean
public DiscountPolicy rateDiscountPolicy() {
return new RateDiscountPolicy();
}
}
}
아래 그림을 살펴보면, 최상단에 BeanFactory라는 인터페이스가 있다. 이는 스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 조회 및 관리하는 역할을 한다. 우리가 지금까지 사용한 getBean 메서드들이 모두 BeanFactory가 제공하는 기능이다.
ApplicationContext 인터페이스는 간단히 말하면 BeanFactory의 기능에 추가적인 기능을 덧붙여 제공하는 인터페이스인데, 실제 해당 인터페이스를 보면 EnvironmentCapable, ListableBeanFactory, MessageSource 등 많은 인터페이스들을 다중 상속받고 있다.
즉, ApplicationContext는 BeanFactory의 기능뿐 아니라 그 밖의 다양한 부가기능을 같이 제공한다고 볼 수 있다.
MessageSource, 메시지 소스를 활용한 국제화 기능
EnvironmentCapable, 환경 변수
ApplicationEventPublisher, 애플리케이션 이벤트
ResourceLoader, 편리한 리소스 조회
지금까지는 자바 코드를 이용해 설정 정보를 등록해 줬다면, 이번에는 XML을 사용해 설정 정보를 등록해 보자.
@DisplayName("XML을 이용한 스프링 컨테이너 생성을 시도한다.")
public class XmlAppContext {
@Test
@DisplayName("XML 파일을 설정 정보로 넘겨준 뒤 빈을 조회한다.")
void xmlAppContext() {
ApplicationContext ac = new GenericXmlApplicationContext("appConfig.xml");
MemberService memberService = ac.getBean("memberService", MemberService.class);
assertThat(memberService).isInstanceOf(MemberService.class);
}
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="memberService" class="hello.core.member.MemberServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository" />
</bean>
<bean id="memberRepository" class="hello.core.member.MemoryMemberRepository"/>
<bean id="orderService" class="hello.core.order.OrderServiceImpl">
<constructor-arg name="memberRepository" ref="memberRepository"/>
<constructor-arg name="discountPolicy" ref="discountPolicy"/>
</bean>
<bean id="discountPolicy" class="hello.core.discount.RateDiscountPolicy"/>
</beans>
위 xml 설정 파일 역시 기존 자바로 작성한 appConfig와 거의 유사하다.
하지만 최근 거의 사용하지 않는 방식이기에 더 자세히 들어가지는 않는다.
스프링 컨테이너를 생성할 때 자바
, XML
, Groovy
등 다양한 방식의 설정 형식을 모두 지원하는데, 그 중심에는 BeanDefinition
이라는 추상화가 있다.
우리가 XML, 자바 코드 등으로 설정 정보를 전달하면 해당 코드를 읽어와서 BeanDefinition을 만든다.
그렇기에 스프링 컨테이너는 이게 자바 코드 인지, XML 인지 알 필요 없이 전달된 BeanDefinition만 알면 된다. 즉, DIP(의존성 역전 원칙) 원칙을 지킴으로써 얻는 이점이라 할 수 있다. 그럼 이 BeanDefinition은 무엇일까?
빈 설정 메타정보
로 자바에서는 @Bean
, XML에서는 <bean>
이 부여된 정보별로 메타 정보를 생성한다.
그리고 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
조금 더 깊이 있게 들어가 보자.
각각의 설정 정보 클래스에 맞는 BeanDefinitionReader
를 사용해 해당 설정 정보를 읽어서 빈 메타정보인 BeanDefinition
을 생성해 전달한다. 만약 여기에 없는(ex: JSON 등) 형식의 설정 정보를 추가하고 싶다면 해당 형식을 읽을 수 있는 XxxBeanDefinitionReader
를 만들어서 BeanDefinition
을 생성하면 된다.
이번에는 코드로 BeanDefinition
을 실제로 확인해 보자
public class BeanDefinitionTest {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
@Test
@DisplayName("빈 설정 메타정보 확인")
void findApplicationBean() {
String[] beanDefinitionNames = ac.getBeanDefinitionNames();
for (String beanDefinitionName : beanDefinitionNames) {
BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName);
if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) {
System.out.println("beanDefinitionName = " + beanDefinitionName +
" beanDefinition = " + beanDefinition);
}
}
}
}
위 코드를 실행하면 결과로 나오는 부분이 AppConfig.class에 작성된 빈들의 BeanDefinition인데, 각각 필드에 대한 간단한 설명을 하면 다음과 같다.
물론, 실무를 하면서 BeanDefinition을 직접 정의해서 사용하는 경우는 흔치않다.
BeanDefinition에 대해서는 너무 깊이 있게 이해하기보다는, 스프링이 다양한 형태의 설정 정보를
BeanDefinition으로 추상화해서 사용하는 것 정도만 이해하면 된다.