의존성 주입(Dependency Injection) 이란?
Spring 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하고 있는데, 그 중 하나가 의존성 주입(Dependency Injection, DI) 이다. DI란 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴인 전략 패턴과 유사한 방식을 따르고 있으며, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해준다.
DI is about how one object acquires a dependency
DI 는 필요로 하는 오브젝트를 스스로 생성하는 것이 아닌 외부로 부터 주입받는 기법을 의미한다.
마틴 파울러의 글에 따르면 3가지 타입으로 정의할 수 있다.
의존한다. 라는 의미
의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다라고 얘기한다.
즉, "A가 B에 의존한다." 라는 말은 "B가 변하면 A에 영향을 미치는 관계다." 라는 말이라고 할 수 있다. = A -> B
DI가 왜 필요한 지 코드를 보면서 설명해보자.
Object Dependencies(객체 의존성)
현재 객체가 다른 객체와 상호작용(참조)하고 있다면 현재 객체는 다른 객체에 의존성을 가진다.
public class SpellChecker {
private KoreanDictionary koreanDictionary;
public SpellChecker(){
this.koreanDictionary = new KoreanDictionary();
}
}
맞춤법 검사기를 만드는 프로젝트를 한다고 가정해보자. 맞춤법 검사기는 사전(Dictionary)을 사용하고 있고 종류는 KoreanDictionary이다. 이것을 SpellChecker는 KoreanDictionary에 의존한다고 표현한다. 그리고 직접 객체를 생성 및 참조를 하기 때문에 강한 결합을 갖는다고 할 수 있다.
만약 한국어가 아닌 영어, 독일어를 맞춤법 검사해야 된다면 어떻게 해야할까? 직접 SpellChecker 클래스 소스 코드를 수정해서 다시 배포를 해야한다.
그래서 위와 같은 예시는 크게 다음과 같은 문제점(의존성이 위험한 이유)을 가지고 있다.
- SpellChecker객체는 KoreanDictionary객체의 생성을 제어하기 때문에 두 객체 간에는 긴밀한 결합(tight coupling)이 생기고, tight coupling에 따라 KoreanDictionary객체를 변경하면 SpellChecker객체도 변경된다.
즉, 하나의 모듈이 바뀌면 의존한 다른 모듈까지 변경 되어야 한다. - 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어짐
1. 두 클래스가 강하게 결합되어 있음
위와 같은 SpellChecker클래스는 현재 KoreanDictionary클래스와 강하게 결합되어 있다는 문제점을 가지고 있다. 두 클래스가 강하게 결합되어 있어서 만약 SpellChecker에서 KoreanDictionary이 아닌 EnglishDictionary와 같은 다른 사전을 사용하고자 한다면 SpellChecker클래스의 코드 변경이 필요하다. 즉, 유연성이 떨어진다.
각각의 다른 사전들을 사용하기 위해 생성자만 다르고 나머지는 중복되는 SpellChecker클래스들이 파생되는 것은 좋지 못하다. 이에 대한 해결책으로 상속을 떠올릴 수 있지만, 상속은 제약이 많고 확장성이 떨어지므로 피하는 것이 좋다.
2. 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어짐
위의 SpellChecker와 KoreanDictionary는 객체들 간의 관계가 아니라 클래스들 간의 관계가 맺어져 있다는 문제가 있다. 올바른 객체지향적 설계라면 객체들 간에 관계가 맺어져야 한다. 객체들 간에 관계가 맺어졌다면 다른 객체의 구체화 클래스(KoreanDictionary인지 EnglishDictionary인지 등)를 전혀 알지 못하더라도, (해당 클래스가 인터페이스를 구현했다면) 인터페이스의 타입(Dictionary)으로 사용할 수 있다.
결국 위와 같은 문제점이 발생하는 근본적인 이유는 SpellChecker에서 불필요하게 어떤 사전을 사용할 지에 대한 관심이 분리되지 않았기 때문이다.
이런 문제점을 해결해 주는게 바로 DI다.
DI 방식은 2가지가 있다. (스프링에선 추가로 필드 주입(Field Injection)을 지원한다.)
일단 총 3가지 방법 중 생성자 주입을 이용하여 문제 해결을 하고 DI종류에 대해서는 다른 포스팅에서 정리하겠다.
의존성 주입(Dependency Injection)을 통한 문제 해결
위와 같은 문제를 해결하기 위해 우리는 다형성을 활용한다. KoreanDictionary, EnglishDictionary등 여러 가지 사전을 하나로 표현하기 위해서는 Dictionary라는 Interface가 필요하다. 그리고 KoreanDictionary에서 Dictionary인터페이스를 구체화 시켜주도록 한다.
public interface Dictionary{
}
public class KoreanDictionary implements Dictionary{
}
이제 우리는 SpellChecker와 Dictionary이 강하게 결합되어 있는 부분을 제거해주어야 한다. 이를 제거하기 위해서는 다음과 같이 외부에서 사전을 주입(Injection)받아야 한다. 그래야 SpellChecker에서 구체화 클래스에 의존하지 않게 된다.
public class SpellChecker {
private Dictionary dictionary;
public SpellChecker(Dictionary dictionary) {
this.dictionary = dictionary;
}
}
여기서 Spring이 DI 컨테이너 혹은 IoC 컨테이를 필요로 하는 이유를 알 수 있다.
우선 SpellChecker에서 Dictionary객체를 주입하기 위해서는 애플리케이션 실행 시점에 필요한 객체(빈)를 생성해야 하며, 의존성이 있는 두 객체를 연결하기 위해 한 객체를 다른 객체로 주입시켜야 하기 때문이다.
예를 들어 다음과 같이 KoreanDictionary이라는 객체를 만들고, 그 객체를 SpellChecker로 주입시켜주는 역할을 위해 DI 컨테이너가 필요하게 된 것이다.
public class BeanFactory {
public void test() {
// Bean의 생성
Dictionary dictionary = new KoreanDictionary();
// 의존성 주입
SpellChecker spellChecker = new SpellChecker(dictionary);
}
}
그리고 이러한 개념은 제어의 역전(Inversion of Control, IoC)라고 불리기도 한다. 어떠한 객체를 사용할지에 대한 책임은 프레임워크에게 넘어갔고, 자신은 수동적으로 주입받는 객체를 사용하기 때문이다.
의존성 주입(Dependency Injection, DI) 정리
의존관계 역전 원칙(Dependency Inversion Principle, DIP)
의존성 주입을 잘 했을 때는 의존 관계 역전 원칙(Dependency Inversion Principle)이 적용된다.
이는 2가지 규칙을 지키는 상태를 말한다.
- 상위 모듈은 하위 모듈에 의존해서는 안 된다. 둘 다 추상화에 의존해야 한다.
- 위 예제에서 상위 모듈인 SpellChecker와 하위 모듈인 KoreanDictionary는 추상화된 존재인 Dictionary에 의존하고 있다.
- 추상화는 세부사항에 의존해서는 안된다. 세부 사항은 추상화에 따라 달라져야 한다.
- 만약 Dictionary의 method 명이 바뀐다면 Dictionary를 의존하고 있는 구체 클래스KoreanDictionary의 method명도 변경되어야 한다.
의존성 주입의 장점
한 객체가 어떤 객체(구체 클래스)에 의존할 것인지는 별도의 관심사이다. Spring은 의존성 주입을 도와주는 DI 컨테이너로써, 강하게 결합된 클래스들을 분리하고, 애플리케이션 실행 시점에 객체 간의 관계를 결정해 줌으로써 결합도를 낮추고 유연성을 확보해준다. 이러한 방법은 상속보다 훨씬 유연하다. 단, 한 객체가 다른 객체를 주입받으려면 반드시 DI 컨테이너에 의해 관리되어야 한다는 것이다.
- Reduced Dependencies
- 종속성이 감소한다.
- components의 종속성이 감소하면 변경에 민감하지 않다.
- More Reusable Code
- 재사용성이 증가한다.
- 일부 인터페이스의 다른 구현이 필요한 경우, 코드를 변경할 필요없이 해당 구현을 사용하도록 components를 구성할 수 있다.
- More Testable Code
- 더 많은 테스트 코드를 만들 수 있다.
- Mock 객체는 실제 구현의 테스트로 사용되는 객체
- 종속성을 components에 주입할 수 있는 경우 이러한 종속성의 Mock 구현을 주입할 수 있다
- 예를 들어, Mock 객체가 올바른 객체를 반환할 때, null을 반환할 때, 예외가 발생할 때 모두 처리한다.
- More Readable Code
- 코드를 읽기 쉬워진다.
- components의 종속성을 보다 쉽게 파악할 수 있으므로 코드를 쉽게 읽을 수 있다.
의존성 주입의 단점
- 결국에는 모듈이 더 생기게 되므로 복잡도가 증가한다.
- 종속성 주입 자체가 컴파일을 할 때가 아닌 런타임 때 일어나기 때문에 컴파일을 할 때 종석성 주입에 관한 에러를 잡기가 어려워 질 수 있다.
하지만 의존 관계를 주입할 객체를 계속해서 생성하고 소멸한다면, 아무리 GC가 성능이 좋아졌다고 하더라도 부담이 된다. 그래서 Spring에서는 Bean들을 기본적으로 싱글톤(Singleton)으로 관리하는데, 우리는 이에 대해 자세히 알 필요가 있다.
참고