Dependency Injection

Dependency Injection 이하 DI는 코드 간의 결합도를 낮추고 유연성을 높이는 디자인 패턴 중 하나이다.

DI는 외부에서 객체간의 의존성을 결정해준다. 즉, 객체를 직접 생성하는것이 아닌 외부에서 생성 후 주입시켜주는 방식이다.

장점/단점

  • 장점
    • Unit Test 가 용이해진다.
    • 코드의 재사용성 증가
    • 객체간 의존성을 줄이거나 제거할 수 있음
  • 단점
    • 주입된 객체들의 코드 추적이 어려움.
    • 러닝커브가 높음.
    • 책임이 분리되는것을 지향하므로, 클래스수, 복잡성이 증가함.

DI 방법

  • Construct Injecton (생성자 주입) ← 많이쓰이고 권장되는 방법
  • Field Injection (필드 주입)
  • Setter Injection (세터 주입)

많이 쓰이는 방법은 생성자 주입 방법이고, 우리는 이 방법을 통하여 DI를 간단하게 알아보려한다.

Let’s DI - Non 3rd party

의존성 주입 패턴 사용전

1
2
3
class Member {
  AuthService service = AuthService();
}

위는 클래스 Member에 service라는 필드를 가지는 객체이다. 만약 AuthService에서 의존성이 추가가 되는 변경이 일어난다면 AuthService() 부분에서 컴파일 오류가 나게 될 것이다.

이때, service에서의 변경이 일어났지만 해당 객체를 의존하고 있는 Member도 영향을 미치게된다. 이럴때 우리는 Member는 AuthService를 의존한다 라고 한다.

의존성 주입 패턴 사용후

1
2
3
4
class Member {
	AuthService service;
	Member(this.service);
}

위의 클래스에서 AuthService를 의존하는건 동일하지만, 이 코드에서는 의존 대상을 직접 생성하는것이 아닌 외부로부터 주입받고 있어, 사용전과 같이 컴파일 오류는 발생하지 않는다.

현 시점으로서 AuthService의 인스턴스들의 상태 차이만 있을뿐이지, 만약 AuthService를 인터페이스로 추상화하면 다양한 구현체가 들어옴으로써, 의존성을 다각화 할 수 있다.

Flutter GetIt - 3rd party

What’s GetIt?

get_it | Dart package

GetIt 은 3rd Party 라이브러리로서, 우리가 하려는 DI를 좀더 손쉽게 관리할 수 있도록 도와주는 라이브러리이다. GetIt에서 제공하는 어노테이션으로 좀더 쉬운 모듈 등록을 지원한다.

특징

  • O(1)의 속도를 가지고 있음
  • 다른 타 DI에 비해 좀더 사용하기 쉬움. (러닝커브가 상대적으로 적음)
  • 데이터를 액세스하기위해 UI트리의 복잡화를 최소화 할 수 있음. (Context 의존적이지 않음)

Let’s DI again

GetIt 사용전

  • 구현체
1
2
3
4
class Member {
	AuthService service;
	Member(this.service);
}
  • 소비
1
2
3
4
5
class InjectionCore {
	Member injection() {
		return Member(AuthService());
	}
}

위의 InjectionCore 클래스에서 Member 클래스를 사용하려면, Member가 의존하고 있는 AuthService를 인스턴스화하여 주입해주어야 한다. 만약 AuthService의 의존성이 변경된다면, 위의 코드에서 AuthService의 의존성을 주입하는 데 어려움이 발생한다.

또한 InjectionCore에 모든 모듈들의 생성 로직이 포함되면 매우 비효율적이며, 코드 추적이 어려워진다.

그래서 우리는 GetIt을 사용하여, 주입하려는 객체가 어떻게 소비되어야 하고 어떻게 등록되어야 하는지 모르더라도 사용할 수 있게 도와줄 것이다.

GetIt 사용후

  • 구현체

GetIt 모듈 구현체 - 이전 예시의 InjectionCore역할

1
2
3
4
@module
abstract class ViewModelModule {
	Member Member(AuthService service) => Member(service); // 인젝션 모듈에 AuthService를 의존 등록후 프로바이딩
}

AuthService구현체

1
2
3
4
5
6
@injectable // 인젝션 모듈에 등록
class AuthService {
	void printText() {
		print("hello")
	}
}

Member구현체

1
2
3
4
5
6
7
8
class Member {
	AuthService service;
	Member(this.service); // service에 대해서 의존생성

	void consumeB() {
		service.printText();
	}
}
  • 소비
1
2
3
4
5
6
7
class DIConsumer {
	Member member = getIt<Member>(); // GetIt으로 제공받음으로서, Member가 의존하는 AuthService에 대한 존재를 몰라도됨.

	void consumeA() {
		member.consumeB();
	}
}

위의 예시에서는 2가지의 방법으로 DI에 등록하는 방법으로 나타냈다. 어노테이션을 사용하는 방법과 module 추상클래스를 생성하여 등록하는 방법. 2가지 모두 용법에 대해서 차이는 없어 사용하면서 팀의 핏이나, 개인의 간편함에 따라서 사용하면 좋다.

이전에 GetIt을 사용하기전보다 복잡성은 늘어났지만, 소비하는 의존도가 떨어져 본인이 소비하려는 객체의 의존성을 몰라도 되는 상황이 일어 났다. 따라서 소비하려는 DIConsumer클래스에서 Member에 AuthService를 넣기위해 더이상 인스턴스화를 안해도 된다. 이미 GetIt (외부)에서 생성되어 주입 되었기 때문이다.

심화

DI를 검색해보았거나, 알고있다면 IoC 제어역전에 대한부분을 알고 있을것이다. DI와 IoC를 동일시하게 인식하고있는 사람이 많은데 이 2개는 분명히 다르다.

IoC (제어역전)

IoC는 제어 흐름의 주체가 외부로 바뀌는 것을 의미한다. 일반적으로, 우리의 코드가 애플리케이션의 흐름을 제어한다. 그러나 IoC를 사용하면, 프레임워크나 라이브러리 등 외부 요소가 애플리케이션의 흐름을 제어하게 된다. 이로 인해 코드 간의 결합도가 낮아지고, 확장성과 유연성이 향상된다. 전체적으로 객체생성이 개발자가 아닌, 프레임워크와 같은 시스템에서 결정되어 생성이 되는것을 의미한다.

  • 제어역전 적용전
    • 객체 생성
    • 객체에서 의존성 객체 생성
    • 의존성 객체 호출
  • 제어역전 적용후
    • 객체 생성
    • 외부에서 의존성 객체 주입
    • 의존성 객체 호출

그래서 차이는?

DI는 IoC를 구현하기 위한 디자인 패턴이고 IoC는 외부에 제어권을 위임하는 설계 원칙이다.

As a result I think we need a more specific name for this pattern. Inversion of Controlis too generic a term, and thus people find it confusing. As a result with a lot of discussion with various IoC advocates we settled on the name Dependency Injection.

그 결과 이 패턴에 대해 좀 더 구체적인 이름이 필요하다고 생각한다. Inversion of Control은 너무 일반적인 용어이기 때문에 사람들은 그것을 혼동한다. 그 결과 다양한 IoC 옹호자들과 많은 논의를 거쳐 Dependency Injection이라는 이름을 정했다

  • 마틴 파울러 - Martin Fowler

객체지향적으로, 심화접근

GetIt 모듈 구현체

1
2
3
4
@module
abstract class ViewModelModule {
	MemberInterface member(AuthService service) => Member(service);
}

AuthService구현체

1
2
3
4
5
6
@injectable
class AuthService {
	void printText() {
		print("hello")
	}
}

Member구현체

1
2
3
4
5
6
7
8
9
class Member implements MemberInterface {
	AuthService service;
	Member(this.service); // AuthService 에 대해서 의존생성

  @override
	void consumeB() {
		service.printText();
	}
}

Member추상클래스

1
2
3
abstract class MemberInterface {
	void consumeB();
}
  • 소비
1
2
3
4
5
6
7
class DIConsumer {
	MemberInterface member = getIt<MemberInterface>();

	void consumeA() {
		member.consumeB();
	}
}

위와 같이 각 레이어 사이 DI를 제공할때, Interface를 둔다면 좀더 객체지향적으로 캡슐화, 은닉화를 가져갈 수 있을것이다. 무엇보다도 Interface를 둔다면 처음에 언급했던 다형성도 유연하게 구현할 수 있을것으로 보여진다.

https://pub.dev/packages/get_it

https://martinfowler.com/articles/injection.html