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

스프링 빈 생성 과정 분석 [1] - Application Context(BeanFactory)

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

[스프링 빈 생성 과정 분석 시리즈]

 

ApplicationContext(IOC 컨텍스트) 소개


스프링의 application context는 ioc 컨텍스트라고도 불리운다. 

스프링에서 ioc의 개념이 적용된 대표적인 예는 DI이다.

DI(Dependency Injection)는 ①의존 객체를 결정하고, ②의존 객체를 생성하고, ③객체 주입을 대신해준다.

우리는 이를 코드로 구현하지 않아도 되며 이에 따른 변경사항도 코드를 통해 수정하지 않아도 된다.

어노테이션이나 xml 등을 통해 설정하면 스프링에서 위 과정을 대신 처리해준다.

 

스프링에서 DI의 대상이 되는 객체를 스프링 빈이라고 한다.

스프링 빈을 생성 및 관리하는 책임을 갖는 인터페이스가 BeanFactory인데 ApplicationContext의 상위 타입이다.

ApplicationContext의 이외에도 다양한 책임들을 갖고 있다.

포스팅 1편에서는 간단하게 빈 생성 및 관리 이외에도 컨텍스트에서 무슨 기능을 제공해주는지 알아보도록 하자.

 

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

제어의 역전이란 프로그램의 제어 흐름 구조를 바꾸는 것이다.
프로그램의 제어권이 개발자가 작성한 코드에 있는 것이 아니라 외부에 존재하는 것을 말한다.
프로그램의 제어권에는 객체 생성, 사용할 객체 결정,  객체 주입, 메서드 호출 등 프로그래밍 언어로 표현할 수 있는 모든 행위를 포함한다. 

 

ApplicationContext의 상속 관계


 

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

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

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

나머지 MessageSource, EnvironmentCapable, ResourceLoader 는 라이브러리와 같이 기능을 제공할 뿐이지 애플리케이션의 흐름을 제어하는 것은 아니다.

 

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를 상속해야 이벤트 객체로 사용할 수 있었으나 이제는 별도의 상속 없이도 이벤트 객체로 사용가능하다.

리스너는 오직 event 객체에만 의존하고 이벤트 발행 대상도 알지 못한다.(객체간 의존관계 없음)

호출자와 피호출자의 의존도가 굉장히 낮은 구조이다. 주로 리턴값 필요 없고 부가 로직을 수행하는데 적용하기 좋다.

 

publishEvent가 메서드가 실행되면 프레임워크에서 해당 이벤트 객체 타입을 처리하는 이벤트 리스너를 가져와 메서드를 실행시켜주는 방식으로 흐름을 제어한다.

(잠깐, 딴소리)
  • 자바 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은 빈의 scope, 별칭, primary, lazy init과 같은 속성이다.

스프링 부트 auto-config 방식에서 사용되는 beanFactory가 ListableBeanFactroy의 구현체이기에

Primary, Qualifier, Lazy와 같은 어노테이션으로 빈의 관리 방식을 정의할 수 있는 것이다.

 

스프링의 빈은 BeanDefinition 기반으로 인스턴스화되어 관리된다. 

이에 대한 자세한 설명은 BeanDefinition이 어떻게 생성되고 관리되는지와 빈 인스턴스화 과정을 설명하며 함께 진행 하겠다.

반응형