글 작성자: beaniejoy
Index

📌 스프링 컨테이너 생성
  - 스프링 컨테이너 생성 과정
📌 컨테이너에 등록된 모든 빈 조회

📌 스프링 빈 조회
  - 기본
  - 스프링 빈 조회 (동일한 타입 둘 이상인 경우)
  - 스프링 빈 조회 (상속 관계)
📌 BeanFactory와 ApplicationContext
  - BeanFactory
  - ApplicationContext
📌 다양한 설정 형식 지원 - XML, 설정 클래스 파일
📌 스프링 빈 설정 메타 정보 - BeanDefinition
📌 정리

 

해당 내용은 강의 내용을 기억하기 위한 정리글입니다. 자세한 내용은 강의에서 확인하실 수 있습니다.
(저는 코틀린 베이스로 강의를 진행하였고 게시글의 코드 예시는 대부분 코틀린으로 이루어져 있습니다.)

스프링 핵심 원리 - 기본편(김영한님) #광고아님#내돈내산#적극추천

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com


 

📌 스프링 컨테이너 생성

이번 강의에서는 스프링 컨테이너와 스프링 빈의 원리를 설명하고 있습니다. 스프링 컨테이너는 스프링 빈을 관리하는 주체이자 저장소라고 할 수 있습니다.

스프링 컨테이너는 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 입니다.
  • ApplicationContextBeanFactory 뿐만 아니라 MessageSource, EnvironmentCapable, ApplicationEventPublisher, ResourceLoader 등 여러 인터페이스들을 상속받아 여러 다양한 기능들을 제공해준다는 점이 있었습니다.
4. 스프링은 다양한 스프링 빈 설정 방식을 지원합니다.
  • ApplicationContext를 구현한 구현체 중에서는 xml 방식, annotation 설정 방식 등 지원하는 구현체가 존재합니다.
  • 이렇게 여러 스프링 빈 설정 방식을 지원할 수 있는 것은 BeanDefinitionReader를 사용해 BeanDefinition 인터페이스 타입으로 반환하도록 설계했기 때문에 가능합니다.
5. 스프링은 스프링 빈 생성 과정부터 설정 방식까지 OOP에 입각해 유연성 있고 확장가능하도록 설계한 프레임워크라는 점에서 인상 깊었습니다.