의존성 주입과 hilt

2024. 2. 19. 23:25- 안드로이드/kotlin

- 인터페이스 사용시

bind module 

- 빌더 패턴 사용시

provide module

 

 

의존 관계란 무엇인가?

객체지향에서 의존관계라는 것은 클래스들이 서로를 알고 있는지를 말하는 것입니다.

 

위의 코드를 보면 클래스 ViewModel에서 FirebaseRepository객체를 생성해서 사용하고 있습니다. 이렇게 ViewModel이 FirebaseRepository를 알고 있을때 ViewModel이 FirebaseRepository에 의존한다고 할수 있습니다.

 

 

위의 같은 경우는 FirebaseRepository는 ViewModel을 알고 있지 못합니다.

FirebaseRepository는 ViewModel을 의존 하지 않습니다.

 

 

여기서 FirebaseRepository는 파이어베이스에서 언어목록을 가져오는 기능을 하고 있습니다.

그런데 더이상 Firebase를 사용하지 않고 다른 저장소를 사용하게 되는 상황이 주어진다면

새로운 Repository 클래스를 만들어서 데이터를 불러오는 기능을 다시 만들어야 할것입니다.

 

이렇게 기능을 수정할때 무작정 새로운 클래스를 만들어서 기존에 사용 하던 클래스(FirebaseRepository)와 교체하게 되면 문제가 생길수 있습니다.

 

 

ViewModel에서 FirebaseRepository를 사용하였는데 아래 처럼 RemoteRepository로 바꿔야 한다고 가정해봅니다.

 

 

FirebaseRepository를 RemoteRepository로 교체하게 되면 FirebaseRepository의 사용하던 메소드들을 RemoteRepository의 메소드들로 다 바꿔줘야 하기 때문에 유지보수에 어려움이 생기고 오류가 생길 확률이 높아지고 생산성이 떨어지게 됩니다.

 

이런 문제들을 인터페이스를 사용하여 개선 할수가 있습니다.

Repository라는 인터페이스를 만들고 FirebaseRepository, RemoteRepository가 Repository를 구현하도록 강제하면

FirebaseRepository, RemoteRepository가 같은 메소드명을 사용하게 할수 있고 같은 기능을 하도록 코드를 짤수 있습니다. 다형성을 이용해서 안전하고 편하게 코드를 수정할수 있습니다.

 


 

하지만 ViewModel에서 FirebaseRepository와 RemoteRepository객체를 각각 직접 생성하여 사용하고 있습니다.

이경우에 새로운 Repository를 사용하도록 변경할 경우 코드를 직접 수정해야 하는 번거로움이 있습니다.

여기서 좀더 코드를 개선해보겠습니다.

 

 

 

 

 

위와 같이 Repository 객체를 생성자로 전달받아서 사용하도록 수정 하면 Repository 객체를 바꿀때마다 ViewModel의 코드를 수정하지 않아도 됩니다. 그냥 ViewModel에서 사용할 Repository 객체를 골라 넣어 주면 됩니다. 

 

이렇게 클래스에서 사용할 객체를 외부에서 만들어서 넣어주는게 바로 의존성 주입 입니다.

 

 

 

위의 코드를 보면 ViewModel은 FirebaseRepository를 사용하고 있습니다. 하지만 ViewModel은 FirebaseRepository에 의존하지 않고 Repository에 의존하고 있습니다. FirebaseRepository의 내용이 수정되어도 ViewModel의 코드는 영향을 받지 않습니다. 또 FirebaseRepository의 코드를 RemoteRepository로 교체를 하더라도 ViewModel의 코드는 영향을 받지 않습니다. 

 

개발중 수정될일이 많은 클래스에 직접 의존 하지 않고 비교적 안정적인 인터페이스에 의존하는 형식으로 하는것이 안정적입니다. 이렇게 되면 인터페이스를 수정하지 않는한 다른 클래스의 변경때문에 코드를 수정하지 않아도 됩니다.

 

 

 

많이 편리해진 코드이지만 아직 불편한 점이 남아있습니다. 위의 코드에서는 main() 함수에서 viewModel을 생성할때 Repository 구현체를 건내주게 됩니다. 하지만 그때그때 상황에 맞는 Repository를 건내주도록 main()함수의 코드를 수정하는것 또한 귀찮고 오류를 발생 시킬수도 있습니다. 또 프레임워크를 사용하는 경우에는 내마음대로 객체를 넘겨주는 코드를 작성하지 못할수도 있습니다. 

 

이런 불편한 상황들이 있기 때문에 의존성 주입 라이브러리를 사용합니다

 

 

Hilt 사용법

hilt 라이브러리를 배우기 전에 Dagger에 대해 알아야 합니다. 

Dagger는 안드로이드에서 사용할수 있는 의존성 주입 라이브러리 입니다. hilt는 Dagger를 기반으로 만들어졌습니다.

 

Dagger의 장점은 compile time에 의존성을 주입한다는 것입니다. 런타임이 아닌 컴파일 타임에 에러 체크를 할수 있고 런타임의 성능 또한 떨어지지 않습니다.

하지만 단점으로는 학습하기가 어려워 초보자들에겐 사용하기가 어렵다는 점입니다.

 

Dagger의 장점은 사용하되 어렵다는 단점은 보완하기 위해 hilt가 만들어졌습니다. 

 

안드로이드에서 Dagger를 사용하려면 안드로이드의 component의 생명주기에 맞춰 객체를 주입하도록 하는 코드를 개발자가 작성해야 합니다. hilt는 이 작업을 대신 처리해줌으로써 개발자가 작업하기 편하게 해줍니다. 

 

Dagger와 Hilt의 구성요소

  • module
    주입할 객체를 생성해서 component에게 전달해주는 역할을 합니다
  • component
    모듈이 생성해서 전달해주는 객체를 Inject에 주입해주는 역할을 합니다
    (android의 component와는 다른것, 인터넷 자료에서 container라고 부르기도 함)
  • Inject
    주입요청, 객체생성 방법을 Hilt에게 알려주는 역할을 합니다

 

 

먼저 힐트를 사용하려면 반드시 안드로이드의 Application() 클래스에 @HiltAndroidApp 어노테이션을 달아서 힐트가 사용할수 있도록 해야 합니다. 힐트가 안드로이드의 생명주기에 맞춰 동작하게 되는데 이에 필수적으로 Application() 객체가 필요 하기 때문입니다.

 

 

 

 

아래 코드를 보시면 MainActivity에서 MainViewModel을 주입 받고 있고 MainViewModel에서는 RemoteRepository를 주입받고 있습니다. MainViewModel과 RemoteRepository를 주입해야 하기 때문에 힐트에게 이 두 객체를 생성하는 방법을 알려줘야 합니다. 이 두 클래스의 생성자에 @Inject어노테이션을 달아주는 것으로 힐트에게 객체를 어떻게 생성하는지 알려줄수 있습니다. 생성자나 필드에 @Inject 어노테이션을 달아 놓으면 필요한 객체를 힐트가 생성해서 주입해줍니다. 

 

그런데 ManiActivity에서 MainViewModel을 주입받는 부분에는 @Inject 어노테이션이 없습니다. Activity나 Fragment에서 ViewModle을 주입받을 때는 Activity ktx , Frament ktx의 기능을 이용해서 아래의 방식처럼 viewModel을 주입받을수 있습니다. 또 ViewModel에는 @HiltViewModel 어노테이션이, Activity에는 @AndroidEntryPoint 어노테이션이 달려 있습니다.

힐트에게 ViewModel임을 알려주기 위해서, 안드로이드 컴포넌트(Activity)임을 알려주기 위해서 @HiltViewModel, @AndroidEntryPoint 어노테이션을 달아야 합니다.

 

위와 같은 방식은 @Inject 어노테이션으로 객체 주입을 요청하면 힐트가 객체를 생성해서 주입해준다고 했습니다. 하지만 이처럼 단순하게 처리 하지 못하는 경우도 있습니다. 바로 인터페이스 타입을 주입받는 경우 입니다. 앞에 나온 예시 코드 처럼 힐트가 객체 생성 방법을 @Inject 어노테이션으로만 유추해낼수 있는 경우에는 별도로 module을 정의해줄 필요가 없었습니다. 하지만 인터페이스의 경우에는 힐트가 객체를 주입할수 있도록 개발자가 bind module을 정의해 줘야 합니다. 

bind module은 정말 간단하게 정의 할수 있습니다. 아래코드를 보겠습니다. 

 

인터페이스 힐트 사용

 

bind module(@Module)을 정의하는 방법은 주입할 인터페이스를 구현한 클래스(RemoteRepository)를 추상클래스(MyModule)의 매개변수로 받고 인터페이스 타입으로 리턴하는 추상메소드(bindRepository)를 선언하기만 하면 됩니다. 메소드 이름은 무엇이 되든 상관 없습니다. bind module은 abstract class로 만들어야 하고 bind method는 abstract fun로 만들어야 합니다. @module 어노테이션과 @binds 어노테이션을 붙여서 bind module과 bind method 임을 표시해야 합니다.

 

이렇게 bind module을 정의 하면 힐트가 MainViewModel에 Repository 타입으로 RemoteRepository을 주입할수 있습니다.

앞전에 구성요소에서 설명했던 module은 주입할 객체를 생성해서 component에게 전달해준다고 하였는데 module을 component와 연결하기 위해 사용하는 것이 @InstallIn 어노테이션 입니다. 또 힐트는 안드로이드의 생명주기에 맞춰 의존성 주입을 해주는 라이브러리 입니다. 힐트가 어떻게 안드로이드의 생명주기에 맞춰서 동작 하는것 일까요?

힐트에 맞춰 미리 정의된 컴포넌트 들이 있고 이 컴포넌트 들이 안드로이드의 생명주기에 맞춰 생성되고 사라지면서 주입을 해주는 겁니다. 

 

아래의 표에서 힐트의 각 컴포넌트 들이 언제 생성되고 사라지는지 또 각각의 힐트 컴포넌트가 어떤 안드로이드의 구성요소를 담당 하는것인 확인 할수 있습니다. 상황에 맞는 컴포넌트들을 찾아서 @InstallIn 어노테이션으로 module을 연결하면 됩니다. 예시 코드에서는 ViewModel에 주입을 할것이기 때문에 module을 ViewModelComponent에 연결 하였습니다.

ViewModelComponent가 MainViewModel의 생명주기에 맞춰 동작 하면서 module에게 주입할 객체를 전달 받아서 주입을 해주게 됩니다. 또한 힐트의 컴포넌트들은 계층 구조로 되어 있어 상위 계층에 있는 컴포넌트를 통해 주입 받을수도 있습니다. 

 

 

builder 패턴 힐트 사용

 

이번에는 builder 패턴을 이용할때 힐트 사용법에 대해 알아보겠습니다. 이 경우에는 provide module을 사용하여 힐트에게 객체 생성 방법을 알려주게 됩니다. provide module은 abstract 클래스로 정의했던 bind module과는 달리 일반 클래스와 일반 메소드로 정의 합니다. provide method 에는 @Provides 어노테이션을 붙이고 주입할 객체를 생성해서 리턴하는 코드를 작성 하면 됩니다. 이때 객체생성에 다른 객체(OkHttpClient)가 필요 하다면 매개변수로 주입을 받아서 사용할수 있습니다. 지금 까지는 생성자 주입만 살펴 봤는데요 필드 주입으로 클래스의 필드에 직접 주입을 받을수도 있습니다. 이 경우에 필드에 @Inject 어노테이션을 붙여 주면 됩니다. 필드 주입은 private 필드에는 사용할수 없습니다. 

 

 

 

참조 

https://www.youtube.com/watch?v=wZn-zpwvxCU&list=PL1cbSxJHr3t3TEmDJSrSNqW3xCG7GrZTI&index=1