Dependency Injection의 줄임말로 말 그대로 해석하자면 "의존성 주입"이다. "의존관계를 외부에서 결정하고 주입하는 것"이며, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하는 방식이다.
개발을 하다보면 코드간의 의존성이 생기게 된다. A가 B를 의존한다는 것은 결국 B가 변하면 A도 그에 따른 영향을 받게 된다는 것이다. 이는 지속적인 코드 유지에 있어 그다지 좋은 영향을 가져다주지 못한다. 또 여기서 추상화를 곁들이면 이야기가 한층 풍부해진다. 인터페이스를 통해 의존관계에 있는 클래스를 추상화해보면, 더 다양한 구현이 가능해지는 것을 확인해 볼 수 있다.
위에서 언급했던 데로, 런타임시에 그 의존관계를 외부에서 주입하는 것으로 DI가 완성된다고 할 수 있다. 아래와 같은 코드를 예시로 들 수 있겠다.
Class Restaurant(
food: Food
) {
val main: Food = food
}
interface Food {
fun cook()
...
}
class Pasta(): Food {
override fun cook() {
~~
}
}
class Pizza(): Food {
override fun cook {
~~
}
}
--------
val restaurant = Restaurant(Pasta())
코드적으로 살짝 문제가 있을 순 있지만(?) 일단 구현 자체는 이러한 것 같다. 결국 실제로 코드가 돌아가는 시점에 restaurant가 어떤 메인 음식을 만들지 주입하게 되고, 이렇게 되면 Dependency Injection이 실현된 것이라고 보면 되는 것 같다.
의존성이 줄어든다
DI를 사용하게 되면 결국 인터페이스를 사용하게 된다. 그렇다는 말은 주입되는 구현체가 변경되더라도 실제적으로 코드 수정이 많지 않게 될 가능성이 커진다. 예를 들어 위 예시에서 메뉴가 피자에서 파스타로 바뀐다 하더라도, 기존에 restaurant 객체가 사용하고 있을 수 있는 cook()과 관련해서는 수정할 이유가 없어지기 때문이다.
재사용성이 높아진다.
인터페이스를 통하기 때문에 해당 인터페이스 자체를 여러 곳에서 재활용할 수 있게 된다.
테스트가 수월해진다.
이는 객체를 외부에서 주입해주기 때문에 특정 객체나 인터페이스에 대한 테스트를 분리해서 진행할 수 있어, 테스트가 용이해진다.
Inversion Of Control의 줄임말로 말 그대로 해석하면 "제어의 역전"정도가 될 것 같다. 프로그래밍적으로 해당 말을 해석해보면 프로그램의 제어권이 역전되는 것을 의미한다. Spring과 같은 framework에서 IOC가 사용된다~ 라는 의미는 결국 각 객체들의 lifecycle 자체를 코드를 작성하는 개발자가 아닌 Framework가 도맡아 진행하는 것을 말하는 것이다
만약 여러 객체들의 생명 주기를 직접 관리한다고 생각해보자. 작은 규모의 코드를 짜야 하는 경우에는 큰 문제가 없을 수 있다. 세개 정도의 객체를 생성하기 특정 method에서 한 개 정도는 삭제도 하고, 뭐 수정도 하고 잘 관리할 수 있을 것이다. 헌데 만약 아주 복잡한 비즈니스 로직을 적용해야 한다고 생각해보자. 세개의 객체의 생성, 수정, 삭제가 복잡하게 엮여있고, 거기에 비즈니스 로직까지 섞는 상상 말이다. 물론, 구현이 가능할 것이다. 허나 편하진 않을 것이다.
더 나아가 서비스가 아주 커져 다뤄야할 객체가 아주 많아졌다고 생각해보자. 개발자고 손수 하나하나 생성하고 삭제하고 하다보면, 반복작업도 굉장히 많아지고 그러다 실수가 나올 확률도 더 커지게 된다.
IOC는 객체의 lifecycle 관리, 흐름 제어를 제 3자(프레임워크 등)에게 위임하는 프로그래밍 모델이다. 결국 객체를 관리해주는 역할을 따로 분리하는 것으로, 개발자는 자신이 집중해야 하는 비즈니스 로직에만 집중할 수 있게된다. 따라서 각 객체는 자신이 나중에 언제 어떻게 쓰일지에 대한 고민 없이, 자신의 기능을 구현하기만 하면 되는 것이다.
IOC에 대해 정리하자면, 결국 개발자는 기계의 부품들을 정갈하고 잘 동작하도록 만들어 두는 것이다. 자신의 역할에 맞는 기능을 수행하도록 부품을 잘 만들어두면, 프레임워크가 객체들을 알맞은 시점에 생성하고, 메서드를 호출하고 또, 소멸시킨다. 이런 것이 바로 제어의 역전, IOC인 것이다.
그렇다면 이 DI와 IOC의 관계를 어떻게 보아야할까?
하나의 객체가 생성될 때 의존관계에 있을 다른 객체를 결국 "외부"에서 주입하게 되는 것이 DI인 것이고, 여기서 이 외부가 제3자가 되어 객체의 생명주기를 컨트롤할 수 있게 되어 IOC 모델이 완성된다.
결국 DI는 IOC 프로그래밍 모델을 구현하는 여러 방식 중 하나가 된다.
더 개념적으로 접근해보다면
스프링 프레임워크에서 객체의 생성/관리를 책임지는 컨테이너가 바로 이 IoC 컨테이너이다. 인스턴스의 생성부터 소멸까지 그 생명주기 관리를 바로 이 컨테이너가 대신 해주기 때문에, 개발자는 비즈니스 로직에 집중할 수 있게 된다.
Spring을 공부하다 보면 Bean이라는 개념을 마주하게 되는데, 이 개념은 바로 IoC Container가 관리하는 오브젝트들을 칭한다.
실제로 스프링에서 IoC container 역할을 하는 것이 ApplicationContext이다. 물론 기본이 되는 IoC Container는 Bean Factory이지만 여러 기능이 추가된 ApplicationContext를 일반적으로 사용한다.
@Configuration : 설정정보를 나타내는데 사용하며 클래스에 붙힌다. 해당 annotation이 있는 class 내부의 @Bean 이 붙은 method를 호출해서 반환된 객체를 스프링 컨테이너에 등록한다.(이렇게 관리를 한다)
@Component : 해당 annotation이 붙어있는 class 자체를 Bean으로 등록해준다. (component scan을 통해서)
스프링에서의 DI는 어떻게 이뤄지는가를 알아봅니다. 우선, 스프링에서는 ApplicationContext가 관리하는 Bean 만이 의존 주입이 가능합니다. 또한 이때 @Autowired라는 annotation을 활용합니다.
DI를 실현하는 방식으로는 field, setter, constructor 세가지가 있으나, 이 중에서 constructor가 가장 안전한 방법이라고 여러 글에서 소개한다. 물론 나도 이 방식을 선호한다.
@Service
class AService(
@Autowired val aRepo: ARepo
) {}
코틀린으로 작성할 경우 이와 같이 constructor에서 @Autowired annotation을 붙혀주면 해당 Type에 맞는 Bean을 탐색해서 자동으로 주입해주게 된다.
결국 @Autowired를 통해서 Spring Bean을 주입했고(DI), 그 주입을 Spring Container(IOC)가 해주는 것이기에, DI가 Spring의 핵심 개념이 되는 것이다.