본문 바로가기
Framework & Lib & API/스프링

스프링 빈 생성 과정 분석 [1] - IOC 컨텍스트(BeanFactory)

by 코딩공장공장장 2024. 4. 20.

ApplicationContext(IOC 컨텍스트) 소개


IOC (Inversion of Control, 제어의 역전)

제어의 역전이란 프로그램의 제어 흐름 구조를 바꾸는 것이다.

프로그램의 제어권이 개발자가 작성한 코드에 있는 것이 아니라 외부에 존재하는 것을 말한다.

프로그램의 제어권에는 객체 생성, 사용할 객체 결정,  객체 주입, 메서드 호출 등 프로그래밍 언어로 표현할 수 있는 모든 행위를 포함한다. 

 

스프링에서는 DI(Dependency Injection)라는 기능을 제공하는데 객체를 생성하고, 의존 타입 객체를 결정하고, 객체를 주입해준다.

 

이 책임을 수행하는 것이 ApplicationContext이다.

 

ApplicationContext가 객체 생성과 의존객체를 주입해주기 때문에 개발자는 이러한 작업을 코드로 표현하지 않아도 된다.

그리고 ApplicationContext가 관리하는 객체를 스프링 빈이라고 표현한다.

 

따라서 스프링 빈 생성 과정을 파악하기 위해서는 ApplicationContext가 어떻게 이루어져있는지 파악하는 것이 중요하다.

 

ApplicationContext의 상속 관계

 

ApplicationContext는 위와 같은 인터페이스를 상속 받고 있다.

  • BeanFactory : 빈의 생성, 스코프, 별칭 등을 관리
  • MessageSource : 다국어 메시지 처리
  • EnvironmentCapable : 애플리케이션 환경변수 참조
  • ResourceLoader : 클래스 경로, 파일 접근
  • ApplicationEventPublisher : 이벤트 리스너에게 이벤트 발행

사실 여기에서 IOC 개념이 적용되어있는 것은 BeanFactory 뿐이다.

따라서 우리가 중점적으로 둬야할 부분도 BeanFactory이지만 내용이 방대하기에 차차 다루도록 하고

다른 인터페이스부터 간단히 소개하겠다.

 

ApplicationContext의 상위 타입

i) MessageSource

String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException;

다국어 메시지를 처리할 수 있는 getMessage 메서드로 이루어져있는 인터페이스이다

code는 메시지 속성명, args는 메시지에 추가할 수 있는 인자값, Locale은 국가이다.

 

[예제]

messages.properties

name=Hello, {0} nice to meet you

messages_ko_KR.properties

name=Hello, {0} nice to meet you

 위와 같이 파일을 만들고 테스트코드를 작성하면

@Autowired
lateinit var messageSource: MessageSource

@Test
fun `메시지 테스트`() {
    Locale.setDefault(Locale("en", "US"))
    println(messageSource.getMessage("name", arrayOf("tony"), Locale.getDefault()))
    println(messageSource.getMessage("name", arrayOf("토니"), Locale.KOREA))
}

[출력]

Hello, tony nice to meet you
안녕, 토니 만나서 반가워

이와 같이 LOCAL에 따라 다국적 메시지를 출력할 수 있다.

운영환경이라면 서버 Local 설정을 그대로 따라갈 것이다.

 

 

ii) EnvironmentCapable

getEnviroment() 메서드 하나로 이루어져 있다.

Environment getEnvironment();

getEnviroment() 의 리턴타입인 Environment를 통해 application.properties나 application.yml에 정의한 프로젝트 환경변수에 접근이 가능하다.

 

[예제]

application-test.yml

spring:
application:
  name: learningExample
@SpringBootTest
@ActiveProfiles("test")
class IocContextTest {
  @Autowired
  private lateinit var context: ApplicationContext

  @Test
  fun `environment 테스트`() {
      val environment = context.environment
     
      environment.activeProfiles.forEach {
          println(it.toString())
      }
      println(environment.getProperty("spring.application.name"))
  }
}

[출력]

test

learningExample

 

 

iii) ResourceLoader

ResourceLoader는 클래스 경로나 파일에 대한 접근을 가능하게 하는 인터페이스이다.

Resource getResource(String location);

getResource 메서드에 경로를 인자로 넣으면 Resource를 반환해주는데 Resource는 getUri나 getFile과 같이 파일에 대한 직접적인 접근 메서드를 정의하고 있다.

 

[예제]

resources 하위에 abc가 입력된 text.txt 작성

@SpringBootTest
class ResourcesTest {

  @Autowired
  lateinit var resourceLoader: ResourceLoader

  @Test
  fun `리소스 파일 추출`() {
      val pattern = "classpath:text.txt"
      val resource: Resource = resourceLoader.getResource(pattern)

      println(resource.exists())
      println(resource.uri)
      println(resource.filename)
      println(resource.getContentAsString(Charset.defaultCharset()))
  }
}

[출력]

true

file:/C:/경로/LearningExample/spring-study/build/resources/main/text.txt

text.txt

abc

 

 

iv) ApplicationEventPublisher

이벤트 발행을 제공하는 인터페이스이다.

default void publishEvent(ApplicationEvent event) {
  this.publishEvent((Object)event);
}

void publishEvent(Object event);

publishEvent 메서드로 구성 되어있는데 과거에는 ApplicationEvent를 상속해야 이벤트 객체로 사용할 수 있었으나 이제는 별도의 상속 없이도 이벤트 객체로 사용가능하다.

 

위에서 설명한 MessageSource, EnvironmentCapable, ResourceLoader외부 파일에 접근하기 위한 인터페이스였지만 ApplicationEventPublisher객체간의 결합도를 느슨하게 해주는 것에 차이가 있다.

 

이벤트 객체를 제공받는 리스너는 오직 event 객체에만 의존하고 이벤트를 발행하는 객체의 존재 자체도 알지 못한다. 대개 리턴값 필요 없이 이벤트 발생 여부와 함께 리스너가 이벤트 처리에 필요로 하는 데이터를 전달해주기만 하면 되는 방식에서 사용하기 좋다. 객체가 연결되지 않으므로 인터페이스를 통한 DI 방식과 비교해도 결합도가 더 낮은 것이 장점이다.

 

(잠깐, 딴소리)
  • 자바 8에 default메서드가 인터페이스에 추가된 이유

    publishEvent(ApplicationEvent event) 메서드를 보면 publish(Object event) 메서드로 처리를 돌리고 있다.
    ApplicationEventPublisher에는 원래 publishEvent(ApplicationEvent event)만 존재하고 이 메서드가 이벤트 발행을 했지만 ApplicationEvent 상속없이 이벤트 발행을 가능하게 하는 구조로 변경함으로써

    기존의 publishEvent(ApplicationEvent event)를 default 메서드로 바꿔 이전에 작성된 구현체들과 호환을 이루게 하고 새롭게 정의한 publish(Object event)를 통해  ApplicationEvent 상속 없이도 사용 가능하게 하였다.

    이렇게 기존에 정의된 인터페이스의 결함이나 비효율으로 변경이 발생하면 기존의 메서드를 default 메서드(선택적 구현)로 선언하여 이전의 구현체와 호환을 이루도록 하고 변경된 새로운 추상 메서드를 제공할 수 있다.  자바8에서는 이러한 목적으로 default 메서드를 추가하였다.

 

[예제]

class AppleEvent(
    val name: String,
)


@SpringBootTest
class ObserverTest {

    @Autowired
    lateinit var eventPublisher: ApplicationEventPublisher

    @Test
    fun `이벤트 발행-구독 옵저버 패턴 테스트`() {
        eventPublisher.publishEvent(AppleEvent("청송사과"))
    }
@Component
class FruitEventListener {
    @Order(1)
    @Async
    @EventListener
    fun handleAppleEvent(apple: AppleEvent) {
        println("과일의 한 종류인 ${apple.name} 이벤트 리슨")
    }
}

@Component
class AppleEventListener {
    @Order(2)
    @Async
    @EventListener
    fun handleAppleEvent(apple: AppleEvent) {
        println("사과의 한 종류인 ${apple.name} 이벤트 리슨")
    }
}

[출력]

과일의 한 종류인 청송사과 이벤트 리슨

사과의 한 종료인 청송사과 이벤트 리슨

 

 

v) BeanFactory

대부분의 메서드가 getBean() 메서드의 오버로딩 형식을 통해 정의되어있고,

singleton이나 prototype과 같은 스코프 범위, aliases같은 빈의 별칭 또한 정의 되어있다.

따라서 스프링 BeanFactory의 빈 정의는 빈의 이름, 스코프(싱글톤, 프로토타입), 클래스 타입, 별칭이다.

Object getBean(String name) throws BeansException;

boolean isSingleton(String name) throws NoSuchBeanDefinitionException;


boolean isPrototype(String name) throws NoSuchBeanDefinitionException;


Class<?> getType(String name) throws NoSuchBeanDefinitionException;


String[] getAliases(String name);

구체적인 내용은 구현체를 다루면서 진행하겠으나 

getBean 메서드는 빈이 존재하지 않으면 빈을 생성하여 제공하는 방식으로 구현되어있다.

 

추가적인 개념으로 BeanFactory를 상속하며 ApplicationContext의 상위타입인 아래 두 BeanFactory에 대해서도 알아보겠다.



vi) ListableBeanFactory

BeanFactory에 정의된 메서드와 다르게 리턴값이 배열타입이다. 갯수 제공 메서드도 있다.

int getBeanDefinitionCount();

String[] getBeanDefinitionNames();

String[] getBeanNamesForType(@Nullable Class<?> type);

스프링 docs에서 설명하는 ListatblaBeanFactory 정의는 빈의 이름으로 하나씩 요청해야하는 BeanFactory와 달리 모든 빈 인스턴스를 열거할 수 있다고 설명한다

 

또 하나의 차이점은 BeanDefinition이라는 용어가 등장한다. 

이 BeanDefinition이 beanFactory가 정의 스프링 빈 정의다.
모든 빈 마다 BeanDefinition을 갖는다.
앞으로 계속해서 설명하겠지만 빈 인스턴스가 저장되는 장소와 BeanDefinition이 저장되는 장소는 다르다.

장소는 다르지만 BeanDefinition과 빈 인스턴스는 1대1 매칭 되어야한다.

이때 용하는 식별값이 빈 이름이다. 스프링에서 빈의 이름은 고유해야한다. 자바 클래스처럼 패키지 다르면 클래명 같아도 되는 것과 달리
빈 이름은 컨텍스트 내에서 반드시 고유해야한다.

 

다음 챕터에서 더욱 자세 설명하겠지만 중요한 부분이니 알고 넘어가자.

 

 

vii) HierarchicalBeanFactory

BeanFactory에도 계층 구조를 만들 수 있다. 부모-자식 관계를 만드는 것인데 객체간의 상속개념이 아니라 그냥 자식 계층에서 부모 계층 빈 팩토리 설정을 덮어쓰는 것으로 생각하면 될 것이다.

[예제]

@Configuration
class ParentConfig {
    @Bean
    fun parent() = Person("parent")

    @Bean
    fun overrideTest() = Person("is overridden?")
}

@Configuration
class ChildConfig {
    @Bean
    fun child() = Person("child")

    @Bean
    fun overrideTest() = Person("Yes, it is overridden")
}

위와 같이 설정하고

  @Test
    fun `BeanFactory 계층 구조 확인`() {
        // config 계층 설정
        val parentContext = AnnotationConfigApplicationContext(ParentConfig::class.java)
        val childContext = AnnotationConfigApplicationContext()
        childContext.parent = parentContext
        childContext.register(ChildConfig::class.java)
        childContext.refresh()

        val parent = childContext.getBean("parent", Person::class.java)
        val child = childContext.getBean("child", Person::class.java)
        val overrideTest = childContext.getBean("overrideTest", Person::class.java)
       
        println(parent.name)
        println(child.name)
        println(overrideTest.name)

        childContext.close()
        parentContext.close()
    }

[출력]

parent

child

Yes, it is overridden

 

HierarchicalBeanFactory는 실제로 어떤 상황에서 쓰이는지에 대해서는 잘 모르겠다.

스프링부트 autoConfig 방식에서도 부모 컨텍스트는 null로 선언되어 있다.

(혹시 알고 있으시다면 댓글로 공유해주시면 감사하겠습니다.)

반응형