현재 Jetpack Compose를 기반으로 안드로이드 앱 개발을 하고 있다. 그 과정에서 MVI Pattern + koin + viewModel + retrofit을 구현하다 보니 혼동이 오는 부분이 있어 확실하게 정리해두려한다.
가장 먼저 들었던 의문은 retrofit을 object로 사용하는 것이 아닌 koin으로 굳이 해야할까 하는 의문이었다.
통신하기 편한 건 그냥 싱글톤 object로 retrofit객체를 만들어두면 어디서나 간편하게 통신을 할 수 있다. 그리고 이에 더해 koin이란 건 의존성을 주입하는 즉, 클래스 내부에서 객체를 만드는 것이 아닌 다른 곳에서 주입받는 걸 의미하는데 애초에 retrofit이 다른 곳에서 주입 받아서 어떤 작용을 할 필요가 있을까 하는 생각에 도저히 이해가 안됐다ㅠ
더군다나 패턴, 아키텍처를 준수하며 이것저것 하려고 하니 머리 속이 복잡하고 더 어렵게 느껴지는 것 같기도 하다.
기존에 앱에서 MVI, Clean Architecture를 전제하에 viewModel, repository패턴을 기반으로 통신을 하려고 하니까 머리 속이 복잡해졌다.
그래서 기본적인 것부터 제대로 이해하고 가자는 생각이 들었다.
아예 새로운 프로젝트 시작.
복잡하게 생각하기 싫어서 그냥 일반 xml을 사용하는 코드로 작성!
파일의 구조

⚫Data
⚫DI
⚫network
⚫presentation
Data
data class Post(
val userId: Int,
val id: Int,
val title: String,
val body: String
)
api통신을 할 때 사용할 기본 model로 백엔드에서 설계한 모델을 그대로 구현하면 된다.
레포지토리 패턴을 이용하기 위해 만든 폴더!
레포지토리 패턴은 데이터와 앱 사이에 매개체? 정도로 데이터를 가져오고 조작하는 역할을 담당한다.
이유는 추상화(안에 뭐가 있는 진 모르겠는데 난 이거만 사용해서 통신하면 돼! 같은 느낌 ), 단일 진입점 제공(get이든 post든 상관없이 이걸 통해!) 등이 있겠다!
interface PostRepository {
suspend fun getPost(): Response<Post>
}
class PostRepositoryImpl(private val apiService: ApiService) : PostRepository {
override suspend fun getPost(): Response<Post> {
return apiService.getPost()
}
}
network
백엔드에서 지원하는 api들을 보고 코드를 작성하면 된다!
interface ApiService {
@GET("posts/1")
suspend fun getPost(): Response<Post>
}
retrofit에서 이 interface를 직접 구현한다. 즉 retrofit 내부에서 create로 interface를 직접 구현하여 retrofit이 apiService를 가지고 있게 된다 라고 생각하면 된다. 이 때 이 부분을 생각하면서 DI를 이용하는 게 맞겠다는 생각이 들었다.
📌아무튼 통신은 retrofit를 직접 사용하는 게 아닌 apiService를 통해서 진행한다!
DI
이 부분이 retrofit과 koin을 연결 시켜주는 부분이다..
우선 koin을 이용하기 위해서는 startKoin을 해서 초기화 해주는 게 필요하다!
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@MyApp)
modules(myModule)
}
}
}
여기서 주의할 점은 앱의 진입점인 Application 클래스를 상속 받고 onCreate를 override함으로 우리는 앱의 진입점에 MyApp의 onCreate부분이 실행되도록 구성하고 있다. 그런데
이것만이 아닌 manifest파일에서 application 부분에 android:name=".DI.MyApp"을 추가해줘야한다..
이 부분이 조금 이해가 안되는 게 애초에 Application클래스는 앱의 진입점인데 왜 mainfest에서 까지 수정을 해야하나 싶었다..찾아보니 이유는 아래와 같다.
Application 클래스는 안드로이드 애플리케이션의 전체 라이프사이클을 관리하는 클래스이며, 앱의 실행과 동시에 시스템에 의해 생성된다. 이 클래스는 애플리케이션의 전역적인 상태를 관리하거나 초기화 작업을 수행하는 데 주로 사용됩니다.
아무튼 코드를 계속 보자면..
val myModule = module {
single {
Retrofit.Builder()
.baseUrl("https://jsonplaceholder.typicode.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
single<PostRepository> { PostRepositoryImpl(get()) }
viewModel { MyViewModel(get()) }
}
🍪 single
🍪 PostRepository>
Koin에서 PostRepository 타입의 싱글톤 객체를 정의한다! 즉, 애플리케이션 전체에서 하나의 PostRepository 객체만 존재하게 된다.
이 내부에서는 PostRepositoryImpl 클래스의 인스턴스를 생성하는데, PostRepositoryImpl은 알다 싶이 class의 매개변수로 apiService가 필요하다! 이 때 apiService는 koin에게 자동으로 할당해달라고 요청하면 되는데 그게 get()이다!!
이를 통해 우리는 PostRepository의 구현체를 생성하여 싱글톤으로 관리할 수 있게 된다!
🍪 viewModel
module안에 viewModel이라고 선언해두면 Koin이 MyViewModel 클래스의 인스턴스를 생성하고 관리하게 된다!
presentation
이제 실제로 데이터를 조작해보자!
koin으로 준비해둔 retrofit의 apiService를 이용해서 통신을 진행하면 된다!
통신을 위해 해당 뷰모델은 매개변수로 apiService를 주입 받는다!
아까 전 module에 있던 get으로 koin이 자동으로 매개변수를 주입할 것이다!
LiveData를 이용하면 데이터를 관리하기 매우 편하다. 자동으로 데이터를 확인하고 계속 변경해주기 때문에 우리가 따로 처리할 부분이 조금 덜어지게 된다. 이를 위해 MutableLiveData를 만들고 외부에서는 이를 수정할 수 없고 내부에서만 수정할 수 있도록 외부용은 post, 내부는 _post로 변경 가능할 수 있게 해둔다!
이제 진~짜 통신을 시작하자.. viewModelScope.launch로 비동기 통신을 시작한다.
class MyViewModel(private val repository: PostRepository) : ViewModel() {
private val _post = MutableLiveData<Post>()
val post: LiveData<Post> = _post
fun fetchPost() {
viewModelScope.launch {
try {
val response = repository.getPost()
if (response.isSuccessful) {
_post.value = response.body()
}
else {
// E R R O R 코 드 . .
}
} catch (e: Exception) {
// E R R O R 코 드 . .
}
}
}
}
이제 진짜 끝이다~ 우리가 한 걸 보여주기만 하면 완성
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private val viewModel: MyViewModel by inject()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 버튼 클릭 시 데이터 가져오기
binding.btn1.setOnClickListener {
viewModel.fetchPost()
}
// LiveData를 관찰하여 데이터가 변경될 때 UI를 업데이트
viewModel.post.observe(this, { post ->
// post가 null이 아닐 때 UI 업데이트
post?.let {
binding.tv1.text = "User ID: ${it.userId}\n" +
"ID: ${it.id}\n" +
"Title: ${it.title}\n" +
"Body: ${it.body}"
}
})
}
}

첫 번째 이유. retrofit 내부에 ApiService가 create되는데, koin을 이용하면 retrofit 내부에 접근할 필요없이 바로 ApiService를 주입 받을 수 있다.
두 번째 이유. 레포지토리 패턴을 이용하였을 때 apiService를 주입 받기 쉽다.
세 번째 이유. 뷰모델이 레포지토리를 이용할 때도 이를 주입 받기 편하다.
좀 더 알아본 결과 아래와 같이도 정리할 수 있다!
의존성 관리: Koin은 의존성을 관리하는 데에 특화되어 있기에, 객체 간의 의존성을 명시적으로 정의하고 주입함으로써 코드의 유연성을 향상 시킬 수 있다
테스트 용이성: Koin은 의존성 주입을 사용하여 코드를 테스트하기 쉽다. 의존성을 주입하여 테스트용 객체를 제공할 수 있으므로, 유닛 테스트 및 통합 테스트를 보다 쉽게 수행할 수 있다.