DI는 Dependency Injection의 줄임말로, 다양한 우리 말 번역이 있지만, 이 글에서는 의존관계 주입이라는 말로 사용하고자 한다.
먼저 Dependency, 의존관계에 대해 알아보자.
“A가 B를 의존한다.”는 표현은 어떤 의미일까? 추상적인 표현이지만, 토비의 스프링에서는 다음과 같이 정의한다.
의존대상 B가 변하면, 그것이 A에 영향을 미친다.
- 이일민, 토비의 스프링 3.1, 에이콘(2012), p113
즉, B의 기능이 추가 또는 변경되거나 형식이 바뀌면 그 영향이 A에 미친다.
다음의 햄버거 가게 요리사 예시를 보며 설명을 계속하겠다.
햄버거 가게 요리사는 햄버거 레시피에 의존한다. 햄버거 레시피가 변화하게 되었을 때, 변화된 레시피에 따라서 요리사는 햄버거 만드는 방법을 수정해야 한다. 레시피의 변화가 요리사의 행위에 영향을 미쳤기 때문에, “요리사는 레시피에 의존한다”고 말할 수 있다. 코드로 표현해보면 다음과 같다.
class BurgerChef {
private HamBurgerRecipe hamBurgerRecipe;
public BurgerChef() {
hamBurgerRecipe = new HamBurgerRecipe();
}
}
의존관계를 인터페이스로 추상화하기
위 BurgerChef 예시를 보자. 지금의 구현에서는 HamBurgerRecipe만을 의존할 수 있는 구조로 되어있다. 더 다양한 BurgerRecipe를 의존 받을 수 있게 구현하려면 인터페이스로 추상화해야 한다.
다음의 코드에서 볼 수 있듯이, 다양한 버거들의 레시피에 의존할 수 있는 BurgerChef가 된다.
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();
}
// ...
}
의존관계를 인터페이스로 추상화하게 되면, 더 다양한 의존 관계를 맺을 수가 있고, 실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.
의존관계가 무엇인지에 대해, 그리고 다양한 의존관계를 위해 인터페이스로 추상화함을 알아봤다. 그렇다면, Dependency Injection은 무엇인가?
지금까지의 구현에서는 BurgerChef 내부적으로 의존관계인 BurgerRecipe가 어떤 값을 가질지 직접 정하고 있다. 만약 어떤 BurgerRecipe를 만들지를 버거 가게 사장님이 정하는 상황을 상상해보자. 즉, BurgerChef가 의존하고 있는 BurgerRecipe를 외부(사장님)에서 결정하고 주입하는 것이다.
이처럼 그 의존관계를 외부에서 결정하고 주입하는 것이 DI(의존관계 주입)이다.
토비의 스프링에서는 다음의 세 가지 조건을 충족하는 작업을 의존관계 주입이라 말한다.
- 클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스만 의존하고 있어야 한다.
- 런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
- 의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.
- 이일민, 토비의 스프링 3.1, 에이콘(2012), p114
DI는 의존관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이다. 런타임 시점의 의존관계를 외부에서 주입하여 DI 구현이 완성된다.
Burger 레스토랑 주인이 어떤 레시피를 주입하는지 결정하는 예시로 설명하고자 한다.
class BurgerChef {
private BurgerRecipe burgerRecipe;
public BurgerChef(BurgerRecipe burgerRecipe) {
this.burgerRecipe = burgerRecipe;
}
}
class BurgerRestaurantOwner {
private BurgerChef burgerChef = new BurgerChef(new HamburgerRecipe());
public void changeMenu() {
burgerChef = new BurgerChef(new CheeseBurgerRecipe());
}
}
class BurgerChef {
private BurgerRecipe burgerRecipe = new HamburgerRecipe();
public void setBurgerRecipe(BurgerRecipe burgerRecipe) {
this.burgerRecipe = burgerRecipe;
}
}
class BurgerRestaurantOwner {
private BurgerChef burgerChef = new BurgerChef();
public void changeMenu() {
burgerChef.setBurgerRecipe(new CheeseBurgerRecipe());
}
}
그렇다면, DI, 의존 관계를 분리하여, 주입을 받는 방법의 코드 구현은 어떠한 장점이 있을까요?
앞서 설명했듯이, 의존한다는 것은 그 의존대상의 변화에 취약하다는 것이다.(대상이 변화하였을 때, 이에 맞게 수정해야함) DI로 구현하게 되었을 때, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게됨.
기존에 BurgerChef 내부에서만 사용되었던 BurgerRecipe을 별도로 구분하여 구현하면, 다른 클래스에서 재사용할 수가 있다.
BurgerRecipe의 테스트를 BurgerChef 테스트와 분리하여 진행할 수 있다.
BurgerRecipe의 기능들을 별도로 분리하게 되어 자연스레 가동성이 높아진다.
DI(의존관계 주입)는 객체가 의존하는 또 다른 객체를 외부에서 선언하고 이를 주입받아 사용하는 것이다. 이를 구현함으로써 얻을 수 있는 장점들을 알아봤다.
자바와 관련된 서적이나, 스프링에 처음 입문하게 될 때, 자주 맞닥뜨리는 단어 DI. 용어의 늪에 빠지지 말고, 이 글을 통해 정리되었으면 한다.
참고 자료
토비의 스프링 3.1, Vol.1
DI는 IoC를 사용하지 않아도 된다
Ioc(DI, Service Locator…)
Dependency Injection Benefits
출처:https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/
DI 관련 많은 사이트를 돌아 보았으나 이해하기 가장 쉽게 설명하는 글을 갖고 왔다.
또한 DI의 방법을 3가지로 예를 들고 있는데 위에서 설명한 내용에 빠진부븐을 추가하고자 한다.
스프링 프레임워크(Spring Framework)에서 DI 방식은 다음과 같이 3가지 종류가 있다.
필드 주입(Field Injection)
세터 주입(Setter Injection)
생성자 주입(Constructor Injection)
Spring 3.x 버전까지만 해도 Setter Injection을 권장하였으나, 최근에는 순환참조, Coupling 등의 문제로 인해서 Spring 4.3 이후 버전 부터는 Contructor Injection를 권장하고 있다.
- 필드 주입(Field Injection) 방식
Field Injection 방식은 가장 간단한 방법으로 Bean으로 등록된 객체를 사용하고자 하는 클래스에 Field로 선언한 뒤 @Autowired 어노테이션 키워드를 붙여주면 자동으로 주입된다.
※ @Autowired는 등록된 Bean의 타입과 변수의 타입을 매칭해서 객체를 주입 시켜주며, 선언 위치는 인스턴스 변수, 생성자, 메소드 위에 선언한다.
@Service
public class BoardService {
@Autowired
private BoardDao boardDao;
public void doSomething() {
// ....
}
}
많이 사용됨에도 불구하고 Field Injection을 통한 의존성 주입은 권장되지 않는다. 이유는 너무 추상적인 Injection 기법 때문이다. 의존성 주입이 쉽기 때문에 의존 관계가 복잡해질 우려가 있으며, 이는 Framework의 사용에 있어 다음과 같은 안티패턴적 측면을 갖는다.
▶ Single Responsibility Principle Violation
너무나 쉬운 의존성의 주입은 하나의 클래스가 지나치게 많은 기능을 하게됨으로써 초기 설계의 목적성이자 "객체는 그에 맞는 동작만을 한다"라는 원칙에 위배되기 쉽다. 위배된 경우 리팩토링의 비용은 크다.
▶ Dependency Hiding
추상화된 의존관계는 의존성을 검증하기 힘들게 만든다.
▶ DI Container Coupling
Field Injection을 사용하면 해당 클래스를 곧바로 인스턴스화 시킬 수 없다. 이 부분 때문에 Constructor Injection 이 권장되는 이유이기도 하다. 가령 컨테이너 밖의 환경에서 해당 클래스의 객체를 참조할 때, Dependency를 정의해두는 Reflection을 사용하는 방법 외에는 참조할 수 있는 방법이 없다.
- 세터 주입(Setter Injection) 방식
Setter Injection 방식은 setter 메소드에 @Autowired를 붙여서 객체를 주입하는 방식입니다(Spring 3.x 버전 까지는 권장됐지만, 현재는 권장되지 않는 방법임).
@Service
public class BoardService {
private BoardDao boardDao;
@Autowired
public void setBoardDao(BoardDao boardDao) {
this.boardDao = boardDao;
}
}
- 생성자 주입(Constructor Injection) 방식
Constructor Injection 방식은 Spring 4.x 버전 이상부터 권장되고 있는 방법이며, 기존 Field Injection 방식의 단점을 극복해낸 패턴입니다.
@Service
public class BoardService {
private BoardDao boardDao;
// @Autowired Spring 4.3 버전 이상부터는 생성자가 하나인 경우 @Autowired 생략 가능
public BoardService(BoardDao boardDao) {
this.boardDao = boardDao;
}
}