🍃Spring

[Spring] IoC와 DI (1/2)

waveofmymind 2023. 5. 27. 12:33

Inversion of Control

  • 제어의 역전이라고 한다.
  • 스프링 애플리케이션에서 스프링 빈의 생성과 의존관계 설정, 사용 및 제거 등의 작업을 애플리케이션 코드가 아닌 스프링 컨테이너가 담당한다.
  • 이를 스프링 컨테이너가 코드 대신 빈에 대한 제어권을 갖고 있다고 해서 IoC라고 부른다.
  • 스프링 컨테이너를 IoC 컨테이너라고 부르기도 한다.

IoC 컨테이너

  • 스프링에서 IoC를 담당하는 컨테이너를 빈 팩토리, DI 컨테이너, Application Context라고 부른다.
  • 빈(오브젝트)의 생성과 빈 사이의 런타임 관계를 설정하는 DI의 관점에서 컨테이너를 빈 팩토리 또는 DI 컨테이너라고 부른다.
  • 그러나 스프링 컨테이너는 단순히 DI작업보다 더 많은 작업을 하는데, DI를 위한 빈 팩토리에 여러가지 기능을 추가한 것을 Application Context라고 한다.
  • 정리하면, Application Context는 그 자체로 IoC와 DI 기능 뿐만 아니라 그 이상의 기능을 가지고 있다.

빈 팩토리와 Application Context

빈 팩토리

  • 스프링 컨테이너의 최상위 인터페이스
  • 스프링 빈을 관리하고 조회하는 역할을 한다.
  • getBean() 메서드가 대표적이다.

ApplicationContext

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, 
		HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, 
				ResourcePatternResolver {}
  • 빈 팩토리를 상속한 만큼, 빈 팩토리의 기능을 모두 제공한다.
  • 위에서 extends한 인터페이스는 모두 빈 팩토리 인터페이스의 서브 인터페이스이며, 빈 팩토리에 없는 추가 기능을 제공한다. 즉, 애플리케이션은 이를 혼합해서 다음과 같은 기능을 제공한다.
    • 메시지 소스를 활용한 국제화 기능
      • 한국에서 들어오면 한국어로, 영미권에서 들어오면 영어로
    • 환경 변수
      • 로컬, 개발, 운영 등을 구분해서 처리한다.
    • 애플리케이션 이벤트
      • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
    • 편리한 리소스 조회
      • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회한다.

설정 메타 정보

IoC 컨테이너의 가장 기초적인 역할은 오브젝트를 생성하고, 이를 관리하는 것이다.

이 때, 스프링 컨테이너가 관리하는 오브젝트를 이라 부른다.

설정 메타 정보는 이 빈을 어떻게 만들고 어떻게 동작하게 할 것인가에 관한 정보이다.

스프링 컨테이너는 자바 코드, XML, Groovy 등 다양한 형식의 설정 정보를 받아들일 수 있도록 유연하게 설계되어있다.

어노테이션 기반의 자바 설정

@Configuration
public class AppConfig {

		@Bean
		public MemberService memberService() {
				return new MemberServiceImpl(memberRepository());
		}
}
  • @Configuration: 1개 이상의 빈을 제공하는 클래스의 경우 반드시 명시해야함
  • @Bean: 클래스를 빈으로 등록할 때 사용한다.

XML 기반의 스프링 빈 설정

<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 http://
www.springframework.org/schema/beans/spring-beans.xsd">

	 <bean id="memberService" class="hello.core.member.MemberServiceImpl">
			 <constructor-arg name="memberRepository" ref="memberRepository"/>
	 </bean>
</beans>
  • 형식이 다를 뿐, 자바 코드로 된 설정 파일과 비슷하다.
  • 최근에는 잘 사용하지 않는다.

스프링 빈 설정 메타 정보 - BeanDefinition

  • 스프링은 이런 다양한 형식을 지원하기 위해 BeanDefinition이라는 추상화가 존재한다.
  • 자바 코드를 읽어 BeanDefinition을, XML을 읽어 BeanDefinition을 만든다. 그렇기 때문에 스프링 컨테이너는 자바 코드, XML을 구분할 필요 없이 BeanDefinition만 알면 된다.
  • 이를 빈 설정 메타 정보라고 하는데, @Bean 과 <bean> 당 각각 하나씩 메타 정보가 생성된다.

  • AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성한다.
  • GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition을 생성한다.

Dependency Injection - 의존 관계 주입

의존관계란?

"A가 B를 의존한다" 는 추상적인 표현이지만, 토비의 스프링에서는 "의존 대상 B가 변하면, 그것이 A에 영향을 미친다" 라고 한다. 

즉, B의 기능이 추가되거나 변경되면 그 영향이 A에 미치는 것이다.

class BurgerChef {
    private HamBurgerRecipe hamBurgerRecipe;

    public BurgerChef() {
        hamBurgerRecipe = new HamBurgerRecipe();        
    }
}

햄버거 레시피가 변하게 되었을때, 변화된 레시피에 따라서 BurgerChef 클래스를 수정해야한다.

레시피의 변화가 요리사의 행위에 영향을 미쳤기 때문에 요리사는 레시피에 의존한다고 할 수 있다.

 

Dependency를 인터페이스로 추상화

위 예제에서, BurgerChef는 HamburgerRecipe만 의존할 수 있는 구조로 되어있다.

더 다양한 햄버거 레시피를 의존할 수 있게 구현하려면, 인터페이스로 추상화해야 한다.

class BurgerChef {
    private BurgerRecipe burgerRecipe;

    public BurgerChef() {
        burgerRecipe = new HamBurgerRecipe();
        //burgerRecipe = new CheeseBurgerRecipe();
        //burgerRecipe = new ChickenBurgerRecipe();
    }
}

interface BugerRecipe {
    newBurger();
} 

class HamBurgerRecipe implements BurgerRecipe {
    public Burger newBurger() {
        return new HamBerger();
    }
}

이제 더 다양한 버거 레시피에 의존할 수 있는 BurgerChef가 되었다.

이처럼 의존 관계를 인터페이스로 추상화하게 되면, 더 다양한 의존 관계를 맺을 수 있고, 실제 구현 클래스와의 관계가 느슨해지면 결합도가 낮아진다.

그래서 결국 DI는?

위 코드에서는 BurgerChef 내부적으로 의존관계인 BurgerRecipe가 어떤 값을 가질지 직접 정하고 있다.

이때 DI는 어떤 햄버거 레시피를 만들 지는 버거 가게 사장이 정하는 상황이라고 할 수 있다. 즉, BurgerChef가 의존하고 있는 BurgerRecipe를 외부에서 결정하고 주입하는 것이다.

class BurgerChef {
    private BurgerRecipe burgerRecipe;

    public BurgerChef(BurgerRecipe bugerRecipe) {
        this.burgerRecipe = bugerRecipe;
    }
}

//의존관계를 외부에서 주입 -> DI
new BurgerChef(new HamBurgerRecipe());
new BurgerChef(new CheeseBurgerRecipe());
new BurgerChef(new ChickenBurgerRecipe());

이처럼 의존관계를 외부에서 결정하는 것을 DI(의존 관계 주입)이라고 한다.

스프링에서는 외부의 대상이 IoC 컨테이너가 되어, 빈을 알아서 주입해준다.