DI란 Dependency Injection의 약자로 의존성 주입을 의미한다. 의존성 주입은하나의 객체가 다른 객체의 의존성을 제공하는 기술이다. 비유하자면 '의존성'은 서비스로 사용할 수 있는 객체이고 '주입'은 의존성(서비스)을 사용하려는 객체로 전달하는 것을 의미한다. DI는 프로그래밍에 널리 사용되는 기법으로, DI의 원칙을 따르면 훌륭한 앱 아키텍처를 위한 토대를 마련할 수 있다.
DI를 클래스들로 예를 들어 설명하자면, Car 클래스와 Engine 클래스가 있는 경우 Car 클래스가 실행되기 위해서는 Engine 클래스의 인스턴스가 있어야 한다. 이러한 필요한 클래스(Engine)를종속 항목(=의존성)이라고 한다. 클래스들은 흔히 다른 클래스 객체가 필요하다(의존적이다). 클래스가 필요한 객체를 얻는 방법은 다음 세 가지 방법이 있다.
1) 클래스가 필요한 종속 항목을 구성한다.
위 예에 적용하면 Car는 자체 Engine 인스턴스를 생성하여 초기화한다.
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
클래스들의 관계를 그림으로 나타내면 다음과 같다.
이미지 출처: 안드로이드 개발자 공식 문서
이러한 방법으로 클래스가 필요한 객체를 얻는 경우 Car와 Engine이 밀접하게 연결되어 있기 때문에 문제가 생길 수 있다. 우선,코드의 재사용이 어려워진다.Car 인스턴스는 자신이 생성한 한 가지 유형의 Engine만을 사용하기 때문에 Engine 유형이 Gas와 Electric 두 가지라면 하나의 Car 인스턴스를 재사용하는 대신 두 가지 유형의 Car를 생성해야 한다. 또한 이처럼 종속 항목이 강하게 의존하고 있는 경우다양한 테스트를 하기 어려워진다.Car가 실제 Engine 인스턴스를 사용하기 때문에 다양한 테스트 상황에서 Engine을 수정할 수 없게 된다.
2) 다른 곳에서 객체를 가져온다.
Context getter 혹은 getSystemService()와 같은 일부 안드로이드 API가 이러한 방식으로 작동한다.
3) 객체를 매개변수로 전달받는다.
앱은 클래스가 구성될 때 종속 항목을 제공하거나 각 종속 항목이 필요한 함수에 전달할 수 있다. 위 예에 적용하면 Car 인스턴스는 초기화 시 자체 Engine 객체를 구성하는 대신 Engine 객체를 생성자의 매개변수로 받는다.
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
클래스들의 관계를 그림으로 나타내면 다음과 같다.
이미지 출처: 안드로이드 개발자 공식 문서
이러한 방법을 사용하는 경우 앱은 Engine 인스턴스를 생성한 후 Car 인스턴스를 구성하기 때문에 다양한 유형의 Engine을 Car에 전달해Car의 재사용이 가능해지며다양한 시나리오를 테스트할 수 있다.
위의 세 가지 방법 중 이 세 번째 방법이 바로'의존성 주입(DI)'이다.
2. DI의 장점
위에서 예시를 살펴보면서 설명한 것처럼 DI를 구현하면 다음과 같은 장점이 있다.
코드 재사용 가능: 종속 항목 객체를 쉽게 교체할 수 있다.
리팩토링 편의성: 종속 항목은 구현 세부정보로 숨겨지지 않고 노출되어 있어 검증 가능한 요소가 되므로 객체 생성 또는 컴파일 시 확인할 수 있다.
테스트 편의성: 클래스가 종속 항목을 관리하지 않으므로 다양한 모든 사례를 테스트할 수 있다.
3. 안드로이드에서 DI(의존성 주입) 실행하기
안드로이드에서 의존성 주입을 실행하는 두 가지 주요 방법은 다음과 같다.
생성자 삽입: 위에서 설명한 방법으로, 클래스의 종속 항목을 생성자에 전달하는 방법이다.
필트 삽입(setter 삽입): Activity나 Fragment 같은 특정 안드로이드 프레임워크 클래스는 시스템에서 인스턴스화하기 때문에 생성자 삽입이 불가능하다. 따라서 필드 삽입을 사용해 클래스가 생성된 후 종속 항목을 인스턴스화한다. 코드는 다음과 같다.
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
4. 안드로이드의 DI 라이브러리
위의 예에서처럼 클래스의 종속 항목을 직접 생성, 제공, 관리하는 것을수동 의존성 주입이라고 한다. 위의 예에서 Car에 종속 항목이 하나만 있었기 때문에 수동으로 의존성을 주입하는 것이 가능했지만 종속 항목과 클래스가 많아지면 수동 의존성 주입을 사용할 때 문제가 발생할 수 있다. 따라서 종속 항목을 생성하고 제공하는 프로세스를 자동화해서 문제를 해결하는 라이브러리를 사용하는 것이 좋다. 안드로이드의 주요 DI 라이브러리는 다음과 같다.
Hilt
Dagger
Dagger는 구글에서 유지 관리하며 자바, 코틀린 및 안드로이드용으로 널리 사용되는 라이브러리이다. 컴파일 타임 정확성, 런타임 성능, 확장성 및 안드로이드 스튜디오 지원 등의 장점이 있다.Hilt는 Dagger를 기반으로 빌드된 Jetpack의 권장 라이브러리로, 프로젝트의 모든 안드로이드 클래스에 컨테이너를 제공하고 생명주기를 자동으로 관리해 앱에서 DI를 실행하는 표준 방법을 정의한다.
MVC, MVVM 패턴 등에 대한 검색을 했을 때 디자인 패턴이라고 하는 경우도 있고 아키텍처 패턴이라고 하는 경우도 있어서 둘의 차이가 무엇인지 궁금해서 알아보았다.
디자인 패턴: 소프트웨어 디자인에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결책을 말한다. 상황에 맞게 사용될 수 있는 문제들을 해결하는데에 쓰이는 템플릿을 의미한다. 프로그래머가 어플리케이션이나 시스템을 디자인할 때 공통된 문제들을 해결하는데에 쓰이는 형식화 된 가장 좋은 패턴이다.
아키텍처 패턴: 아키텍처 패턴은 디자인 패턴과 유사하지만범위가 더 넓다. 아키텍처 패턴은 소프트웨어 공학의 다양한 문제를 해결하는데, 예를 들어 컴퓨터 하드웨어 성능 제한, 비즈니스 위험의 최소화와 고가용성 등이 있다. 일부 아키텍처 패턴은 SW 프레임워크 안에 구현되어 있다. 아키텍처 패턴이 시스템의 이미지를 전달하기는 하지만 아키텍처 자체를 의미하는 것은 아니다. 다양한 아키텍처가 동일한 패턴을 구현하고 관련 특성을 공유할 수 있다.
출처: 위키백과 - 소프트웨어 디자인 패턴
간단하게 정리하자면 디자인 패턴과 아키텍처 패턴은 소프트웨어공학에서 발생하는 문제를 해결한다는 점에서 비슷하지만 디자인 패턴은 특정 문제를 해결하기 위한 방법이고, 아키텍처 패턴은 전체적인 소프트웨어에서 발생하는 문제들을 해결하기 위한 방법이라고 이해하면 될 것 같다.
또한 각 패턴들은 범위에 따라 디자인 패턴으로 사용될 수도 있고 아키텍처 패턴으로 사용될 수도 있으며, 안드로이드 분야에서 자주 언급되는 MVC, MVP, MVVM 패턴은 아키텍처 패턴이라고 하는 게 더 정확하다고 볼 수 있다.
2. 이러한 패턴들을 알아야 하는 이유
안드로이드 앱에는 activity, fragment, service, content provider, broadcast receiver 등 다양한 컴포넌트가 포함되며 사용자는 짧은 시간 동안 여러 앱과 상호작용하는 경우도 많다. 예를 들어 앱을 사용하는 도중 전화나 알림에 의해 사용 환경이 중단될 수 있고 사용자는 이 중단이 끝난 후(다른 앱에서 활동을 끝낸 후) 다시 작업을 이어갈 수 있기를 기대한다. 그렇기 때문에 앱에서 이러한 흐름을 올바르게 처리하기 위해 좋은 아키텍처 패턴을 적용하는 것이 중요하다.
클린 아키텍처는 의존성 규칙을 따라 관심사를 분리하고, 본질적으로 테스트하기 쉬운 시스템을 만들 수 있는 아키텍처 구조이다. 클린 아키텍처를 적용하면 변화의 위치가 명확하기 때문에 생산성을 증대시킬 수 있고 변화에 더 잘 대응할 수 있다.
레이어의 가장 바깥 쪽이 사용자와의 접점에 있는 Presentation이고 가장 안쪽의 Entities가 사용자가 실제로 생각하는 개념 단위이다. 클린 아키텍처에서는 서버 쪽 내용이지만 안드로이드에도 이 원리를 적용시켜 UI를 독립시키고, Database를 분리시키고, 외부적인 설정에 독립적인 구조를 적용하면 프레임워크에 의존적이지 않은 코드를 짤 수 있다.
각 레이어의 역할
1) Entities
애플리케이션에서 핵심적인 기능인 Business Rule들을 담고 있다.
Entity들은 outer layer들에 속한 다른 class나 component들에 대해 전혀 몰라도 된다.
2) Use cases
특정 애플리케이션에 대한 Business Rules
시스템이 어떻게 자동화될 것인지 정의하고 애플리케이션의 행위를 결정한다.
즉, 프로젝트 레벨의 Business Rules(Entities)를 사용하여 목적을 달성한다.
3) Adapters
domain과 infrastructure 사이의 번역기 역할
GUI로부터 입력을 받아 Use cases와 Entities에게 편리한 형태로 repackage한다.
GUI의 MVC 아키텍처를 완전히 내포하며, Presenter, View, Controller가 모두 여기에 속한다.
4) Infrastructure
모든 I/O components (UI, DB, frameworkds, devices)가 있는 곳
변화될 가능성이 매우 높기 때문에 stable한 domain과는 확실히 분리가 되어있고, 따라서 비교적 쉽게 변하고 component 또한 쉽게 교환된다.
2. 안드로이드에 클린 아키텍처 적용하기
결과적으로 4개의 계층을 재구성하여 3개의 계층으로 분리하는 것으로 일반화
각 계층이 자체 데이터 모델을 사용하므로 계층 간 독립성을 확보할 수 있다는 것이 중요
이때 계층 간 데이터 변환을 수행하기 위해Mapper가 필요
비즈니스 로직이란
하나의 프로젝트나 프로그램 중 업무와 관련된 처리를 하는 일부분을 뜻하는데 프로젝트를 하면서 데이터베이스에서 어떠한 자료를 가져와 웹에서 출력을 할 때 데이터베이스 연결, 통신 , 자료, 가공, 페이지 구성 등 여러 작업을 하지만 그중에서 사용자가 원하는 자료의 가공 부분을 의미 즉어떻게 데이터가 생성, 저장되고 수정되는지를 정의한 것
Domain Layer
비즈니스 로직을 포함하고 있으며, 이에 필요한Entity와Usecase,Repository를 포함
Domain 계층은 Presentation 계층, Data 계층과 어떠한 의존성을 맺지 않아야 합니다.
순수 코틀린 혹은 자바 코드로 구성되어 있어 다른 프레임 워크에서도 유연하게 사용이 가능
UseCase
각 기능 혹은 비즈니스 로직 단위로 구성
이름만 보고 어떤 기능을 수행하는지 짐작할 수 있어야 한다.
Presentation 계층의 UI에서 어떤 이벤트 혹은 동작에 의해 호출되는 방향으로 설계
Data계층에서 실제 데이터를 어떻게 가져올지 정의는 하지 않고, 해당 메서드를 호출하는 방식으로 데이터를 저장
Translator
Data 계층의 Entity를 Domain 계층의 Model로 변환
Repository
Domain 계층에서는 인터페이스로 정의하고, 실제 구현은 Data 계층에서 진행
Model
애플리케이션의 실질적 데이터가 여기에 구현
안드로이드와 의존성을 가지고 있어선 안되고, 다른 프로젝트에서도 동일하게 사용할 수 있는 클래스를 작성해야 함
Data Layer
Data 계층은Domain계층의 의존성을 갖는다
Repository
UseCase가 필요로 하는 데이터 저장, 수정 등의 기능을 제공
위의 Domain계층에서 설계한 Repository를 실제고 구현
DataSource
실제 데이터의 입출력을 진행
Room을 사용한다고 하면 Dao를 이 부분에서 접근하고 결과를 출력
Entity
Domain 계층의 Model과는 다르게, REST API의 요청/응답을 위한 JOSN, 로컬 DB에 저장하기 위한 테이블에 저장하기 위한 데이터를 저장.