의존성 주입

코와->코어·2022년 5월 6일
0

https://ko.wikipedia.org/wiki/%EC%9D%98%EC%A1%B4%EC%84%B1_%EC%A3%BC%EC%9E%85

소프트웨어 엔지니어링에서 의존성 주입(dependency injection)은 하나의 객체가 다른 객체의 의존성을 제공하는 테크닉이다. "의존성"은 예를 들어 서비스로 사용할 수 있는 객체이다. 클라이언트가 어떤 서비스를 사용할 것인지 지정하는 대신, 클라이언트에게 무슨 서비스를 사용할 것인지를 말해주는 것이다. "주입"은 의존성(서비스)을 사용하려는 객체(클라이언트)로 전달하는 것을 의미한다. 서비스는 클라이언트 상태의 일부이다. 클라이언트가 서비스를 구축하거나 찾는 것을 허용하는 대신 클라이언트에게 서비스를 전달하는 것이 패턴의 기본 요건이다.

의존성 주입의 의도는 객체의 생성과 사용의 관심을 분리하는 것이다. 이는 가독성과 코드 재사용을 높혀준다.

의존성 주입은 광범위한 역제어 테크닉의 한 형태이다. 어떤 서비스를 호출하려는 클라이언트는 그 서비스가 어떻게 구성되었는지 알지 못해야 한다. 클라이언트는 대신 서비스 제공에 대한 책임을 외부 코드(주입자)로 위임한다. 클라이언트는 주입자 코드를 호출할 수 없다. 그 다음, 주입자는 이미 존재하거나 주입자에 의해 구성되었을 서비스를 클라이언트로 주입(전달)한다. 그리고 나서 클라이언트는 서비스를 사용한다. 이는 클라이언트가 주입자와 서비스 구성 방식 또는 사용중인 실제 서비스에 대해 알 필요가 없음을 의미한다. 클라이언트는 서비스의 사용 방식을 정의하고 있는 서비스의 고유한 인터페이스에 대해서만 알면 된다. 이것은 "구성"의 책임으로부터 "사용"의 책임을 구분한다.
의존성 주입은 다음과 같은 문제를 해결한다.[1]

어떻게 애플리케이션이나 클래스가 객체의 생성 방식과 독립적일 수 있는가?
어떻게 객체의 생성 방식을 분리된 구성 파일에서 지정할 수 있는가?
어떻게 애플리케이션이 다른 구성을 지원할 수 있는가?
객체를 필요로하는 클래스 내에서 직접 객체를 생성하는것은 클래스를 특정 객체에 커밋하는 것이고 이후에 클래스로부터 독립적으로(클래스의 수정 없이) 인스턴스의 생성을 변경하는것이 불가능하기 때문에 유연하지 못하다. 이는 다른 객체를 필요로하는 경우 클래스를 재사용할 수 없게하며, 실제 객체를 모의 객체로 대체할 수 없기 때문에 클래스를 테스트하기 힘들게한다.

클래스는 더 이상 객체 생성에 대한 책임이 없으며, 추상 팩토리[2] 디자인 패턴에서처럼 팩토리 객체로 생성을 위임할 필요가 없다.

아래의 UML 클래스 및 시퀀스 다이어그램을 참고.
의존성 주입은 프로그램 디자인이 결합도를 느슨하게[6] 되도록하고 의존관계 역전 원칙과 단일 책임 원칙을 따르도록 클라이언트의 생성에 대한 의존성을 클라이언트의 행위로부터 분리하는 것이다.[7] 이는 클라이언트가 의존성을 찾기 위해 그들이 사용하는 시스템에 대해 알도록 하는 서비스 로케이터 패턴과 정반대되는 것이다.

의존성 주입의 기본 단위인 주입은 새롭거나 관습적인 메커니즘이 아니다. "매개변수 전달"과 동일하게 동작한다. 주입으로써 "파라미터 전달"은 클라이언트를 세부 사항과 분리하기 위해 수행되고 있는 부가적인 의미를 전달한다.

또한 주입은 전달을 제어하는 대상(클라이언트가 아님)에 대한 것이며 전달의 수행 방식-참조 또는 값 전달-과 독립적이다.

의존성 주입은 네 가지 역할을 담당하는 객체 및 인터페이스를 전제로 한다.

사용될 서비스 객체
사용하는 서비스에 의존하는 클라이언트 객체
클라이언트의 서비스 사용 방법을 정의하는 인터페이스
서비스를 생성하고 클라이언트로 주입하는 책임을 갖는 주입자
비유하자면,

서비스 - 전기, 가스, 하이브리드 또는 디젤 자동차
클라이언트 - 엔진에 상관 없이 동일하게 차를 사용하는 운전자
인터페이스 - 운전자가 기어와 같은 엔진의 세부 사항을 이해할 필요가 없도록 보장해주는 자동변속기
주입자 - 아이에게 어떤 차를 사줄지 결정하고 구매해준 부모
사용될 수 있는 모든 객체는 서비스로 여겨진다. 다른 객체를 사용하는 모든 객체는 클라이언트로 여겨진다. 이름은 객체가 무엇을 위한 것인지와 무관하며 한 번의 주입에서 객체가 하는 역할과 관련이 있다.

인터페이스는 클라이언트가 의존성으로 예상하는 타입이다. 쟁점은 무엇을 접근할 수 있게 하는가이다. 이는 실제로 서비스에 의해 구현된 인터페이스 타입일 수도 있지만 추상 클래스 또는 DIP[8]를 위반하고, 테스트를 하기위한 동적으로 결합도 낮추는것을 희생하긴 하지만 concrete 서비스 자체일 수도 있다. 클라이언트가 인터페이스를 구성하거나 확장하는 것처럼 구체적으로 다룰 수 없도록 알지 못하게 하는 것이 요구된다.

클라이언트는 의존성의 특정 구현에 대한 구체적인 지식이 필요 없다. 인터페이스의 이름과 API만 알면 된다. 결과적으로 인터페이스가 변경되더라도 클라이언트는 수정할 필요가 없어진다. 하지만 인터페이스가 클래스로 존재하다가 인터페이스 타입으로(그 반대의 경우에도) 리팩토링될 경우 클라이언트는 다시 컴파일이 필요할 수 있다. 이는 클라이언트와 서비스가 독립적으로 발행될 경우 중요하다. 이런경우에 결합도가 높아지는 문제는 안타깝게도 의존성 주입으로 해결할 수 없다.

주입자는 클라이언트에게 서비스를 소개한다. 종종 클라이언트를 생성하기도 한다. 주입자는 객체를 클라이언트와 같이 처리하고 나중에 다른 클라이언트의 서비스로 처리하여 매우 복잡한 객체 그래프를 함께 연결할수도 있다. 주입자는 실제로 함께 동작하는 많은 객체가 될 수 있지만 클라이언트는 될 수 없다. 주입자는 다음과 같은 많은 이름들로 참조될 수 있다: assembler, provider, container, factory, builder, spring, construction code, or main.

의존성 주입은 모든 객체가 구성과 동작을 분리하도록 요구하는 하나의 규율로 적용될 수 있다. 구성을 수행하는 DI 프레임워크에 의지하면 new 키워드의 사용을 금지하거나 덜 엄격하게 값 객체의 직접 생성을 허용할 수 있다.[9][10][11][12]

분류
역제어(IoC)는 DI보다 더 일반적이다. 간단하게 말하자면 IoC는 호출을 요구하는 대신 다른 코드가 호출할 수 있게 함을 의미한다. DI가 없는 IoC의 한 예로 템플릿 메소드 패턴이 있다. 여기서 다형성은 서브클래싱, 즉, 상속을 통해 달성된다.[13]

의존성 주입은 컴포지션을 통해 IoC를 구현하므로 종종 전략 패턴과 동일하지만, 전략 패턴의 의도는 객체의 수명동안 의존성을 교환할 수 있도록 하는 것이고, 의존성 주입에서는 단일 의존성 인스턴스만 사용할 수 있도록 하는 것이다.[14] 여전히 다형성은 유지되지만 위임과 컴포지션을 통해 이루어진다.

의존성 주입 프레임워크
CDI와 그 구현물인 Weld, Spring, Guice, Play framework, Salta, Glassfish HK2, Dagger, Managed Extensibility Framework(MEF)와 같은 애플리케이션 프레임워크는 의존성 주입을 지원하지만 필수는 아니다.[15][16]

장점
의존성 주입은 클라이언트의 구성 가능성을 유연하게 해준다. 클라이언트의 행위는 고정되어 있다. 클라이언트는 클라이언트가 기대하는 고유한 인터페이스를 지원하는 모든 것을 할 수 있다.
의존성 주입을 통해 시스템의 구성 세부 사항을 외부의 구성 파일에서 사용하여 리컴파일 없이 시스템을 재 구성할 수 있다. 분리된 구성은 컴포넌트의 여러 구현을 요구하는 다양한 상황을 위해 작성될 수 있다. 이는 국한되어 있지는 않지만 테스팅을 포함한다.
의존성 주입은 코드의 동작에서의 어떠한 변경도 요구하지 않으므로 리팩터링으로써 레거시 코드에도 적용할 수 있다. 클라이언트는 더 독립적이며 테스트에 속하지 않은 다른 객체를 가장하는 stubs 또는 모의 객체를 사용해 독립된 유닛 테스트가 더 쉬워지는 결과를 얻는다.
의존성 주입을 통해 클라이언트는 사용해야하는 모든 구체적인 구현에 대한 지식을 제거할 수 있다. 디자인 변경이나 결함의 영향으로부터 클라이언트를 독립하는데 도움을 주며, 이는 재사용성, 테스트가능성, 유지가능성을 향상시킨다.[17]
구조
UML 클래스 및 시퀀스 다이어그램

A sample UML class and sequence diagram for the Dependency Injection design pattern.[18]
의존성 주입의 이점
의존 관계 설정이 컴파일시가 아닌 실행시에 이루어져 모듈들간의 결합도를 낮출 수 있다.
코드 재사용을 높여서 작성된 모듈을 여러 곳에서 소스코드의 수정 없이 사용할 수 있다.
모의 객체 등을 이용한 단위 테스트의 편의성을 높여준다.
적용 유형
마틴 파울러는 다음과 같은 세 가지의 의존성 주입 패턴을 제시하였다. [19]

생성자 주입 : 필요한 의존성을 모두 포함하는 클래스의 생성자를 만들고 그 생성자를 통해 의존성을 주입한다.
세터(Setter)를 통한 주입 : 의존성을 입력받는 세터(Setter) 메소드를 만들고 이를 통해 의존성을 주입한다.
인터페이스(Interface)를 통한 주입 : 의존성을 주입하는 함수를 포함한 인터페이스를 작성하고 이 인터페이스를 구현하도록 함으로써 실행시에 이를 통하여 의존성을 주입한다.

문제점
개발을 하다보면 코드에 의존성이 생기기 마련입니다. 그럼 의존성은 무엇이고, 왜 생겨나는 걸까요?

아래의 코드를 봐주세요.

// Programmer.java
class Programmer {
private Coffee coffee;

public Programmer() {
	this.coffee = new Coffee();
}

public startProgramming() {
	this.coffee.drink(); // 일단 마시고 시작하자
    ...
}

}
위 코드와 같이 Programmer 클래스에서 startProgramming 함수가 호출되기 위해서는 Coffee 클래스를 필요로 합니다. 이것을 Programmer 클래스는 Coffee 클래스의 의존성을 가진다 라고 합니다.

이와 같이 코드를 설계하였을 때, 코드의 재활용성이 떨어지고, 위 예제에서 Coffee 클래스가 수정 되었을 때, Programmer 클래스도 함께 수정해줘야하는 문제가 발생합니다.

즉, 결합도(coupling)가 높아지게 됩니다.

DI(의존성 주입)를 해야 하는 이유
DI로 프로그램을 설계 했을 때, 다음과 같은 이점을 얻을 수 있습니다.

Unit Test가 용이해진다.
코드의 재활용성을 높여준다.
객체 간의 의존성(종속성)을 줄이거나 없엘 수 있다.
객체 간의 결합도이 낮추면서 유연한 코드를 작성할 수 있다.
만약 DI를 사용하지 않고 Coffee 클래스의 상속을 받은 Cappuccino나 Americano 클래스를 사용해야 한다면 다음과 같이 직접 수정해 줘야 합니다.

class Coffee {...} // interface로 설계할 수도 있다

// Coffee 클래스를 상속
class Cappuccino extends Coffee {...}
class Americano extends Coffee {...}

// Programmer.java
class Programmer {
private Coffee coffee;

public Programmer() {
	this.coffee = new Cappuccino(); // 직접 수정
    // 또는 
    this.coffee = new Americano(); // 직접 수정
}

...

}
극단적으로, 만약 Coffee 클래스를 사용하는 클래스가 100개라면 그 중 Cappuccino가 필요한 클래스가 있다면 직접 수정해줘야겠죠? (바로 야근 🔥)

이것은 굉장히 비효율적이라고 할 수 있습니다.

의존성 주입(DI)을 이용한다면 아래와 같이 할 수 있습니다.

// Programmer.java
class Programmer {
private Coffee coffee;

// 그 날 마실 커피를 고를 수 있게된 개발자
public Programmer(Coffee coffee) {
	this.coffee = coffee;
}

public startProgramming() {
	this.coffee.drink();
    ...
}

}
위와 같이 필요한(의존하는) 클래스를 직접 생성하는 것이 아닌, 주입해줌으로써 객체 간의 결합도를 줄이고 좀 더 유연한 코드를 작성할 수 있게됩니다.

즉, 한 클래스를 수정하였을 때, 다른 클래스도 수정해야 하는 상황을 막아줄 수 있습니다.

To Quickly Recap:
DI, 의존성 주입은 필요한 객체를 직접 생성하는 것이 아닌 외부로 부터 필요한 객체를 받아서 사용하는 것이다.
이를 통해 객체간의 결합도를 줄이고 코드의 재활용성을 높여준다.

의존관계를 인터페이스로 추상화하기
위 BurgerChef 예시를 보자. 지금의 구현에서는 HamBurgerRecipe만을 의존할 수 있는 구조로 되어있다. 더 다양한 BurgerRecipe를 의존 받을 수 있게 구현하려면 인터페이스로 추상화해야 한다.

다음의 코드에서 볼 수 있듯이, 다양한 버거들의 레시피에 의존할 수 있는 BurgerChef가 된다.

의존관계를 인터페이스로 추상화하게 되면, 더 다양한 의존 관계를 맺을 수가 있고, 실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.

그렇다면 Dependency Injection은?
의존관계가 무엇인지에 대해, 그리고 다양한 의존관계를 위해 인터페이스로 추상화함을 알아봤다. 그렇다면, Dependency Injection은 무엇인가?

지금까지의 구현에서는 BurgerChef 내부적으로 의존관계인 BurgerRecipe가 어떤 값을 가질지 직접 정하고 있다. 만약 어떤 BurgerRecipe를 만들지를 버거 가게 사장님이 정하는 상황을 상상해보자. 즉, BurgerChef가 의존하고 있는 BurgerRecipe를 외부(사장님)에서 결정하고 주입하는 것이다.

이처럼 그 의존관계를 외부에서 결정하고 주입하는 것이 DI(의존관계 주입)이다.

토비의 스프링에서는 다음의 세 가지 조건을 충족하는 작업을 의존관계 주입이라 말한다.

클래스 모델이나 코드에는 런타임 시점의 의존관계가 드러나지 않는다. 그러기 위해서는 인터페이스만 의존하고 있어야 한다.
런타임 시점의 의존관계는 컨테이너나 팩토리 같은 제3의 존재가 결정한다.
의존관계는 사용할 오브젝트에 대한 레퍼런스를 외부에서 제공(주입)해줌으로써 만들어진다.

  • 이일민, 토비의 스프링 3.1, 에이콘(2012), p114

DI 구현 방법
DI는 의존관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이다. 런타임 시점의 의존관계를 외부에서 주입하여 DI 구현이 완성된다.

Burger 레스토랑 주인이 어떤 레시피를 주입하는지 결정하는 예시로 설명하고자 한다.

생성자를 이용
메소드를 이용 (대표적으로 Setter 메소드)
DI 장점
그렇다면, DI, 의존 관계를 분리하여, 주입을 받는 방법의 코드 구현은 어떠한 장점이 있을까요?

  1. 의존성이 줄어든다.

앞서 설명했듯이, 의존한다는 것은 그 의존대상의 변화에 취약하다는 것이다.(대상이 변화하였을 때, 이에 맞게 수정해야함) DI로 구현하게 되었을 때, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게됨.

  1. 재사용성이 높은 코드가 된다.

기존에 BurgerChef 내부에서만 사용되었던 BurgerRecipe을 별도로 구분하여 구현하면, 다른 클래스에서 재사용할 수가 있다.

  1. 테스트하기 좋은 코드가 된다.

BurgerRecipe의 테스트를 BurgerChef 테스트와 분리하여 진행할 수 있다.

  1. 가독성이 높아진다.

BurgerRecipe의 기능들을 별도로 분리하게 되어 자연스레 가동성이 높아진다.

정리
DI(의존관계 주입)는 객체가 의존하는 또 다른 객체를 외부에서 선언하고 이를 주입받아 사용하는 것이다. 이를 구현함으로써 얻을 수 있는 장점들을 알아봤다.

자바와 관련된 서적이나, 스프링에 처음 입문하게 될 때, 자주 맞닥뜨리는 단어 DI. 용어의 늪에 빠지지 말고, 이 글을 통해 정리되었으면 한다.
https://tecoble.techcourse.co.kr/post/2021-04-27-dependency-injection/

https://mangkyu.tistory.com/150
https://mangkyu.tistory.com/125

https://medium.com/@jang.wangsu/di-dependency-injection-%EC%9D%B4%EB%9E%80-1b12fdefec4f

https://velog.io/@moongq/Dependency-Injection

profile
풀스택 웹개발자👩‍💻✨️

0개의 댓글