Spring IoC Container

Jaeyoung·2023년 1월 2일
0
post-thumbnail

Spring IoC Container에 대해 알아보고자 이 글을 작성하게 되었습니다. Spring에서 DI Container, Bean Factory, ApplicationContext로 여러가지 이름으로 불리는 Bean을 생성하고 관리해주고 주입해주는 Spring Core에 있는 IoC Container는 객체지향적으로 잘 설계할 수 있도록 어플리케이션에 많은 기여를 해주고 있습니다. 지금의 Spring이 지속적으로 많이 쓰이는 이유도 이와같은 DI를 쉽게 사용할 수 있도록 지원해주기 때문이라고 생각이듭니다. 그래서 IoC Container가 무엇인지 한번 알아보도록 합시다.

IoC?

IoC Container의 IoC는 무엇일까요?

IoC는 Inversion of Control의 약자입니다. 해석하자면 제어의 역전이라고 불립니다. 제어의 역전이라고 하면 말 그대로 내가 직접 코드로 제어하는게 아닌 외부에서 제어하는 것을 뜻합니다. 일반적으로 라이브러리 같은경우는 사용자가 직접 그 코드를 이용해서 개발 할 수 있지만 프레임워크 같은 경우는 직접 제어 할 수가 없습니다. 이러한 프레임워크의 특성은 외부에서 제어하기 때문에 내부에서는 의존성을 따로 가지지 않기 때문에 모듈성을 높이고 확장성을 높여 줍니다. 그래서 Spring IoC Container는 왜 IoC Container라고 불릴까요? 그 이유는 빈(IoC Container가 관리하는 객체)을 생성하고 관리하고 주입해 준다는 특성에서 찾아 볼 수 있습니다. 프로그래머가 객체를 직접 생성하고 관리하는게 아닌 IoC Container를 통해 외부에서 주입을 받게 됩니다. 이것을 DI라고 하는데 DI는 Dependency Injection의 약자입니다. 의존성 주입이라는 의미를 가지고 있는데요 말 그대로 프로그래머가 직접 생성하는게 아닌 의존성에 대해 외부에서 주입을 받기 때문에 의존성 주입이라고 이야기 합니다. 의존성의 결합을 느슨하게 만드는게 목적인 DI는 주입하고자 하는 객체가 interface가 아니면 사실상 DI를 사용하는 의미가 없습니다. 왜냐하면 직접 생성해서 사용하는거랑 다를바가 없기 때문이죠 그래서 이러한 특성을 제대로 사용하기 위해서는 interface를 통한 추상화로 느슨한 결합을 만들어 준다는게 핵심입니다.

DI

의존성 주입은 크게 3가지 방법이 있는데요 그중 첫번째는 가장 많이 사용하는 생성자 주입입니다.

일반적으로 생성자 주입을 많이 사용하는 이유는 처음 딱 한번만 주입 받게되기 때문에 직관적이게 볼 수 있기도 하고 프레임워크를 갑자기 사용하지 않더라도 문제 없이 사용할 수 있기 때문입니다. 아래 코드를 통해 한번 보도록 하겠습니다.

public class DIClass(){
  private final InjectionClass ic;

	@Inject
	public DIClass(InjectionClass ic){
			this.ic = ic;
	}
}

그 다음은 Setter 주입입니다. 런타임 중에 의존성을 바꿀 수 있다는게 장점인데 사이드 이펙트가 발생하면 해당 문제에 대해 트래킹하기가 쉽지 않기 때문에 저는 별로 선호하지 않습니다. 아래 코드를 통해 한번 보도록 하겠습니다.

public class DIClass(){
  private final InjectionClass ic;

	@Inject
	public void setInjectionClass (InjectionClass ic){
			this.ic = ic;
	}
}

마지막으로는 가장 선호하지 않는 필드 주입입니다. 이 필드 주입은 프레임워크를 통해서만 가능하기 때문에 DI를 지원해주는 프레임워크를 사용하지 않으면 사용할 수 없습니다. 그래서 프레임워크에 종속적이라고 볼수 있습니다. 아래 코드를 통해 한번 보도록 하겠습니다.

public class DIClass(){
	@Inject
  private final InjectionClass ic;
}

확실히 상용구 코드가 줄기 때문에 좀 더 깔끔하지만 위와 같은 단점 때문에 잘 쓰이지는 않습니다.

Configuration Metadata

출처 : https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans

위에 있는 그림에서 볼 수 있듯이 Spring Container는 구성요소 메타데이터를 사용합니다. 이러한 구성요소 메타데이터는 어플리케이션 개발자가 Spring Container한테 어떻게 어떻게 해야해 라고 알려주는 것과 같다. 그래서 Spring Container는 이러한 정보를 통해 객체를 생성하고 관리하고 조립해줍니다.

이러한 구성요소 메타데이터를 설정하는 방법에는 크게 XML방식과 Code를 통해 정의하는 방식이 존재합니다. 일반적으로 많은 개발자들이 Code를 통해 정의하는 방식을 많이 사용합니다.

두 방식을 코드를 통해 한번 보도록 하겠습니다. 먼저 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
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="..." class="...">  
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>

이렇게 id와 class를 통해 빈에 대한 구성요소를 설정할 수 있습니다. id는 빈정보를 식별하는 값입니다. class는 해당 빈에 대한 정규화된 클래스 이름입니다.

자바 어노테이션으로 하는 방법은 아래와 같습니다.

@Configuration
public class TestConfiguration{
	@Bean
	public TestBean testBean(){
		return new TestBeanImpl();
	}
}

아무래도 자바코드로 작성할 수 있고 좀 더 직관적이기 때문에 많은 사람들이 어노테이션 방식을 선호하는 것 같습니다.

이 구성정보들을 가지고 ApplicationContext 즉 IoC Container를 생성하는 방법에 대해 아래 코드를 통해 알아 보겠습니다.

XML 방식

ApplicationContext context = new ClassPathXmlApplicationContext("구성정보.xml");

자바 코드 방식

ApplicationContext context = new AnnotationConfigApplicationContext(구성정보.class);

Bean과 ApplicationContext

Bean라는 개념을 Spring IoC Container에서 사용하게 되는데 Bean은 IoC Container에 의해 관리되는 객체를 이야기합니다. 그래서 IoC Container에 의해 생성되고 관리되는 객체를 Bean이라고 이야기합니다.

그러면 ApplicationContext란 뭘까요? ApplicationContext는 interface로 Spring IoC Container를 이야기합니다. 그래서 빈을 생성하고 구성하고 조립하는 책임을 가지고 있습니다. 그래서 구성요소 메타데이터을 읽어 해당 정보를 통해 빈을 생성하고 구성하고 조립합니다.

Bean에 대한 구성정보는 스프링 컨테이너 안에서 BeanDefinition 객체로 구현됩니다. 이 BeanDefinition은 아래 와 같은 메타정보들을 포함하고 있습니다.

  • 패키지 클래스 이름(일반 적으로 정의중인 Bean의 구현체)
  • Bean의 동작 구성요소(Scope,LifeCycle 등등)
  • Bean이 작업을 수행하기 위해 필요한 다른 Bean의 의존성
  • 기타 구성요소들

Bean naming convention은 일반적으로 인스턴스 클래스 이름에 대한 자바 표준 naming convention을 따릅니다. 그래서 빈 이름의 시작 글자는 소문자로 시작하고 카멜형태로 작성하는게 좋습니다. 이렇게 일관적이게 작성하면 직관적으로 이해할 수 있습니다. 컴포넌트 스캔을 통해 빈을 등록할 때는 해당 컨벤션을 통해 등록됩니다.

Bean Scope를 통해 Bean에 대한 Scope를 정의 해줄 수 있습니다. Bean Scope의 종류는 아래와 같습니다.

  • Singleton - 한개의 인스턴스만 생성됩니다.
  • prototype - 여러개의 인스턴스로 생성됩니다.
  • request - Http Request의 라이프 사이클을 따릅니다.
  • session - Http Session의 라이프 사이클을 따릅니다.
  • application - ServletContext의 라이프 사이클을 따르고 Spring ApplicatoinContext에서만 유효합니다.
  • websocket - WebSocket의 라이플 사이클을 따르고 Spring ApplicatoinContext에서만 유효합니다.

Singleton Scope를 가진 Bean에 prototype scope가진 Bean을 의존성 주입을 하게되면 Singleton Scope를 가진 Bean은 컴파일시점에 생성되고 주입 되기 때문에 prototype scope도 이 시점에 같이 생성되서 주입이 되기 때문에 런타임 시점에서 새로운 인스턴스를 생성하기 위해서는 setter 주입 방식이 있습니다. 해당 빈이 필요할 때마다 ApplicationContext를 통해 해당 빈을 조회하고 가져오게 되면 새로운 인스턴스가 반환됩니다. 또 다른 방법은 Aop를 통한 프록시로 해결하는 방법이 있습니다.

request, session, application, websocket scope는 웹 어플리케이션에만 사용되는 scope입니다.

빈 라이프사이클에 따라 수행해야하는 로직들이 존재 할 수 있는데 Spring에서는 이 또한 지원해줍니다. 빈이 생성될 때 어떤 로직을 수행하려면 InitializingBean를 구현하거나 @PostConstructor를 통해 수행할 수 있습니다. 반대로 빈이 제거 될 때 어떤 로직을 수행하려면 DisposableBean를 구현하거나 @PreDestory를 통해 수행할 수 있습니다.

스프링 컨테이너가 관리하고 있는 모든 빈들은 ApplicationContext의 시작 종료에 대한 event를 수신할 수 있습니다. LifeCycle interface를 구현해서 사용할 수 있습니다.

LifeCycle interface를 보면 다음과 같습니다.

public interface Lifecycle {
	
	void start();

	void stop();

	boolean isRunning();

}

start, stop, isRunning 메서드가 추상화 되어있는데 start는 ApplicationContext가 시작될 때 호출이 되고 stop은 종료될 때 호출하게 됩니다. isRunning은 현재 동작하고 있는지에 대한 메서드 입니다. 좀 더 세밀한 Lifecycle의 event를 수신해야하는 경우 LifecycleProcess interface를 구현하면 됩니다. 해당 Interface는 Lifecycle Interface에 대한 확장 interface입니다. onRefresh, onClose 메서드가 추가되었습니다.

public interface LifecycleProcessor extends Lifecycle {

	/**
	 * Notification of context refresh, e.g. for auto-starting components.
	 */
	void onRefresh();

	/**
	 * Notification of context close phase, e.g. for auto-stopping components.
	 */
	void onClose();

}

Refresh되거나 close될 때 호출되는 메서드입니다. 이런 라이프사이클에 대한 event가 어떻게 전달되는지 보려면 DefaultLifecycleProcess를 보면 직관적으로 이해할 수 있습니다. 아래는 등록된 빈들에게 start event를 송신 하는 코드입니다.

private void startBeans(boolean autoStartupOnly) {
		Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
		Map<Integer, LifecycleGroup> phases = new TreeMap<>();

		lifecycleBeans.forEach((beanName, bean) -> {
			if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())) {
				int phase = getPhase(bean);
				phases.computeIfAbsent(
						phase,
						p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
				).add(beanName, bean);
			}
		});
		if (!phases.isEmpty()) {
			phases.values().forEach(LifecycleGroup::start);
		}
	}

일단 lifecycleBeans와 phases라는 지역변수가 있는데 lifecycleBeans은 BeanFactory를 통해 Lifecycle를 구현한 빈들을 가져오게 되고 그 목록에 대한 Map입니다. phases같은 경우는 실행우선 순위를 보장하기 위해 phases를 TreeMap으로 선언하였습니다. BeanFactory를 통해 라이프사이클을 구현하고 있는 빈들을 가져오고 루프를 돌면서 우선순위를 설정해두고 모든 루프를 돌았으면 순차적으로 start를 호출해주게 됩니다. 이런식으로 빈에게 start event를 송신할 수 있습니다.

profile
Programmer

0개의 댓글