서론
요즘 Android 개발자에게 요구하는 기본 stack 중 하나인 의존성 주입에 대해 정리해보려 한다.
의존성 주입에 Dagger, Hilt, Koin 같은 다양한 라이브러리를 활용하는 것으로 알고 있는데, 라이브러리를 사용하기 전에 의존성 주입에 대한 개념부터 차근차근 정리해보려 한다.
의존성 주입이란?
의존성 주입은 객체 지향 프로그래밍(OOP)에서 사용되는 디자인 패턴 중 하나이다.
Android에서 의존성 주입(Dependency Injection)을 흔히 DI라고 부르는데, DI는 Class와 Class 간에 관계를 형성할 때 내부에서 생성하는 것이 아닌, 외부에서 주입하여 관계를 만드는 것을 의미한다.
인터페이스화를 통해 객체 변경에 대한 유연성을 증대시키며, 객체를 내부에서 생성하는 것이 아닌, Container에 생성하여 주입하는 것을 의미한다.
위 관계에서 볼 때 StrengthTaining가 Excercise에 속해있다, 즉 의존 관계에 있다고 표현할 수 있다.
이때 만약 Excercise가 변경된다면 StrengthTaining이 함께 변경될 수도 있는데, Excercise가 변경된 영향으로 다시 Excercise가 변경되는 경우를 순환참조라고 한다. 이는 유지보수를 어렵게 만든다.
이 순환 참조를 깨기 위해 의존 역전 원칙이 필요하고, 이를 의존성 주입이라고 한다. 의존성 주입을 사용하게 되면 유지보수도 편안해지고, 테스트도 쉬워진다는 장점을 갖는다.
개발자가 의존성 주입을 개별적으로 구현하기는 까다롭기 때문에 Dagger, Hilt, Koin 등과 같은 라이브러리를 많이 사용한다.
순환 참조란, A 클래스가 B 클래스의 객체를 주입받고, B 클래스가 A 클래스의 객체를 주입받는 경우인데 서로 순환되어 참조할 경우 발생하는 문제를 의미한다.
의존성 주입의 장점
- 코드 재사용 가능
- 클래스 간의 결합도가 약해져 리팩토링이 편해짐
- 테스트 편의성
- 생명주기 별로 Container를 관리할 수 있게 된다면 리소스의 낭비를 막을 수 있음
의존성 주입 실현 방식
의존성 주입에는 정해진 방법이 있는 게 아니므로 필요한 방식을 선택하면 됨
⭐생성자 주입 방식
- 클래스를 초기화하는 시점에 외부에서 작성한 클래스를 주입하는 방식
- 필요한 모든 의존 객체를 생성하는 시점에 준비 가능
- 생성 시점에 의존 객체가 정상인지 아닌지 판별 가능
⭐메소드 주입 방식
- 클래스 초기화가 끝난 뒤 외부 클래스의 메소드를 실행시켜 객체를 주입하는 방식
⭐인터페이스를 통한 주입 방식
- 메소드 주입 방식과 유사하지만 인터페이스를 통해 의존성을 주입하는 방식
- 의존 객체가 나중에 생성되는 경우에 사용 가능
- 메소드의 이름을 통해 어떤 의존 객체를 주입하는지 더 알기 쉬움
⭐서비스 로케이터 방식
- 로케이터 클래스에 모든 의존 객체를 모으고, 로케이터에서 각 클래스에 의존성 주입
- 동일한 의존 객체를 여러 클래스에서 사용해야 할 경우, 제공하는 메소드를 각 객체의 수만큼 준비해야 함
- 의존성에 문제가 있어도 컴파일 타임에 확인 불가능
- 한계 : 인터페이스 분리 원칙을 위반하게 됨
Sample Code
fun main(args: Array<String>) {
val exercise = Exercise()
exercise.workOut()
}
class Exercise {
private val strengthTraining = StrengthTraining()
fun workOut() {
strengthTraining.start()
}
}
class StrengthTraining {
fun start() {
println("StrengthTraining Start")
}
}
위의 코드를 보면, Exercise 클래스의 변수에 StrengthTraining 클래스를 포함시켰다.
그럼 자연스럽게, 의존 관계가 형성되어 Exercise 클래스의 workOut 메소드를 실행시키게 되면 StrengthTraining 클래스의 start 메소드의 내용인 StrengthTraining Start를 출력하게 된다. 실제로 디버깅해보면 정상적으로 수행될 것이다.
만약, 사용자가 근력 운동이 아닌 유산소 운동을 기록한다면 어떻게 해야 할까? 아마 클래스를 변경해 준다거나, 메소드를 변경해 주는 등 수정을 해줘야 할 것이다. 이러한 코드의 수정이 위의 예시처럼 한 줄이라면 크게 문제가 되지 않지만 10줄, 100줄이라면 유지보수에 어려움이 있을 것이고, 코드의 안정성도 같이 떨어지게 된다.
문제점 해결을 위해 의존성 주입(클래스 외부에서 객체를 생성하여 해당 객체를 내부 클래스에 주입)의 개념이 필요하다.
fun main(args: Array<String>) {
val exercise = Exercise()
exercise.workOut()
}
interface Training {
fun start()
}
class Exercise {
//private val training = StrengthTraining()
private val training = AerobicTraining()
fun workOut() {
training.start()
}
}
class StrengthTraining : Training {
override fun start() {
println("StrengthTraining Start")
}
}
class AerobicTraining : Training {
override fun start() {
println("AerobicTraining Start")
}
}
위처럼 interface를 활용하게 된다면 기존 객체가 변경되든, 새로운 객체가 추가되든 유지보수에 대한 부담이 줄어들 것이다. 하지만, 메소드 변경은 간단하게 한 줄만 수정하면 해결되지만 만약 클래스 자체를 변경해야 된다면 클래스 전체를 변경한 후 객체화하는 모든 코드를 수정해야 할 것이다. 위의 예시에는 한 줄 뿐이지만, 객체화하는 소스가 100줄이라 생각하면.. 벌써 어지럽다..
즉, 의존성 주입은 자주 변경될 수 있는 코드에 직접 의존하지 않고, 인터페이스나 컨테이너를 활용해 의존성을 역전시킨 뒤 코드의 유지보수를 보다 효율적으로 할 수 있도록 해주는 것이다. 컨테이너(모듈)를 생성하여 의존성 주입을 하는 방법은 라이브러리(Hilt, Dagger, Koin 등..)를 활용하면 보다 간편하게 할 수 있다.
마치며
MVVM 디자인 패턴과 Clean Architecture 관련하여 공부하고 있는데, DI에 대한 개념이 추가되면 좋은 구조가 나올 것 같아서 개념 정리부터 시작하였다. 아직 DI에 대해 완벽하게 이해하고 있는 수준은 아니라서 추가적으로 공부하려고 한다. 또한, Android에서는 요즘 Hilt라는 라이브러리를 활용하여 DI를 적용한다고 하니 다음 포스팅 때 작성해보려고 한다.
화이팅😋
Reference
Android의 종속 항목 삽입 | Android 개발자 | Android Developers
Android의 종속 항목 삽입 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 종속 항목 삽입(DI)은 프로그래밍에 널리 사용되는 기법으로, Android 개발에 적합합니
developer.android.com