JAVA/Spring

[Java / Spring] - Spring IoC / DI란

nam_ji 2024. 6. 10. 20:04

IoC / DI란

IoC란

  • IoC란 Inversion of Control의 줄임말이며, 제어의 역전이라고 합니다.
  • 스프링 애플리케이션에서는 오브젝트(빈)의 생성과 의존 관계 설정, 사용, 제거 등의 작업을 애플리케이션 코드 대신 스프링 컨테이너가 담당합니다.
  • 이를 스프링 컨테이너가 코드 대신 오브젝트에 대한 제어권을 갖고 있다고 해서 IoC라고 부릅니다.
  • 따라서, 스프링 컨테이너를 IoC 컨테이너라고 부릅니다.

IoC 컨테이너란

  • 스프링에서는 IoC를 담당하는 컨테이너를 빈팩토리, DI 컨테이너, 애플리케이션 컨텍스트라고 부릅니다.
  • 오브젝트의 생성과 오브젝트 사이의 런타임 관계를 설정하는 DI 관점으로 보면, 컨테이너를 빈팩토리 또는 DI 컨테이너라고 부릅니다.
  • 그러나 스프링 컨테이너는 단순한 DI 작업보다 더 많은 일을 하는데, DI를 위한 빈팩토리에 여러가지 기능을 추가한 것을 애플리케이션 컨텍스트라고 합니다.
  • 정리하면, 애플리케이션 컨텍스트는 그 자체로 IoC와 DI 그 이상의 기능을 가졌다고 보면 됩니다.

빈팩토리와 애플리케이션 컨텍스트

빈팩토리와 애플리케이션 컨텍스트 관계

1. 빈팩토리

  • 스프링 컨테이너의 최상위 인터페이스입니다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당합니다.
  • 대표적으로 getBean() 메서드를 제공합니다.

2. 애플리케이션 컨텍스트

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

설정 메타 정보

  • IoC 컨테이너의 가장 기초적인 역할을 오브젝트를 생성하고 이를 관리하는 것입니다.
  • 스프링 컨테이너가 관리하는 이런 오브젝트는 빈이라 부릅니다.
  • 설정 메타 정보는 바로 이 빈을 어떻게 만들고 어떻게 동작하게 할 것인가에 관한 정보입니다.
  • 스프링 컨테이너는 자바 코드, XML, Groovy 등 다양한 형식의 설정 정보를 받아들일 수 있도록 유연하게 설계되어 있습니다.


1. 어노테이션 기반 자바 코드 설정

@Configuration
public class AppConfig {
        @Bean
        public Service service() {
                return new ServiceImpl(repository());
        }
}
  • @Configuration : 1개 이상의 빈을 제공하는 클래스의 경우 반드시 명시해야 합니다.
  • @Bean : 클래스를 빈으로 등록할 때 사용합니다.

2. 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="service" class="spring.example.class.ServiceImpl">
             <constructor-arg name="repository" ref="repository"/>
     </bean>
</beans>
  • XML 기반의 설정 파일을 보면 자바 코드로 된 설정 파일과 거의 비슷하다는 것을 알 수 있습니다.
  • XML 기반으로 설정하는 것은 최근에 잘 사용하지 않습니다.

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

  • 스프링이 이렇게 다양한 형식을 지원하는 방법 중 하나가 BeanDefinition이라는 추상화가 있습니다.
  • 쉽게 말해, XML을 읽어서 BeanDefinition을 만들고, 자바 코드를 읽어서 BeanDefinition을 만듭니다.
  • 따라서 스프링 컨테이너는 자바 코드인지, XML인지 몰라도 오직 BeanDefinition만 알면 됩니다.
  • BeanDefinition을 빈 설정 메타 정보라고 하는데, @Bean과 <Bean> 당 각각 하나씩 메타 정보가 생성됩니다.

  • AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성합니다.
  • GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정 정보를 읽고 BeanDefinition을 생성합니다.
  • 새로운 형식의 설정 정보가 추가되면, XxxBeanDefinitionReader를 만들어서 BeanDefinition을 생성하면 됩니다.

Dependency(의존 관계)란

  • 토비의 스프링에서 의존 대상 B가 변하면, 그것이 A에 영향을 미친다고 합니다.
    즉, B의 기능이 추가되거나 변경되면 그 영향이 A에 미치는 것입니다.

DI(의존 관계 주입)란

  • DI란 Dependency Injection의 약자이며, 의존 관계 주입 (의존성 주입)이라고 불립니다.
  • new(객체를 직접 생성)로 생성하는 것이 아닌, 외부에서 생성된 객체를 주입받아 사용하는 것입니다.
  • IoC가 행하여 질때, Spring 내부의 객체들 간의 관계를 관리할 때 사용하는 기법입니다.
    • 강하게 결합된 Class를 분리
    • Application 실행 시점에 객체 간의 관계를 결정, 주입
    • 결합도를 낮추고 유연성을 확보합니다.


DI(의존 관계 주입) 구현 방법

1. 필드 주입

@Service
public class A {

    @Autowired
    private B b;
}
  • 변수 선언부에 @Autowired 어노테이션을 붙입니다.
  • 장점
    • 사용하기 편리합니다.
  • 단점
    • 단일 책임 원칙 위반 가능성이 커집니다.
      • @Autowired 선언만 하면 되므로 의존성 주입이 쉽습니다.
      • 따라서, 하나의 클래스가 많은 책임을 갖게 될 가능성이 높습니다.
    • 의존성이 숨는다.
      • 생성자 주입에 비해 의존 관계를 한 눈에 파악하기 어렵습니다.
    • DI 컨테이너와의 결합도가 커지고, 테스트하기 어렵습니다.
    • 불변성을 보장할 수 없습니다.
    • 순환 참조가 발생할 수 있습니다.

2. 수정자 주입

@Service
public class A {

    private B b;

    @Autowired
    public void setB(B b) {
        this.b = b;
    }
}
  • Setter를 사용한 주입입니다.
  • 장점
    • 선택적인 의존성을 사용할 수 있습니다.
  • 단점
    • 선택적인 의존성을 사용할 수 있다는 것은 A에 모든 구현체를 주입하지 않아도 B 객체를 생성하 수 있고, 객체의 메서드를 호출할 수 있습니다.
      즉, 주입받지 않은 구현체를 사용하는 메서들에서 NPE가 발생합니다.
    • 순환 참조 문제가 발생할 수 있습니다.

3. 생성자 주입

@Service
public class A {

    private B b;

    @Autowired
    public B(B b) {
        this.b = b;
    }
}
  • 생성자에 @Autowired 어노테이션을 붙여 의존성을 주입받을 수 있으며, 가장 권장되는 주입 방식입니다.
  • 장점
    • 의존 관계를 모두 주입해야만 객체 생성이 가능하므로 NPE를 방지할 수 있습니다.
    • 불변성을 보장할 수 있습니다.
    • 순환 참조를 컴파일 단계에서 찾아낼 수 있습니다.
    • 의존성 주입하기 번거롭고, 생성자 인자가 많아지면 코드가 길어져 위기감을 느낄 수 있습니다.
      • 이를 바탕으로 SRP 원칙을 생각하게 되고, 리팩토링을 수행하게 됩니다.

순환 참조

  • 순환 참조란 서로 다른 여러 빈들이 서로를 참조하기 있음을 의미합니다.
  • A에서 B에 의존하고, B가 A에 의존하면 순환 참조라고 볼 수 있습니다.

1. 필드 주입인 경우

@Service
public class A {

    @Autowired
    private B b;

    @Override
    public void aMethod() {
        b.bMethod();
    }
}

@Service
public class B {

    @Autowired
    private A a;

    @Override
    public void bMethod() {
        a.a();
    }
}
  • 이 상황은 A의 aMethod는 B의 bMethod를 호출하고 B의 bMethodsms aMethod를 호출하고 있는 상황입니다.
  • 서로 호출을 반복하면서 끊임없이 호출하여 StackOverflowError를 발생시키게 됩니다.
  • 이처럼 필드 주입이나 수정자 주입은 객체 생성 후 비즈니스 로직 상에서 순환 참조가 일어나기 때문에 컴파일 단계에서 순환 참조를 잡아낼 수 없습니다.

2. 생정자 주입인 경우

@Service
public class A {

    private final B b;

    @Autowired
    public A(B b) {
        this.b = b;
    }

    @Override
    public void AMethod() {
        b.bMethod();
    }
}

@Service
public class B {

    private final A a;

    @Autowired
    public B(A a) {
        this.a = a;
    }

    @Override
    public void BMethod() {
        a.aMethod();
    }
}
  • 생성자 주입일 때 애플리케이션을 실행하면 로그가 찍히며 실행이 실패합니다.
  • 이처럼 생성자 주입은 스프링 컨테이너가 빈을 생성하는 시점에 순환 참조를 확인하기 때문에 컴파일 단계에서 순환 참조를 잡아낼 수 있습니다.

@Autowired

  • DI를 할 때 사용하는 어노테이션이며, 의존 관계의 타입에 해당하는 빈을 찾아 주입하는 역할을 합니다.
  • 쉽게 말해, 스프링 서버가 올라갈 때 애플리케이션 컨텍스트가 @Bean이나 @Service, @Controller 등 어노테이션을 이용하여 등록한 스프링 빈을 생성하고, @Autowired 어노테이션이 붙은 위치에 의존 관계 주입을 수행하게 됩니다.
  • 그렇다면, @Autowired 어노테이션이 붙은 위치에 어떻게 의존 관계를 주입할 수 있는 방법은
    코드를 보면
  • /**
    * Note that actual injection is performed through a BeanPostProcessor which in turn means
    * that you cannot use @Autowired to inject references into BeanPostProcessor or 
    * BeanFactoryPostProcessor types. Please consult the javadoc for the 
    * AutowiredAnnotationBeanPostProcessor class (which, by default, checks for the presence 
    * of this annotation).
    * Since:
    * 2.5
    * See Also:
    * AutowiredAnnotationBeanPostProcessor, Qualifier, Value
    * Author:
    * Juergen Hoeller, Mark Fisher, Sam Brannen
    */
    @Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Autowired {
        boolean required() default true;
    
    }


    • @Target : 생성자와 필드, 메서드에 적용 가능합니다.
    • @Retention : 컴파일 이후(런타임 시) JVM에 의해 참조가 가능합니다. 런타임 시 이 어노테이션의 정보를 리플렉션으로 얻을 수 있습니다.
    • 위 코드 상담 주석을 보면, 실제 타깃에 Autowired가 붙은 빈을 주입하는 것은 BeanPostProcessor라는 내용을 찾을 수 있고, 그것의 구현체는 AutowiredAnnotationBeanPostProcessor인 것을 확인할 수 있습니다.