Spring

스프링의 핵심, IoC/DI, AOP, PSA - IoC/DI 편

minturtle 2024. 11. 26. 22:17
반응형

제어의 역전, IoC

개발자가 개발한 코드가 제어 흐름을 가지고 있지 않고, 외부 제어로부터 호출되어 자신의 일을 하는 것. 즉 프로그램 전체의 제어 권한이 프로그래머에게 있지 않은 것을 Inversion of Control(제어의 역전)이라고 합니다.

  • 전통적인 프로그래밍 방식은 개발자의 코드가 라이브러리를 호출하는 방식입니다.
  • 제어의 역전이 적용되면 외부 라이브러리 코드가 개발자의 코드를 호출합니다.

의존 관계 주입, DI

정의

프로그램에 속한 객체들은 의존 객체들의 구현체를 직접 생성하지 않고 외부로 부터 주입받아 사용하는 것을 의미합니다.

  • Java에서는 인터페이스를 통해 실제 구현체를 몰라도 실행이 가능하기 때문에 모듈 결합도를 낮추고 변경에 유리합니다.
class NoDIClass{
	private final B b = new B();
}
// ----------------
class DIClass{
	private final B b;
	
	public DIClass(B b){
		this.b = b;
	}
}

class DIFactory{
	
	public B b(){
		return new B();
	}

	public DIClass diClass(){
		return new DIClass(b());
	}
}

Spring 에서는 DI Container를 지원해(위의 DIFactory와 유사) 객체를 생성하고 의존성을 주입하도록 도와주며, 프로그램이 실행되면 Spring은 Spring DI Container를 사용하여 Spring이 객체를 관리하게 됩니다.(DI가 IoC를 만족하는 하나의 디자인 패턴)

 

Spring의 DI

  • 그럼 Spring에서 어떻게 DI를 사용할 수 있을까요? 아래의 예시 코드를 보겠습니다.
package org.example;

public class Main {
    public static void main(String[] args) {

        ApplicationContext applicationContext = new AnnotationConfigApplicationContext("org.example");

        MyService myService = applicationContext.getBean("myService", MyService.class);
        MyServiceB myServiceB = applicationContext.getBean("myServiceB", MyServiceB.class);

        myService.print();
        myServiceB.print();
    }
}
package org.example.config;

public class AppConfig {
    @Bean
    public MyService myService() {
        return new MyService();
    }
}
package org.example.service;

@Component
public class MyServiceB {

    public void print(){
        System.out.println("hello world B");
    }

}
  • 위 코드에서 main 함수는 Spring의 DI Container 구현체 중 하나인 AnnotationConfigApplicationContext를 호출하는데요. AnnotationConfigApplicationContext의 생성자 중 하나인 basePackage Scan은 basePackage를 입력받아 그 패키지와 하위 패키지에서 @Configuration 어노테이션 내부의 @Bean이 붙은 객체와, @Component 객체를 읽어 들여 객체를 생성, 의존성 주입, 관리를 하게 됩니다.

Bean Scope

  • Spring ApplicationContext는 이렇게 빈 객체를 등록, 의존성 주입 하는 것 외에도 여러 기능을 추가로 지원합니다만, 그 중 DI와 관련된 기능 중 하나인 Bean Scope에 대해 설명드리고자 합니다.
  • Bean Scope는 빈 객체의 생명주기를 의미하며, 아래의 Scope 종류가 있습니다. ApplicationContext가 가지고 있는 BeanFactory에 따라 지원하는 Bean Scope가 조금씩 다릅니다.
public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory {
	protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, 
	                       @Nullable Object[] args, boolean typeCheckOnly) {
			//...
		  if (mbd.isSingleton()) {
		      // create singleton bean...
		  }
		  else if (mbd.isPrototype()) {
				// create prototype bean
			}
			else{
				// create etc scope bean
				Scope scope = this.scopes.get(mbd.getScope());
				// ...
			}
	    return (T) bean;
	}
}

 

모든 ApplicationContext에서 사용 가능한 Scope 

  1. singleton : DI 컨테이너 당 하나의 인스턴스만 생성
  2. prototype: 매번 새로운 인스턴스 생성

Web 관련 ApplicationContext에서만 사용 가능한 Scope

  1. request: 각 HTTP 요청마다 하나의 인스턴스를 가짐.
  2. session: HTTP Session 당 하나의 인스턴스를 가짐.
  3. application: ServletContext당 하나의 인스턴스 생성
  4. websocket: WebSocket 세션당 하나의 인스턴스 생성

Reactive Web ApplicationContext에서만 사용가능한 Scope

  1. request: Webflux 환경에서 HTTP 요청마다 하나의 인스턴스를 가짐.
  2. session: WebSession당 하나의 인스턴스 생성
  3. application: ApplicationContext당 하나의 인스턴스를 가짐.

만약 Scope가 다른 Bean을 주입받으려고 하면 주의해야 할 점이 있는데요, 예시는 다음과 같습니다.

싱글톤 빈의 생성 시점에 Request 빈을 주입받으면 아직 HTTP Request가 들어오지 않았기 때문에 주입을 받을 수 없다.

 

이 문제를 해결하기 위해선 Spring은 두가지의 방법을 제안합니다. (참고로 아래 두 방법은 객체가 의존성을 찾는다는 의미로 Dependency Lookup, 즉 DL이라고 합니다.)

1. Provider 사용

@Autowired
private ObjectProvider<PrototypeBean> provider;
public int logic() {
      PrototypeBean prototypeBean = provider.get();
      return prototypeBean.count();
}

2. Proxy 사용

@Scope(value = "request", ScopedProxyMode.TARGET_CLASS)
public class HttpRequestBean{

}

내부 구현 찍먹하기

  • 그럼 AnnotationConfigApplicationContext가 어디에 bean을 저장하고 가져올까요?
public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext{

		@Override
		public <T> T getBean(String name, Class<T> requiredType) throws BeansException {
			assertBeanFactoryActive();
			return getBeanFactory().getBean(name, requiredType);
		}
		
		public abstract ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;

	}
	
public class GenericApplicationContext extends AbstractApplicationContext
	implements BeanDefinitionRegistry {

		@Override
	public final ConfigurableListableBeanFactory getBeanFactory() {
		return this.beanFactory;
	}
}
  • 빈 객체를 가져오는 메서드는 AbstractApplicationContext의 getBean() 메서드로, 이 메서드는 Template Method 패턴을 통해 getBeanFactory에서 구현체가 가지고 있는 BeanFactory를 반환하도록 하고, 여기서 Bean을 조회하게 됩니다.
  • 여기서 getBean(String name, Class<T> requiredType) 메서드는 AbstractBeanFactory에서 정의되어 있는데요, 이는 내부적으로 doGetBean 메서드를 호출합니다.
protected <T> T doGetBean(String name, @Nullable Class<T> requiredType, 
                         @Nullable Object[] args, boolean typeCheckOnly) {
    
    // 1. Bean 이름 변환 (별칭 처리)
    String beanName = transformedBeanName(name);
    Object bean;

    // 2. 싱글톤 캐시에서 찾기
    Object sharedInstance = getSingleton(beanName);
    if (sharedInstance != null && args == null) {
        bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
    }
    else {
    
				RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
        // 3. Singleton Bean 생성
        if (mbd.isSingleton()) {
            sharedInstance = getSingleton(beanName, () -> {
                try {
                    // 실제 Bean 생성 로직
                    return createBean(beanName, mbd, args);
                }
                catch (BeansException ex) {
                    destroySingleton(beanName);
                    throw ex;
                }
            });
            bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
        }
        else if (mbd.isPrototype()) {
					// 3.1 ProtoTypeBean 생성
				}
				else{
					// Singleton, ProtoType이 아닌 Bean 생성
					Scope scope = this.scopes.get(mbd.getScope());
					// ...
				}
    }
    return (T) bean;
}

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
			Object singletonObject = this.singletonObjects.get(beanName);
}
  • 마지막으로 doGetBean 의 내부 구현인데요, getSingleton 메서드를 통해 내부 Map 자료구조에 이미 만들어진 Singleton 객체가 있는지 확인하고, 없다면 BeanDefinition을 찾아서 Bean을 생성하는 것으로 보입니다.
  • 사실 createBean의 상세 구현도 보고싶었는데, 너무 깊게 들어가기엔 머리만 아플거 같아서 이정도만 보기로 했습니다.

정리하자면, ApplicationContext는 내부적으로 BeanFactory를 갖고있습니다. BeanFactory에서는 BeanDefinition이라는 Bean 객체에 대한 메타데이터를 갖고 있으며, 설정에 따라 Bean 객체를 생성합니다.(자세한 내용은 Spring Bean Lazy Init 참고)

 

계층형 Application Context

Application Context를 계층 구조로 포함할 수 있다고 하는데요, 자식 Context는 부모 Context와 빈을 공유하지 않기 때문에 특정 환경에 따라 다른 Application Context를 사용할 수 있다는 장점이 있습니다.(참고

  • 실제로 Spring Boot나 Spring Web MVC와 같은 라이브러리는 계층형 Application Context를 사용합니다.

정리

  • 제어의 역전은 프로그래머의 코드가 프로그램 전체의 주도권을 가지고 외부 라이브러리를 실행하는 것이 아닌, 외부 라이브러리가 주도권을 가지고 프로그래머의 코드를 실행하는 것
  • DI는 제어의 역전을 만족하는 디자인 패턴 중 하나로, 코드에서 의존 객체를 직접 생성하는 것이 아닌 의존 객체를 외부로 부터 주입받는 것
  • Spring은 Application Context를 사용해 DI를 구현하고 있으며, Application Context는 개발자가 Bean으로 등록한 클래스들을 생성하고 의존성을 주입해주는 역할을 수행

 

 

참고

반응형