Baeldung - Intro to Inversion of Control and Dependency Injection with Spring

sycho·2024년 3월 15일
0

Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

Overview

  • 이 글에선 Spring 프레임워크 형성에 핵심적인 개념이 된 Inversion of Control (IoC)와 Dependency Injection (DI)에 대해 알아본다.

What is Inversion of Control? (IoC)

  • 직역하면 제어 반전이다. 사용자가 짠 구체적인 코드가 framework 등에서 flow of control을 받는 것을 의미한다.

  • 보통 우리가 프로그램을 짜면 우리가 작성한 프로그램이 있고, 그 프로그램이 사용하는 라이브러리가 있다. 우리 프로그램이 라이브러리(framework)에 있는 함수를 호출하면 거기로 control이 넘어간다. 이를 control flow가 사용자 코드에서 framework으로 넘어간다고 생각할 수 있다.

  • 이것의 반대 상황, 즉 control flow가 framework에서 사용자 code로 넘어간다는 것을 Inversion of Control이라고 한다.

  • 이럴 때 프로그램은 어떻게 동작할까? 처음에 프레임워크의 코드가 control flow를 쥐고 있고 특정 상황에서 우리가 작성한 코드로 넘어가는 방식을 취한다.

  • 그럴려면 프레임워크가 우리의 코드를 추적할 수 있어야 우리 코드로 control을 넘길 수 있다. 마치 우리가 라이브러리를 모종의 방법으로 추적할 수 있어야 라이브러리로 control을 넘길 수 있는 것처럼 말이다. 그래서 관련 추상화 및 방식들을 프레임워크에서 제공한다. 아니면 프레임워크에서 제공하는 인식 기능을 활용해서 우리가 처음부터 직접 만든 class를 인지하게 하는 것도 가능하다.

  • 이 뭔가 오묘한 방식의 이점은 다음과 같다.

    • 어떤 과제 해결을 실제로 수행하는 영역과, 과제 해결을 위해 만들어놓은 구현체들 간의 분리가 용이하다.
    • 과제 해결을 위한 구현체를 손쉽게 바꿀 수가 있다. 그냥 수행 측에서 선택할 구현체를 바꾸면 되기 때문.
    • 프로그램의 모듈성이 좋아진다.
    • 프로그램 테스트가 쉬워진다. 각 구현체를 독립적으로 테스하는 것도 쉽고, 구현체의 mock object를 만들어가지가 프로그램을 수행하는 것도 직관적이게 된다.
    • 모든 component가 통일된 규칙 하에 통신을 하도록 조절하는것이 편하다.
    • 뒤에 나오는 object 간의 의존 관계 관리를 편리하게 해주는 환경을 만들 수 있다.

What is Dependency Injection (DI)

  • dependency는 직역하면 의존관계다. 흔히 OOP에서 배우는 상하 관계 (hierarchy relationship)과는 다르다. 밑의 pseudocde의 경우 B가 A에 의존 관계라고 한다.
class A {
	...
}
class B {
	A obj = new A();
}
  • 애플리케이션을 만들다 보면은 특정 software이 본인이 의도한 기능을 제공하기 위해 다른 기능을 제공하는 software에 의존하는 경향이 많이 있다. 핵심적인 기능을 담당하는 software은 다양한 다른 핵심적인 기능, 혹은 부수적인 기능을 담당하는 software들이 그러다 보면 이들은 각기 다른 요소랑 의존 관계를 형성하게 될 확률이 높다.

  • 위 의존관계의 경우 B에서 A에 해당하는 object를 '직접' 생성하고 있다. 하지만 외부에서 B를 위한 A class instance를 '주입'하는 것도 가능하다. 이를 injection이라고 한다.

  • 밑과 같이 pseudocode를 만들어보자.

class A {
	...
}

class B{
	A obj;
    injection(A obj) {
    	this.obj = obj;
    }
}
  • 지금 B는 A랑 의존관계를 가지고 있다. 그리고 injection이라는 method는 주입을 위한 툴이다. 이제 밑과 같이 코드를 짜면 B에 대해 A의 instance를 주입하는 것이다. 이를 종합해 Dependency Injection이라고 한다.
B bInst = new B;
bInst.injection(new A)
  • 이를 활용해 IoC를 구현하는게 가능하다. 이에 대해 자세히 알려면 DIP라는 개념도 알아야 한다.

Dependency Inversion Principle (DIP)

  • SOLID design principle의 D에 해당한다. 영문 정의를 직역하면
  1. 상위 module이 하위 module에 의존해선 안된다. 상위 모듈과 하위 모듈 모두 추상화에 의존해야 한다. 여기서 상위 모듈은 이 추상화를 활용하는 측이고(user-level) 하위 모듈은 추상화에 대한 구체적 구현을 정의하는 측이다 (machine-level)
  2. 추상화는 세부사항에 의존해서는 안된다. 세부사항은 추상화에 의존해야 한다.
  • 여기서 중요한게 하위 모듈과 상위 모듈 사이의 '관계'가 부정되는것이 아니다. 다만 서로 '의존'해서는 안된다는 것을 의미한다. Java의 경우 상위/하위 모듈이 공유하는 추상화를 구현하는 대표적인 방식이 interface를 활용하는 것이다.

DI를 통해 DIP 형식으로 IoC 이루기 + 의의

  • 관련해서 좋은 글이 있으며 이 글의 예시를 참고해서 설명하니 이해가 잘 안되면 이 링크의 소스코드 확인 바람.

  • AB라는 class가 있는데, A가 하위 module, B가 상위 module이라고 해보자.

  • BA가 의존 관계라고 하고, 둘을 연결짓는 추상화 (위 링크의 경우, Interface)를 I라고 해보자.

  • B는 저 I에 해당해는 객체를 보유하도록 설계되며, I의 구현체를 받을 수 있는 방식을 어떻게든 만든다. 대표적인 방식은 앞의 DI 설명때 나온 구현체를 method를 통해 받는 것.

  • AI를 실제로 구현한다. (내부 구현)

  • BA랑 의존관계를 갖도록 디자인할거면 저 I의 실제 구현으로 들어가는 것이 A의 객체이도록 한다. 즉 user level에서 보여질 기능, 즉 제어 주체를 외부에서 넣는다.

  • 이러면 AB의 의존관계가 중간의 추상화에 의해 독립된다. 즉 B 입장에서 제어주체는 임의의 추상화로 보이게, 관련해서 오류가 나올 때도 추상화 부분에서 오류가 나오기 때문에 분석이 좀 더 쉬워진다.

  • 위는 위키에 있는 IoC에 대한 그림이다. 보시다시피 DI랑 DIP를 활용해서 위와 같은 형태의 구조를 만들 수 있게 되었다.

The Spring IoC Container

  • IoC를 활용하는 프레임워크들은 보통 IoC container이라는 것을 보유한다. IoC container이란 위의 IoC를 적용받는 객체들이 언제 생성되고 언제 소멸할지를 관리한다. 또 앞의 DI 및 DIP 형식으로 IoC를 관리할 수 있도록 객체의 의존성 및 의존성 주입을 총괄하는 녀석이다. 한마디로 IoC 유지 총괄자다.

  • 저번 글에서 Spring도 IoC를 활용한다고 했다. 그리고 실제로 IoC container도 보유한다. 이에 해당하는 interface가 바로 ApplicationContext다.

  • IoC container, 그러니까 Spring에서 ApplicationContext가 관리하는 객체들을 Spring에서는 Bean이라고 부른다.

  • Spring은 이 ApplicationContext의 여러 구현체를 제공한다. AnnotationConfigApplicationContext, ClassPathXmlApplicationContext, FileSystemXmlApplicationContext, WebApplicationContext 등이 대표적. 마지막은 관련 인터페이스이긴 하다.

  • IoC container이 Bean을 생성하려면 설정 정보를 읽어야 한다. 위의 구현체들도 이 설정 정보들을 어떻게 읽느냐에 대해서 차이를 가지는 것이다.

    • AnnotationConfigApplicationContext : 설정 정보를 @Configuration과 같은 annotation을 통해 파악한다.
    • ClassPathXmlApplicationContext : 설정 정보를 Java class path에 있는 xml 파일을 읽어서 파악한다.
    • FileSystemXmlApplicationContext : 설정 정보를 URL 혹은 파일 시스템에 대한 경로를 활용해가지고 파악한다.
    • WebApplicationContext : 웹 애플리케이션 용도의 configuration context다. 가장 큰 차이점은 앞의 3개와 같은 일반 application용 ApplicationContext는 application 하나당 context가 하나지만, WebApplicationContext는 DispatcherServlet별로 context를 하나씩 가지는 것도 가능하다.
  • 위의 class의 생성자들을 이용해서 직접 ApplicationContext를 생성하는 것이 가능하다. 생성하는 그 즉시 Spring IoC container이 그걸 기반으로 bean을 관리하기 시작한다.

ApplicationContext context
  = new ClassPathXmlApplicationContext("applicationContext.xml");
AnnotationConfigApplicationContext annotationContext = new AnnotationConfigApplicationContext();
  • AnnotationConfigApplicationContext를 예로 들면, 이 녀석에게 @Configuration이 달린 class들을 전부 전달하면 IoC container이 거기 안의 @Bean과 관련된 annotation이 뭐가 있는지를 파악한다. 전부 파악이 되면 관련 bean들을 전부 생성하며, 이들 간의 관계도 고려해서 DI까지 직접 해준다. 그리고 언제 소멸이 될지까지도 관리를 한다.

  • 이 때 Spring의 IoC container은 여러가지 방식으로 bean을 초기화 및 주입한다.

Constructor-Based Dependency Injection

  • 생성자를 호출해가지고 bean을 주입하는 방식이다.

  • AnnotationConfigApplicationContext를 활용한다고 가정시 밑과 같이 코드를 짜면 ItemStore에 해당하는 bean을 하나씩 만들고 Store bean에 방금 만든 Item bean이 들어간다.

생성되는 bean에 이름 부여도 되는데, 안할 경우 생성에 사용한 생성자 이름이 bean의 이름이 된다. (item1, store)

@Configuration
public class AppConfig {

    @Bean
    public Item item1() {
        return new ItemImpl1();
    }

    @Bean
    public Store store() {
        return new Store(item1());
    }
}
  • 만약 xml 형식으로 설정하고 싶다면 밑과 같이 하자.
<bean id="item1" class="org.baeldung.store.ItemImpl1" /> 
<bean id="store" class="org.baeldung.store.Store"> 
    <constructor-arg type="ItemImpl1" index="0" name="item" ref="item1" /> 
</bean>

Scope of bean : Singleton and Prototype

  • 여기서 잠시 다른 얘기를 좀 해보겠다. bean 생성에서 한가지 유의할게 있는데, 바로 bean의 scope. 여기서는 Singleton과 Prototype에 대해 얘기해보겠다.

  • 명시가 없으면 singleton scope로 보통 bean을 생성한다. 이 경우 해당 종류의 bean은 딱 하나만 유지가 된다. 객체가 하나라는 것이다. 그래서 관련 bean을 여러 곳에서 주입을 받을 때 다 동일한 bean임이 보장된다.

  • 반대로 prototype으로 scope를 설정할 수 있다. 이 경우 해당 bean에 대한 주입을 요청하는 bean들 마다 해당 bean의 다른 instance를 보유하게 된다. 즉 여러 곳에 주입을 받은 bean들이 실제로 다 서로 다르다는 것이다. 이 bean은 더이상 사용이 안될 때 garbage collector에 의해 제거가 된다.

  • @Scope라는 annotation을 관련 @Configuration 포함 annotation이 달린 class에 달면 scope 변경이 가능하다.

  • 이 외에도 여러 종류의 scope가 존재하지만 여기서는 언급하지 않고 이 글을 참고하자.

Setter-Based Dependency Injection

  • 또 다른 bean 주입 방식. argument가 없는 생성자나 static factory method (이게 뭔지 몰라도 일단 넘어가자)를 호출해서 bean이 instantiate 된 다음에 setter을 통해 instantiate된 bean을 주입하는 것이다.

  • annotation 기반으로 한다면 밑과 같이 하면 Store에 해당하는 bean에 Item에 해당하는 bean을 instantiate해 주입할 수 있다.

@Bean
public Store store() {
    Store store = new Store();
    store.setItem(item1());
    return store;
}
  • xml의 경우 밑과 같이 하자.
<bean id="store" class="org.baeldung.store.Store">
    <property name="item" ref="item1" />
</bean>

Field-Based Dependency Injection

  • 어떤 class가 본인의 특정 field에 bean을 주입받고 싶다고 해보자. 그러면 annotation 기반의 경우 @Autowired를 사용하면 된다.
public class Store {
    @Autowired
    private Item item; 
}
  • xml의 경우 setter 방식을 사용하거나, 생성자 방식을 사용하는게 가능하다. 밑의 2개가 각각의 경우에 대한 예제.
<bean 
  id="indexService" 
  class="com.baeldung.di.spring.IndexService" />
     
<bean 
  id="indexApp" 
  class="com.baeldung.di.spring.IndexApp" >
    <property name="service" ref="indexService" />
</bean>    
<bean 
  id="indexApp" 
  class="com.baeldung.di.spring.IndexApp">
    <constructor-arg ref="indexService" />
</bean>   

Reflection

  • 이 때 한가지 의문이 들 수 있는게, 저 item field는 private다. 접근자도 주어져있지 않는데 어떻게 setter이나 생성자가 주어지지 않아도 IoC가 저기에 bean을 주입하는게 가능할까?

  • 가능하다. Java의 reflection이라는 기능 덕분이다. 이에 대한 자세한 내용은 이 글 참고

  • 이 기능을 사용한다는 것 때문에 Field-based DI를 추천하지 않는다. 이유는 생성자 기반이나 setter 기반보다 비용이 더 들기 때문. 뭐 그 외에도 너무 쉽게 dependency를 주입하는게 가능해서 SOLID의 S인 Single Responsibility Principle을 어기기 쉽다는 것도 있다만.

Autowiring Dependencies

  • 앞과 같이 Spring에서 알아서 적절히 주입을 해주는 것을 Wiring이라고 한다. 이 Wiring 방식이 앞과 같은 한가지 방식만 있는게 아니다. xml 형태의 configuration 파일을 활용해 어떤 방식으로 wiring을 할지 지정하는게 가능한데 밑의 종류가 가능하다.

    • no : 기본값. autowiring 사용 안함.
    • name : 성분들의 이름을 기반으로 주입. 해당 성분과 같은 이름의 bean을 찾아 주입
    • byType : 성분들의 타입을 기반으로 주입. 해당 성분과 같은 type의 bean을 찾아 주입
    • constructor : 생성자 argument를 기반으로 주입. 생성자의 argument와 같은 type의 bean들을 찾은 다음에 그걸 생성자의 argument로 사용.
  • 예시로 byType의 경우 다음과 같이 하면 된다.

<bean id="store" class="org.baeldung.store.Store" autowire="byType"> </bean>
  • 다른 예시로 byName의 경우 다음과 같이 하면 된다.

  • 코드에서도 위의 종류들을 다 사용하는게 가능하다.

    • no의 경우 그냥 @Autowire을 안 사용하면 된다.

    • name의 경우 정확히 대응되는건 없는데, @Qualifier을 활용해서 유사한 효과를 내는게 가능하다.

      public class Store {
          @Autowired
          @Qualifier("item1")
          private Item item;
      }
    • 앞에서 소개한 예제가 type 기반 autowire이다.

    • 마지막으로 생성자 기반 autowire은 밑과 같이 한다.

      public class Store {
          private Item item;
      
      			   @Autowired
          public Store(Item item) {
              this.item = item;
          }
      }

Lazy Initialized Beans

  • 기본 설정에서 container은 singleton bean을 처음 생성할 때 전부 초기화까지 완료를 한다. 이를 방지하고 싶으면 lazy-init을 설정하면 된다.
<bean id="item1" class="org.baeldung.store.ItemImpl1" lazy-init="true" />
  • 코드로도 가능하다. @Lazy라는 annotation을 사용하면 된다.
@Configuration
public class AppConfig {

    @Bean
    @Lazy
    public Item item1() {
        return new ItemImpl1();
    }
}
  • 만약 '전부' lazy-init을 하고 싶은 경우라면 그냥 application.propertiesspring.main.lazy-initializationtrue로 설정하면 된다. application.yml도 비슷한 방식.
profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글