[Spring Core #2] 스프링 컨테이너와 스프링 빈 (스프링 핵심 원리 강의정리)
Index
📌 스프링 컨테이너 생성
- 스프링 컨테이너 생성 과정
📌 컨테이너에 등록된 모든 빈 조회
📌 스프링 빈 조회
- 기본
- 스프링 빈 조회 (동일한 타입 둘 이상인 경우)
- 스프링 빈 조회 (상속 관계)
📌 BeanFactory와 ApplicationContext
- BeanFactory
- ApplicationContext
📌 다양한 설정 형식 지원 - XML, 설정 클래스 파일
📌 스프링 빈 설정 메타 정보 - BeanDefinition
📌 정리
해당 내용은 강의 내용을 기억하기 위한 정리글입니다. 자세한 내용은 강의에서 확인하실 수 있습니다.
(저는 코틀린 베이스로 강의를 진행하였고 게시글의 코드 예시는 대부분 코틀린으로 이루어져 있습니다.)
스프링 핵심 원리 - 기본편(김영한님) #광고아님, #내돈내산, #적극추천
📌 스프링 컨테이너 생성
이번 강의에서는 스프링 컨테이너와 스프링 빈의 원리를 설명하고 있습니다. 스프링 컨테이너는 스프링 빈을 관리하는 주체이자 저장소라고 할 수 있습니다.
스프링 컨테이너는 XML 설정 방식, Annotation 기반 자바 설정 클래스(AppConfig)로 구성 가능합니다.
스프링 컨테이너 생성 과정
애노테이션 기반의 설정 클래스 기준으로 먼저 예시를 들고 있습니다.
val context: ApplicationContext = AnnotationConfigApplicationContext(AppConfig::class.java)
AnnotationConfigApplicationContext를 통해 애노테이션 기반 설정 클래스로 구성한 스프링 컨테이너를 가져올 수 있습니다.
@Configuration
class AppConfig {
@Bean
fun memberService(): MemberService {
return MemberServiceImpl(memberRepository())
}
@Bean
fun memberRepository(): MemberRepository {
return MemoryMemberRepository()
}
@Bean
fun orderService(): OrderService {
return OrderServiceImpl(memberRepository(), discountPolicy())
}
@Bean
fun discountPolicy(): DiscountPolicy {
return FixDiscountPolicy()
// return RateDiscountPolicy()
}
}
설정 클래스 안에는 @Bean 애노테이션으로 빈을 정의하고 있습니다. 여기서 따로 빈 이름을 설정하지 않으면 스프링은 메소드 이름으로 스프링 빈 이름을 설정합니다.
물론 빈 이름을 직접 지정할 수도 있습니다.
@Bean(name = ["memberCustomService"])
fun memberService(): MemberService {
return MemberServiceImpl(memberRepository())
}
코틀린에서는 Bean 애노테이션의 name 옵션 값이 Array로 되어 있어 이를 명시적으로 Array 형태로 입력해야 합니다.
스프링 컨테이너를 생성과정은 다음과 같이 축약할 수 있을 것 같습니다.
- 스프링 컨테이너 생성
스프링 빈 구성 정보지정(AppConfig.class) - 스프링 빈 등록
스프링 컨테이너 내부의 빈 저장소는 일종의 key, value로 이루어져 있음. 여기에 설정파일의 메소드 이름과 반환 객체를 가지고 각각 key, value로 객체를 저장소에 저장. - 스프링 빈 의존관계 설정
원래는 스프링 빈들을 다 생성하고 그다음 의존관계를 주입하는 단계가 나누어져 있지만 자바 설정파일로 한 경우에는 빈 등록시 생성자가 호출되면서 해당 생성자 인자를 가지고 의존관계 주입도 같이 이루어지게 됨
주의할 점은 빈 이름은 항상 다른 이름으로 설정해야 한다는 점입니다. 여러 빈들에 같은 이름으로 중복 설정하면 의도치 않게 다른 빈이 무시되거나 기존의 빈을 overwrite할 수 있습니다. (Spring Boot는 기본적으로 이러한 빈 중복을 아예 원천 차단한다네요.)
애노테이션 기반의 설정 클래스로 스프링 빈을 등록하면 그 안에 생성자를 호출하면서 스프링 빈 생성과 의존관계 주입이 한 번에 처리된다는 점도 중요해보였습니다.
📌 컨테이너 등록된 모든 빈 조회
applicationContext.getBeanDefinitionNames()
스프링 컨테이너에 등록된 모든 빈들을 조회할 수 있는데 위와 같이 등록된 모든 빈들의 이름 목록을 가져올 수 있습니다.
@Test
@DisplayName("모든 빈 출력하기")
fun findAllBean() {
val beanDefinitionNames = ac.beanDefinitionNames
for (beanDefinitionName in beanDefinitionNames) {
val bean = ac.getBean(beanDefinitionName)
println("name = ${beanDefinitionName}, object = ${bean}")
}
}
스프링 빈 이름으로 빈 객체를 조회(getBean) 할 수 있습니다. 위와 같은 코드로 스프링에 등록된 모든 빈들의 이름과 빈 인스턴스를 한 꺼번에 확인할 수 있습니다.
그런데 스프링에 등록된 모든 빈들 중에는 우리가 AppConfig를 통해 직접 설정한 빈들 뿐만 아니라 스프링 내부적으로 사용하기 위해 등록한 빈들도 있습니다. 이를 따로 구분해서 조회할 수 있습니다.
@Test
@DisplayName("애플리케이션 빈 출력하기")
fun findApplicationBean() {
val beanDefinitionNames = ac.beanDefinitionNames
for (beanDefinitionName in beanDefinitionNames) {
val beanDefinition = ac.getBeanDefinition(beanDefinitionName)
if (beanDefinition.role == BeanDefinition.ROLE_APPLICATION) {
val bean = ac.getBean(beanDefinitionName)
println("name = ${beanDefinitionName}, object = ${bean}")
}
}
}
- ROLE_APPLICATION: 사용자가 설정한 일반적인 스프링 빈
- ROLE_INFRASTRUCTURE: 스프링이 내부적으로 사용하기 위해 등록한 스프링 빈
우리가 직접 등록한 스프링 빈만 확인하고 싶을 때는 ROLE_APPLICATION로 조회하면 됩니다.
📌 스프링 빈 조회
기본
applicationContext.getBean([BeanName], [type])
applicationContext.getBean([type])
기본적으로 스프링 빈 이름과 타입을 가지고 스프링 빈을 조회할 수 있습니다. (ApplicationContext getBean 사용)
@Test
@DisplayName("빈 이름으로 조회")
fun findBeanByName() {
val memberService = ac.getBean("memberService", MemberService::class.java)
assertThat(memberService).isInstanceOf(MemberServiceImpl::class.java)
}
applicationContext.getBean([BeanName], [type]) 이걸 사용해서 스프링 빈을 조회한 것입니다.
@Test
@DisplayName("구체 타입으로 조회")
fun findBeanByName2() {
val memberService = ac.getBean("memberService", MemberServiceImpl::class.java)
assertThat(memberService).isInstanceOf(MemberServiceImpl::class.java)
}
첫 번째에서는 MemberService 인터페이스 타입으로 조회했는데 이번에는 MemberService의 구현체 타입(MemberServiceImpl)으로 스프링 빈을 조회한 것입니다.
@Test
@DisplayName("이름 없이 타입으로만 조회")
fun findBeanByType() {
val memberService = ac.getBean(MemberService::class.java)
assertThat(memberService).isInstanceOf(MemberServiceImpl::class.java)
}
스프링 빈 이름 없이 타입만으로도 스프링 빈을 조회할 수 있습니다. 당연하게 MemberService 뿐만 아니라 MemberServiceImpl로도 조회 가능합니다.
그런데 구체(구현체) 타입으로 조회하는 것은 바람직하지 못합니다. 객체 지향적으로 설계했는데 구체 타입으로 조회하면 유연성이 떨어지고 확장하는데 많은 변경지점이 발생하게 됩니다. (OCP 위반)
@Test
@DisplayName("빈 이름으로 조회 X")
fun findBeanByNameX() {
assertThrows(NoSuchBeanDefinitionException::class.java) {
val memberService = ac.getBean("xxxxx", MemberService::class.java)
}
}
등록이 안 된 빈 이름으로 조회를 하려고 하면 NoSuchBeanDefinitionException 예외가 발생합니다. 참고해두면 좋을 것 같네요.
스프링 빈 조회 (동일한 타입 둘 이상인 경우)
@Configuration
private class SameBeanConfig {
@Bean
fun memberRepository1(): MemberRepository {
return MemoryMemberRepository()
}
@Bean
fun memberRepository2(): MemberRepository {
return MemoryMemberRepository()
}
}
이렇게 같은 타입으로 스프링 빈을 2개 중복 등록하면 어떻게 되는지도 강의에서 설명하고 있습니다.
@Test
@DisplayName("타입으로 조회시 같은 타입이 둘 이상 있으면, 중복 오류가 발생한다.")
fun findBeanByTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException::class.java) {
val memberRepository = ac.getBean(MemberRepository::class.java)
}
}
기본적으로 getBean 조회시 이런 상황에서는 NoUniqueBeanDefinitionException 예외가 발생하게 됩니다.
val memberRepository = ac.getBean("memberRepository1", MemberRepository::class.java)
assertThat(memberRepository).isInstanceOf(MemberRepository::class.java)
두 개 이상의 같은 타입으로 중복된 빈들 중 하나를 조회하려면 빈 이름을 특정해서 조회하면 됩니다.
@Test
@DisplayName("특정 타입을 모두 조회한다.")
fun findAllBeansByType() {
val beansOfType = ac.getBeansOfType(MemberRepository::class.java)
beansOfType.keys.forEach {
println("key = ${it}, value = ${beansOfType[it]}")
}
println("beansOfType = ${beansOfType}")
assertEquals(beansOfType.size, 2)
}
ac.getBeansOfType() 을 통해서 특정 타입으로 등록된 모든 스프링 빈들을 조회할 수 있습니다.
Map 형식으로 반환해주는데 key는 빈 이름, value는 해당 빈 인스턴스입니다.
그런데 특정타입 하나로 여러 개의 스프링 빈을 등록한 경우는 실무에서 잘 사용되지 않습니다. (저는 아직까지 본 적이 없네요.)
스프링 빈 조회 (상속 관계)
스프링 빈 조회에 있어서 상속 관계가 적용이 됩니다.
즉, 상위 타입으로 스프링 빈을 조회하면 그에 해당하는 모든 하위 타입들을 같이 조회하게 됩니다.
@Configuration
private class TestConfig {
@Bean
fun rateDiscountPolicy(): DiscountPolicy {
return RateDiscountPolicy()
}
@Bean
fun fixDiscountPolicy(): DiscountPolicy {
return FixDiscountPolicy()
}
}
이번에는 DiscountPolicy 구현체 두 개를 다른 이름으로 하여 빈으로 등록했습니다.
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 중복 오류가 발생한다.")
fun findBeanByParentTypeDuplicate() {
assertThrows(NoUniqueBeanDefinitionException::class.java) {
ac.getBean(DiscountPolicy::class.java)
}
}
위에 동일한 타입 둘 이상인 경우와 비슷하게 DiscountPolicy로 다른 구현체 두 개가 스프링 빈으로 등록되었기 때문에 getBean 조회시 중복 에러가 발생합니다. 즉, DiscountPolicy 타입으로 조회하면 그의 하위 타입인 FixDiscountPolicy, RateDiscountPolicy 둘다 조회하게 되는 것을 알 수 있습니다.
@Test
@DisplayName("부모 타입으로 조회시, 자식이 둘 이상 있으면, 빈 이름을 지정하면 된다..")
fun findBeanByParentTypeBeanName() {
val discountPolicy = ac.getBean("rateDiscountPolicy", DiscountPolicy::class.java)
assertThat(discountPolicy).isInstanceOf(RateDiscountPolicy::class.java)
}
@Test
@DisplayName("특정 하위 타입으로 조회")
fun findBeanBySubType() {
val rateDiscountPolicy = ac.getBean(RateDiscountPolicy::class.java)
assertThat(rateDiscountPolicy).isInstanceOf(RateDiscountPolicy::class.java)
}
마찬가지로 등록된 스프링 빈 타입으로 조회하는 방법이 있습니다. 또한 상위 타입이 아닌 구현체 타입으로 특정 지어 조회하는 방법도 있습니다. 하지만 앞에서 봤듯이 유연성이 없어진다는 문제가 있습니다.
@Test
@DisplayName("부모 타입으로 모두 조회하기")
fun findAllBeansByParentType() {
val beansOfType = ac.getBeansOfType(DiscountPolicy::class.java)
assertEquals(beansOfType.size, 2)
beansOfType.keys.forEach {
println("key = ${it}, value = ${beansOfType[it]}")
}
}
@Test
@DisplayName("부모 타입으로 모두 조회하기 - Object")
fun findAllBeansByObjectType() {
val beansOfType = ac.getBeansOfType(Object::class.java)
beansOfType.keys.forEach {
println("key = ${it}, value = ${beansOfType[it]}")
}
}
ac.getBeansOfType()을 통해 상위 타입으로 모든 관련 하위 타입들을 한꺼번에 조회할 수 있습니다. 여기서 Object 타입으로 조회하게 되면 모든 스프링 빈들을 조회하게 되는데요. Object는 모든 클래스들의 최상위 타입이기 때문입니다.
📌 BeanFactory와 ApplicationContext
스프링 컨테이너로 사용되는 ApplicationContext는 사실 BeanFactory를 상속받고 있습니다. 즉, 스프링 컨테이너의 최상위 인터페이스가 바로 BeanFactory라 할 수 있습니다.
BeanFactory
BeanFactory는 getBean 메소드 등 기본적인 스프링 빈 조회 기능과 빈이 singleton, prototype인지 체크하는 기능 등 스프링 컨테이너의 베이스가 되는 기본 기능들을 제공해줍니다.
ApplicationContext
BeanFactory의 모든 기능들을 사용하면서 이외 추가 기능들도 제공해줍니다.
- MessageSource
- 국제화(i18n) 기능을 제공하는 인터페이스 - EnvironmentCapable
- 프로파일과 프로퍼티를 다루는 인터페이스
- 로컬, 개발, 운영 등의 애플리케이션 구동 환경을 설정할 수 있음 - ApplicationEventPublisher
- 이벤트를 발행, 구독하는 모델을 편리하게 지원
- 이벤트 프로그래밍에 필요한 인터페이스 - ResourceLoader
- 파일, 클래스패스, 외부에서 리소스를 편리하게 조회
- 리소스를 읽어오는 기능을 제공하는 인터페이스
ApplicationContext는 기본 스프링 빈 조회 기능 뿐만 아니라 그 외 편리한 기능들을 확장한 인터페이스라고 보시면 됩니다.
📌 다양한 설정 형식 지원 - XML, 설정 클래스 파일
스프링은 다양한 스프링 빈 설정 방식을 지원합니다. 위의 Annotation 기반의 설정파일(AppConfig) 방식은 AnnotationConfigApplicationContext를 통해 사용할 수 있습니다.
Annotation 기반 뿐만 아니라 xml 설정 파일 방식도 지원하는데 GenericXmlApplicationContext를 사용합니다.
<?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" />
</beans>
xml 설정 방식은 위와 같이 bean 태그 안에 지정하고자 하는 빈 class 정보, id 값, 그리고 (생성자, setter)주입 받을 빈 내용을 지정할 수 있습니다. xml 설정 방식은 요즘에도 사용하고 있는 곳이 있지만 거의 사용되지 않는 방식이고 거의 대부분은 Annotation 기반으로 설정한다고 보면 됩니다.
이렇듯 스프링은 빈 설정 방식까지 추상화를 통해 유연하게 여러 가지 방식을 적용할 수 있도록 지원하고 있습니다.
📌 스프링 빈 설정 메타 정보 - BeanDefinition
바로 위의 내용과 같이 스프링은 다양한 스프링 빈 설정 방식을 유연성 있게 지원하고 있습니다. 추상화를 통해 유연성을 가질 수 있었는데요. 여기서 BeanDefinition 내용이 나오게 됩니다.
- AnnotationConfigApplicationContext > AnnotatedBeanDefinitionReader > BeanDefinition
- GenericXmlApplicationContext > XmlBeanDefinitionReader > BeanDefinition
- XxxApplicationContext > XxxBeanDefinitionReader > BeanDefinition
여러 방식으로 빈을 설정하면 해당 설정파일을 ApplicationContext 여러 구현체들이 읽어들이는데요. ApplicationContext 구현체들은 BeanDefinitionReader를 사용해서 BeanDefinition을 생성하게 됩니다.
@Test
@DisplayName("빈 설정 메타정보 확인")
fun findApplicationBean() {
val beanDefinitionNames = ac.beanDefinitionNames
for (beanDefinitionName in beanDefinitionNames) {
val beanDefinition = ac.getBeanDefinition(beanDefinitionName)
if (beanDefinition.role == BeanDefinition.ROLE_APPLICATION) {
println("beanDefinitionName = ${beanDefinitionName}, beanDefinition = ${beanDefinition}")
}
}
}
ac.getBeanDefinition을 통해 설정된 BeanDefinition 정보들을 전부 조회할 수 있습니다. (안의 정보들은 참고)
BeanDefinition는 스프링이 다양한 형태의 설정 정보를 추상화해서 사용하는 것 정도만 이해하면 됩니다.
📌 정리
이번 강의 섹션에서는 스프링 컨테이너와 설정된 스프링 빈을 조회하는 방법, 그리고 마지막으로 추상화를 통한 여러 스프링 빈 설정 방식 지원한다는 내용과 BeanDefinition까지 살펴보았습니다. 정리하면 다음과 같습니다.
1. 스프링 생성 과정을 스프링 컨테이너 생성 > 스프링 빈 등록 > 스프링 빈 의존관계 설정 순서로 설명했습니다.
- 특히 애노테이션 기반의 설정 클래스로 스프링 빈을 등록하면 그 안에 생성자를 호출하면서 스프링 빈 생성과 의존관계 주입이 한 번에 처리된다는 점이 중요해 보였습니다.
2. 컨테이너에 등록된 빈 객체들을 조회하는 방법에는 여러가지가 존재했습니다.
- 결국 구현체 타입으로 조회하기 보다 추상화된 타입으로(상위 타입으로) 조회하는 것이 유연성에 부합한다는 것이 중요했습니다.
- 하나의 타입으로 여러 개 빈들을 생성하면 예기치 않은 스프링 에러가 발생할 수 있다는 점에서 주의해야 할 점으로 보았습니다.
3. BeanFactory와 ApplicationContext 관계를 확인할 수 있었습니다.
- BeanFactory는 getBean등 빈 조회하는 가장 기본적인 기능들을 담고 있고 이를 구현한 것이 ApplicationContext 입니다.
- ApplicationContext는 BeanFactory 뿐만 아니라 MessageSource, EnvironmentCapable, ApplicationEventPublisher, ResourceLoader 등 여러 인터페이스들을 상속받아 여러 다양한 기능들을 제공해준다는 점이 있었습니다.
4. 스프링은 다양한 스프링 빈 설정 방식을 지원합니다.
- ApplicationContext를 구현한 구현체 중에서는 xml 방식, annotation 설정 방식 등 지원하는 구현체가 존재합니다.
- 이렇게 여러 스프링 빈 설정 방식을 지원할 수 있는 것은 BeanDefinitionReader를 사용해 BeanDefinition 인터페이스 타입으로 반환하도록 설계했기 때문에 가능합니다.
5. 스프링은 스프링 빈 생성 과정부터 설정 방식까지 OOP에 입각해 유연성 있고 확장가능하도록 설계한 프레임워크라는 점에서 인상 깊었습니다.