Search
Duplicate

스프링 컨테이너와 스프링 빈

목차

스프링 컨테이너 생성

이전챕터 스프링 핵심 원리 이해2 - 객체 지향 원리 적용 에서 ApplicationContext를 스프링 컨테이너라 부른다고 했다. 이번 챕터에서는 이 스프링 컨테이너가 생성되는 과정에 대해 알아본다.
ApplicationContext는 보시다시피 인터페이스이다. 즉, 다형성이 적용되었다고 할 수 있다.
그러기에 스프링 컨테이너는 XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들수도 있다.
이전 챕터에서 다뤄본 방식(AppConfig)은 애노테이션 기반의 자바 설정 클래스로 스프링 컨테이너를 만든것이다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
Java
복사
ApplicationContext 인터페이스의 구현체
심화: 더 정확히는 스프링 컨테이너는 BeanFactory, ApplicationContext로 구분해서 이야기한다. 하지만, BeanFactory를 직접 사용하는 경우는 거의 없기에 일반적으로 ApplicationContext를 스프링 컨테이너라 한다.

스프링 컨테이너 생성 과정

스프링 컨테이너 생성

: new AnnotationConfigApplicationContext() 생성자 호출을 통해 스프링 컨테이너를 생성한다. 여기서 해당 생성자의 인자값으로 구성 정보를 지정해줘야하는데, 여기서는 예제코드로 작성한 구성정보 클래스인 AppConfig.class를 전달해준다.
스프링 컨테이너 생성 및 구성정보 지정

스프링 빈 등록

전달받은 구성정보(AppConfig)를 사용해서 스프링 빈을 등록해준다.
여기서 빈 이름은 구성정보에 있는 메서드 이름을 사용하며, 임의로 어노테이션 속성(name)을 사용해 지정해줄수도 있다. 다만, 여기서 빈 이름은 항상 다른 이름을 부여해야하는데 그 이유는 빈 이름이 중복될 경우 다른 빈이 무시되거나 기존 빈을 덮어버리거나 설정에따라 에러가 발생할 수 있기 때문이다. 이럴 경우 어노테이션 속성으로 빈 이름을 바꿔줄수는 있지만, 가장 좋은건 빈 이름을 별개로 해놓는거다.
AppConfig의 빈 정보를 읽어와 등록해준다.

스프링 빈 의존관계 설정

이제 생성된 스프링빈에 의존관계를 설정해줘야 한다. 기존 AppConfig에는 MemberService와 OrderServicer가 있는데 이 두 클래스를 생성하기위해서는 각각 할인정책, 회원 리포지토리를 의존관계를 주입해줘야 한다. 이런 정보가 AppConfig라는 구성정보 클래스에 담겨있기에 이를 베이스로 스프링 빈 의존관계를 설정해준다.
스프링 빈 의존관계 설정
스프링 컨테이너는 위와같이 설정 정보를 참고해 의존관계를 주입(DI)해 준다.
이처럼 스프링은 빈을 생성하고 의존관계를 주입하는 단계가 있는데 이처럼 자바 코드를 통해 스프링 빈을 등록하면 생성자를 호출하며 의존관계 주입도 처리가 된다.

컨테이너에 등록된 모든 빈 조회

스프링 컨테이너에 실제로 등록된 스프링 빈들이 제대로 등록되어있는지 확인해보자.

1. 테스트 클래스 생성

package hello.core.beanfind; import hello.core.AppConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.AnnotationConfigApplicationContext; 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); } } }
Java
복사
[실행 결과]
실행하면 모든 빈 정보를 출력할 수 있다.
ac.getBeanDefinitionNames() 메서드에서 스프링에 등록된 모든 빈 이름을 조회하고 ac.getBean()에 해당 beanName을 인자로 넘겨 빈 객체(인스턴스)를 조회한다.
실행결과를 보면 내가 만들어준 빈 말고도 많은 빈들이 출력되는걸 볼 수 있다. 이는 스프링의 기능을 위해 생성되는 빈들인데, 섞여서 나오니 가독성이 떨어져서 내가 등록한 빈들만 조회하고 싶다면 아래와 같이 사용을 할 수 있다.
@Test @DisplayName("애플리케이션 빈 출력하기") void findApplicationBean() { String[] beanDefinitionNames = ac.getBeanDefinitionNames(); for (String beanDefinitionName : beanDefinitionNames) { BeanDefinition beanDefinition = ac.getBeanDefinition(beanDefinitionName); if (beanDefinition.getRole() == BeanDefinition.ROLE_APPLICATION) { Object bean = ac.getBean(beanDefinitionName); System.out.println("beanDefinitionName = " + beanDefinitionName + " bean = " + bean); } } }
Java
복사
[실행 결과]
이제 내가 만들어준 빈들만 출력되는것을 볼 수 있다.
코드중 getRole()을 통해 빈을 구분할 수 있는데, 여기서는 내가 사용하기위해 만든 빈들(혹은 외부라이브러리에서 등록한 빈들)을 구분하기 위해 BeanDefinitio.ROLE_APPLICATION을 사용해준다.

스프링 빈 조회 - 기본

이번시간에는 스프링 컨테이너에서 스프링 빈을 찾는 방법을 학습해본다.
위에서 테스트 케이스를 작성하면서 이미 빈 조회 메서드를 사용해봤다.
getBean(빈이름, 타입)
getBean(타입)
여기서 만약 조회 대상 스프링 빈이 존재하지 않는다면 NoSuchBeanDefinitionException이 발생한다.
package hello.core.beanfind; import hello.core.AppConfig; import hello.core.member.MemberService; import hello.core.member.MemberServiceImpl; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; 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); } }
Java
복사
참고 : MemberService 빈을 조회할 때 인터페이스가 아닌 구현체를 조회할때도 정상적으로 조회되지만 구현체로 조회하면 변경시에 유연성이 떨어지기 때문에 추천하지 않는다.

스프링 빈 조회 - 동일한 타입이 둘 이상

getBean을 통해 타입으로 빈을 조회할 수 있다고 하였는데, 여기서 해당 타입의 빈이 하나가 아니라 둘 이상이라면 어떻게 될까? 한번 실행해보자.
package hello.core.beanfind; import hello.core.discount.DiscountPolicy; import hello.core.discount.FixDiscountPolicy; import hello.core.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 static org.assertj.core.api.Assertions.assertThatThrownBy; 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(); } } }
Java
복사
기존 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); }
Java
복사

스프링 빈 조회 - 상속관계

지금까지 getBean 메서드를 통해 이름과타입, 혹은 타입으로 빈을 조회했는데, 의문점이 생긴다.
할인정책 타입 DiscountPolicy이라는 인터페이스로 빈을 조회해도 구현체들이 모두 조회된다. 이는 부모 타입으로 조회할 경우 자식 타입도 함께 조회되기 때문인데, 그렇기의 자바의 최고 조상 객체인 Object 타입으로 조회하면, 모든 스프링 빈을 조회하게 된다. 위 예시에서 Object로 빈을 조회하면 ParentBeanA, ParentBeanB , ... ChildBeanBB까지 전부 조회되게 된다.
좀 더 이해하기 쉽게 탈것이라는 빈이 있다고 가정하자. 여기서 내가 getBean(항공기)를 한다면 조회되는 빈은 무엇일까?
정답은 항공기, 보잉, 에어버스이다. 이처럼 자기자신과 자기자신을 상속하는 하위 빈들까지 조회한다.
탈것의 상속 관계
이번에는 코드를 통해 학습해보자.
package hello.core.beanfind; import hello.core.discount.DiscountPolicy; import hello.core.discount.FixDiscountPolicy; import hello.core.discount.RateDiscountPolicy; import org.assertj.core.api.Assertions; 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.*; 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(); } } }
Java
복사
이전 챕터의 테스트코드와 중복되는 코드가 있지만, 테스트의 의의는 다르기에 작성한다.
Object타입으로 조회하는 테스트는 그저 내용 확인용도이기에 컴퓨터에서 검증하는 코드를 따로 작성하진 않았다.

BeanFactory와 ApplicationContext

이전에 스프링 컨테이너는 엄밀히 말해 BeanFactory와 ApplicationContext로 구분한다고 언급했다.(1)
우측 그림을 하나씩 살펴보면, 최상단에 BeanFactory라는 인터페이스가 있다. 이는 스프링 컨테이너의 최상위 인터페이스로 스프링 빈을 조회및 관리하는 역할을 한다. 우리가 지금까지 사용한 getBean 메서드들이 모두 BeanFactory가 제공하는 기능이다.
그렇다면 ApplicationContext인터페이스는 무엇인가? 간단히 말하면 BeanFactory의 기능에 추가적인 기능을 덧붙혀 제공하는 인터페이스인데,
실제 해당 인터페이스를 보면 EnvironmentCapable, ListableBeanFactory, MessageSource 등 많은 인터페이스들을 다중상속받고 있다. (2)
즉, ApplicationContext는 BeanFactory의 기능뿐 아니라 그밖의 다양한 부가기능을 같이 제공한다고 볼 수 있다.

ApplicationContext가 제공하는 부가기능

MessageSource
: 메세지 소스를 활용한 국제화기능 즉, 국가별로 그 나라에 맞는 언어로 나오도록 하는 기능
EnvironmentCapable
: 로컬, 개발, 운영등을 구분해서 처리할 수 있게 하는 기능
ApplicationEventPublisher
: 이벤트를 발행하고 구독하는 모델을 편리하게 지원하는 기능
ResourceLoader
:파일 클래스패스, 외부 등에서 리소스를 편리하게 조회하게 지원하는 기능

정리

ApplicationContext 는 BeanFactory의 기능을 상속받는다.
ApplicationContext는 빈 관리기능 + 기타 기능(MessageSource, EnvironmentCapable등)을 제공한다.
BeanFactory를 직접 사용하기보다는 다 합쳐진 ApplicationContext를 사용한다.
BeanFactory나 Application Context를 스프링 컨테이너라 한다.

다양한 설정 형식 지원 - 자바 코드, XML

지금까지 스프링 컨테이너를 설정한 방식은 자바 코드를 작성해서 설정을 해줬다.
하지만, 스프링 부트가 아닌 이전 스프링을 사용했거나 했던 사람들은 알 수 있듯이 스프링 컨테이너는 자바뿐아니라 XML이나 Groovy등 다양한 방식으로 스프링 컨테이너를 생성할 수 있다.
지금까지는 자바 코드를 이용해 설정정보를 등록해줬다면, 이번시간에는 간단하게나마 XML을 사용해 설정정보를 등록해보자.
테스트코드 XmlAppContext
package hello.core.xml; import hello.core.member.MemberService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.GenericXmlApplicationContext; import static org.assertj.core.api.Assertions.assertThat; @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); } }
Java
복사
xml 설정파일
<?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>
Java
복사
위 xml 설정파일 역시 기존 자바로 작성한 appConfig와 거의 유사하다.
더하여 최근 거의 사용하지 않는 방식이기에 더 자세히 들어가지는 않는다.

스프링 빈 설정 메타 정보 - BeanDefinition

스프링 컨테이너를 생성할 때 자바, XML, Groovy등 다양한 방식의 설정 형식을 모두 지원하는데, 이게 어떻게 가능한 것일까? 여기서 기억해야할 부분은 BeanDefinition이라는 인터페이스이다.
우리가 XML, 자바코드 등으로 설정 정보를 전달하면 해당 코드를 읽어와서 BeanDefinition을 만든다.
그렇기에 스프링 컨테이너는 이게 자바코드인지, XML인지 알 필요없이 전달된 BeanDefinition만 알면 된다. 즉, DIP(의존성 역전 원칙)원칙을 지킴으로써 얻는 이점이라 할 수 있다. 그럼 이 BeanDefinition은 무엇일까?

BeanDefinition

빈 메타 설정정보로 자바에서는 @Bean, XML에서는 <bean>이 부여된 정보별로 메타 정보를 생성한다.
그리고 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.
다양한 방식의 설정정보를 BeanDefinition 인터페이스화하면 스프링 컨테이너로 만들 수 있다.
그럼 이렇게 다른 각각의 설정정보를 어떻게 BeanDefinition으로 만드는 것일까? 다음 그림을 보자.
각각의 설정정보 클래스에 맞는 BeanDefinitionReader를 사용해 해당 설정정보를 읽어서 빈 메타정보인 BeanDefinition을 생성해 전달한다. 만약 여기에 없는(ex: JSON, TXT등) 형식의 설정 정보를 추가하고 싶다면 해당 형식을 읽을 수 있는 XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 된다.
이번에는 코드로 BeanDefinition을 실제로 확인해보자.
package hello.core.beandefinition; import hello.core.AppConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.AnnotationConfigApplicationContext; 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); } } } }
Java
복사
[실행 결과]
위 코드를 실행하면 결과로 나오는 부분이 AppConfig.class에 작성된 빈들의 BeanDefinition인데, 각각 필드에 대한 간단한 설명을 하면 다음과 같다.
BeanClassName: 생성할 빈의 클래스명
factoryBeanName: 팩토리 역할의 빈을 선택할 경우 이름(ex: appConfig)
factoryMethodName: 빈을 생성할 팩토리 메서드 지정(ex: memberService)
Scope: 싱글톤(기본값)
lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아닌 빈을 실제 사용할 때 생성하는 지연 로딩
InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전 호출되는 메서드 명
Constructor argument, Properties: 의존관계 주입에서 사용한다.(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)
물론, 실무를 하면서 BeanDefinition을 직접 정의해서 사용하는 경우는 흔치않다. 다만, 코드 트레이싱을 할 때 이런 키워드를 만난다면 이런 역할이라는 점을 기억하면 된다.

이전 챕터로

다음 챕터로