DI
안녕하세요 ㅎㅎ
오늘은 Spring의 3대 요소중 하나인 DI(Dependency Injection)에 대해서 알아보려고 해요.!
먼저 우리가 제일 어려워하는 단어인, 의존성 주입(DI)이 무엇일까요?
의존성 주입이란?
-> 클래스의 연관관계를 주입해준다.
보통 의존성이라는 것은 클래스의 연관관계를 의미하는데,
class Person {
private Animal animal;
public String getPetName() {
return animal.name();
}
}
이 경우 Person은 Animal에 의존한다 라고 보시면 될 것 같습니다
Person이라는 객체가 Animal을 참조하고 있고, animal을 바탕으로 로직이 흐르기 때문이죠
그럼 다시 돌아와서 의존성 주입은?
클래스가 가지고 있는 연관관계를 주입해주는 것이다
라고도 볼 수 있겠네요.
만일 Animal에 구체적인 종류가 다양하다고 한번 가정해볼게요
class Dog extends Animal {
public String name() {
return "dogy";
}
}
class Mouse extends Animal {
public String name() {
return "jjikk";
}
}
간단하게 Dog와 Mouse라는 동물들이 존재한다고 하면,
Person이라는 객체는 어떤 Animal을 소유하고 있을 것인지 결정해야 되요
코드 레벨로 이야기하면 Person이라는 객체를 사용하게 전에, 어떤 Animal을 주입해줄 것인지 결정해주어야 하는데,
이것이 바로 Animal의 종류를 주입해주는
다시 말해서 Animal의 의존성을 주입해주는 과정이 있어야 Person이라는 객체를 사용할 수 있게 됩니다
의존성을 주입하는 방법
의존성 주입 과정에는 어떠한 과정을 통해 주입을 할 수 있을까요?
바로 3가지 과정이 있는데요,
- 생성자 주입(Constructor Injection)
- 세터 주입(Setter Injection)
- 필드 주입(Field Injection)
생성자 주입은, 객체가 생성되는 시점에 의존성이 주입되는 것이고
세터 주입은, 객체의 set Method 호출되는 시점에 의존성이 주입되는 것이고
필드 주입은, 객체의 인스턴스 필드에 의존성이 주입되는 것이다
그렇다면 어떤 방법이 제일 안전할까요?
바로 생성자 주입 방식이 제일 안전합니다
왜냐하면
- 단일 책임의 원칙(SRP)을 지키기가 쉽다 - 한눈에 해당 객체에 대한 의존관계와 복잡도를 알 수 있다
- 테스트가 용이하다 - DI컨테이너를 사용하지 않고도 인스턴스화 할 수 있고, 다른 DI 프레임워크로 쉽게 바꿀 수 있다
- Immutablity - 생성자로 주입했기 때문에, 객체의 불변성(final)을 보장할 수 있다
- 순환 의존성 - 생성자 주입에서는 객체가 서로 순환 의존성을 가질 경우 BeanCurrentlyInCreationException 이 발생한다
보통 Runtime에 마구잡이로 set method를 통해서 객체를 건드리는 습관은 좋지 않습니다
이 경우에는 NullPointerException이 발생하거나, 예상치 못하는 버그가 발생할 확률이 정말정말 높기 때문이죠
그렇기 때문에 객체의 멤버변수는 항상 불변객체(final)로 해주는 습관은 너무나 좋아요 ㅎㅎ
이 글을 보신 여러분들은 꼭꼭 지켜드리기를 당부드릴게요 😄
Spring DI
그렇다면, 도대체 Spring에서 DI는 어떻게 이루어질까요?
그림을 보시면 이 Application Context가 Bean Factory를 의존하고 있고,
Bean Factory가 객체에 대한 생성을 담당하는 것을 볼 수 있습니다
Spring에서 IoC Container라는 것은 결국 Application Context와 동일하며,
바로 이 IoC(Inversion of Control) Container가 주축이 되어 DI가 이루어지게 됩니다
IoC Container가 객체(Bean)에 대한 설정 파일(xml) 혹은 설정 객체(Configuration)들을 읽어서
해당 객체(Bean)의 의존성 주입이 이루어지게 되는 것이죠
한마디로 IoC Conatiner는 객체(Bean)에 대한 생명 주기(life cycle)을 담당하고 있다고 보시면 될 것 같습니다
그럼 도대체 생명 주기를 어떠한 과정을 통해서 객체(Bean)에 대한 생성과 소멸이 이루어지게 될까요?
객체의 생명 주기(life cycle)
먼저 IoC Container, 여기서는 Application Context라고 명시하겠습니다
- Container가 로드되면, Bean에 해당하는 객체들을 scan 하여, 해당 Bean들을 생성하려고 합니다
- 이 과정에서 의존성 주입이 이루어지게 되는데, 만약 순환 참조가 있다면 예외가 발생하여 Application은 종료됩니다
- 이제 Bean들이 생성되려고 하는데, 사용자가 지정한 init method가 존재한다면, 객체가 생성되고 init이 실행되게 됩니다
- 그 뒤에 사용자가 지정한 utility method(afterPropertiesSet)과 같은 메서드가 존재한다면, 해당 메서드가 실행되게 됩니다 = 콜백 함수
- 프로그램이 종료되기 전에 이제 Container도 같이 종료되려고 하는데, 이 과정에서 destory 메서드가 존재한다면, 실행하고 정상적으로 종료 됩니다
3번 과정의 경우 init에 대한 설정은 @PostConstruct 와 같은 어노테이션으로 지정할 수 있고,
다만, PostConstruct는 Spring 뿐만 아니라 다른 곳에서도 사용될 수 있다는 점 꼭 인지해주세요!
5번 과정의 경우 destory에 대한 설정은 @PreDestory 와 같은 어노테이션으로도 지정할 수 있습니다
PreDestory도 마찬가지로 Spring 뿐만 아니라 다른 POJO 프레임워크에 사용이 가능합니다 ( javax 라이브러리기 때문 )
init 과정과 4번 과정에 있는 utility method는 Spring에서 아래와 같은 인터페이스를 상속받아서 구현하면 됩니다
package org.springframework.beans.factory;
/**
* Interface to be implemented by beans that need to react once all their properties
* have been set by a {@link BeanFactory}: e.g. to perform custom initialization,
* or merely to check that all mandatory properties have been set.
*
* <p>An alternative to implementing {@code InitializingBean} is specifying a custom
* init method, for example in an XML bean definition. For a list of all bean
* lifecycle methods, see the {@link BeanFactory BeanFactory javadocs}.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @see DisposableBean
* @see org.springframework.beans.factory.config.BeanDefinition#getPropertyValues()
* @see org.springframework.beans.factory.support.AbstractBeanDefinition#getInitMethodName()
*/
public interface InitializingBean {
/**
* Invoked by the containing {@code BeanFactory} after it has set all bean properties
* and satisfied {@link BeanFactoryAware}, {@code ApplicationContextAware} etc.
* <p>This method allows the bean instance to perform validation of its overall
* configuration and final initialization when all bean properties have been set.
* @throws Exception in the event of misconfiguration (such as failure to set an
* essential property) or if initialization fails for any other reason
*/
void afterPropertiesSet() throws Exception;
}
설명에서도 보실 수 있듯이, 사용자가 원하는 초기화를 할 수 있다라고 명시하고 있네요 ㅎㅎ
반면, 5번 과정 destory 에 대한 과정을 구현 하고 싶다면?
@PreDestory 어노테이션으로 구현해주시고, 해당 함수의 콜백함수로.!
package org.springframework.beans.factory;
/**
* Interface to be implemented by beans that want to release resources on destruction.
* A {@link BeanFactory} will invoke the destroy method on individual destruction of a
* scoped bean. An {@link org.springframework.context.ApplicationContext} is supposed
* to dispose all of its singletons on shutdown, driven by the application lifecycle.
*
* <p>A Spring-managed bean may also implement Java's {@link AutoCloseable} interface
* for the same purpose. An alternative to implementing an interface is specifying a
* custom destroy method, for example in an XML bean definition. For a list of all
* bean lifecycle methods, see the {@link BeanFactory BeanFactory javadocs}.
*
* @author Juergen Hoeller
* @since 12.08.2003
* @see InitializingBean
* @see org.springframework.beans.factory.support.RootBeanDefinition#getDestroyMethodName()
* @see org.springframework.beans.factory.config.ConfigurableBeanFactory#destroySingletons()
* @see org.springframework.context.ConfigurableApplicationContext#close()
*/
public interface DisposableBean {
/**
* Invoked by the containing {@code BeanFactory} on destruction of a bean.
* @throws Exception in case of shutdown errors. Exceptions will get logged
* but not rethrown to allow other beans to release their resources as well.
*/
void destroy() throws Exception;
}
DisposableBean이라는 인터페이스를 상속받아서, destory의 콜백 메서드를 구현해주시면 됩니다
해당 로직을 구현해주시면, Container가 종료되는 시점에 Bean을 삭제하는 과정 직전 해당 메서드를 호출하고,
종료되는 것을 볼 수 있습니다
Spring의 Bean Life Cycle
그럼 Application Context는 어떻게 Bean들의 LifeCycle을 관리할까요?
바로 그것이 LifeCycle 이라는 인터페이스가 담당하고 있습니다
public interface Lifecycle {
/**
* Start this component.
* <p>Should not throw an exception if the component is already running.
* <p>In the case of a container, this will propagate the start signal to all
* components that apply.
* @see SmartLifecycle#isAutoStartup()
*/
void start();
/**
* Stop this component, typically in a synchronous fashion, such that the component is
* fully stopped upon return of this method. Consider implementing {@link SmartLifecycle}
* and its {@code stop(Runnable)} variant when asynchronous stop behavior is necessary.
* <p>Note that this stop notification is not guaranteed to come before destruction:
* On regular shutdown, {@code Lifecycle} beans will first receive a stop notification
* before the general destruction callbacks are being propagated; however, on hot
* refresh during a context's lifetime or on aborted refresh attempts, a given bean's
* destroy method will be called without any consideration of stop signals upfront.
* <p>Should not throw an exception if the component is not running (not started yet).
* <p>In the case of a container, this will propagate the stop signal to all components
* that apply.
* @see SmartLifecycle#stop(Runnable)
* @see org.springframework.beans.factory.DisposableBean#destroy()
*/
void stop();
/**
* Check whether this component is currently running.
* <p>In the case of a container, this will return {@code true} only if <i>all</i>
* components that apply are currently running.
* @return whether the component is currently running
*/
boolean isRunning();
}
자세한 내용은 주석을 통해 알아볼 수 있습니다 ㅎㅎ
우리는 이제 Spring 에서의 Bean 들이 어떻게 관리되고 생성/소멸 되는지를 알았으니
조금 더 디테일하게 들어가볼까요?ㅎㅎ
Application Context 파헤쳐보기
위에서 언급하였듯이 이 Application Context가 곧 IoC Container 입니다
IoC Container가 어떻게 Bean들을 생성하는지 한번 코드 레벨로 들어가볼께요 ㅎㅎ
[참고] - Spring DI 를 직접 구현한 레파지토리
Bean Factory 🚧
Spring에서는 기본적으로 Bean Factory
인터페이스를 상속받아서Application Context
를 구현하고 있어요
package core.di.factory;
import java.util.List;
public interface BeanFactory {
void initialize();
void registerBeanDefinition(Class<?> clazz, BeanDefinition beanDefinition);
<T> T getBean(Class<T> requiredType);
List<Class<?>> getBeanClasses();
List<Object> getBeans();
}
보시면 아시겠지만, bean에 대한 생성, 등록, 조회하는 기능들을 명시하고 있어요
BeanScanner 🔎
어쩌면 Spring에서 가장 중요한 발전중의 하나인 Bean Scanning
방식이 제일 중요하다고 생각해요
과거 Spring에서는 xml
을 기반으로 bean config을 등록해서 관리가 많이 힘들었다면,
지금은 @Bean / @Component
를 바탕으로 정말 쉽게 관리할 수 있기 때문이죠❗
package core.di;
import java.util.Set;
public interface Scanner<T> {
Set<T> scan(Object... basePackage);
}
@Override
public Set<Object> scan(Object... object) {
final AnnotationScanner annotationScanner = new AnnotationScanner();
final Set<Class<? extends Annotation>> scannedAnnotations = annotationScanner.scan(COMPONENT_SCAN_ANNOTATION);
final Set<Class<?>> classesAnnotatedComponentScan = allReflections.getTypesAnnotatedWith(COMPONENT_SCAN_ANNOTATION, true);
registerBasePackageOfComponentScan(classesAnnotatedComponentScan);
registerPackageOfClassesWithOutBasePackage(classesAnnotatedComponentScan);
registerBasePackageOfAnnotations(scannedAnnotations);
return new HashSet<>(this.basePackages);
}
가장 핵심은 Reflection에서 getTypesAnnotatedWith
라는 메서드인데요
Reflection을 통해 Base Package에 지정한 하위 패키지에 있는 모든 Class들을 가져와서@Component
라는 어노테이션이 달려진 Class들을 scan 하는 책임을 가지고 있는 메서드입니다~!
그래서 위 과정을 통해 우리가 만들어 놓은 Bean Factory
에 Map<Class<?> Object>
로 객체들을 가지고 있게 되는 거죠.!!
물론, Bean을 등록하는 과정에서 순환 참조 에 대한 검사도 해야됩니다 ㅎㅎ
private Object registerBean(Class<?> preInstantiateBean) {
if (beanInitializeHistory.contains(preInstantiateBean)) {
throw new CircularReferenceException("Circular Reference can't add to Bean Factory: " + preInstantiateBean.getSimpleName());
}
if (beans.containsKey(preInstantiateBean)) {
return beans.get(preInstantiateBean);
}
this.beanInitializeHistory.push(preInstantiateBean);
final Object instance = registerBeanWithInstantiating(preInstantiateBean);
this.beanInitializeHistory.pop();
return instance;
}
특정 객체에서부터 참조하고 있는 객체들을 registerBean
을 시행할 때,beanInitializeHistory
를 통해 객체들이 이미 등록되어 있는지 아닌지 체크 ✅해서
순환 참조에 대한 유효성 검사를 실시하게 되요.!
이렇게 하면 우리가 Spring DI
에서 지원하는 핵심적인 기술.!
- Bean Scanner
- Bean Factory
- Application Context
의 동작원리를 정확하게 알게 되었습니다 👋 👋 👋
참고
DI가 필요한 이유와 Spring에서 Field Injection보다 Constructor Injection이 권장되는 이유
'Developer > Spring' 카테고리의 다른 글
Spring Cloud Feign Testing - Feign Client를 테스트해보자 (6) | 2021.04.11 |
---|---|
Spring Data JPA - 영속성 상태에 대해서 (2) | 2021.03.02 |
Spring Boot 에서 Log4j2 를 설정하는 방법 (0) | 2020.09.28 |
Spring AOP의 원리 - CGlib vs Dynamic Proxy (11) | 2020.08.18 |
Spring에서 API 문서를 자동화하는 방법 - Spring REST Docs (0) | 2020.06.22 |