관리 메뉴

나만을 위한 블로그

[Android] UseCase에서 invoke를 사용하는 이유 (with 클린 아키텍처) 본문

Android

[Android] UseCase에서 invoke를 사용하는 이유 (with 클린 아키텍처)

참깨빵위에참깨빵 2023. 11. 12. 22:10
728x90
반응형

안드로이드 관련한 내용을 찾아보다가 아래 포스팅을 발견했다.
 
https://mashup-android.vercel.app/mashup-11th/heejin/useCase/useCase/

Clean Architecture - Use case in Android"  | 매쉬업 안드로이드 개발자

Clean Architecture의 UseCase가 뭐죠?! 안드로이드에선 어떻게 쓰나요?

mashup-android.vercel.app

 
MVVM 바탕의 클린 아키텍처 관련 포스팅인데, usecase를 특이하게 사용한 코드에 눈이 갔다.
 

interface GetCurrentUserUseCase {
    operator fun invoke(): Result<User>
}

class GetCurrentUserUseCaseImpl(
    private val userRepository: UserRepository
) : GetCurrentUserUseCase {
    override fun invoke(): Result<User> = userRepository.getUser()
}

 
그리고 뷰모델에선 아래와 같이 사용했다.
 

class TransactionsViewModelImpl(
    private val getUserTransactionsUseCase: GetUserTransactionsUseCase
) : TransactionsViewModel, ViewModel() {

    private val compositeDisposable = CompositeDisposable()
    ...

    override fun loadTransactions() {
        setLoadState()
        getUserTransactionsUseCase()
            .subscribeBy {
                handleResult(it)
            }.addTo(compositeDisposable)
    }
    ...
}

 
어떻게 이렇게 사용할 수 있을까? 보통은 usecase라는 클래스에 foo() 함수를 정의했다면, 뷰모델에선 생성자 파라미터로 usecase의 객체를 넘겨받고, 뷰모델에 정의한 함수 내에서myUsecase.foo() 형태로 사용하는 게 일반적인 형태일 것이다.
하지만 위 코드에선 usecase의 이름을 그대로 함수처럼 사용하고 있다. 이게 어떻게 가능한 일일까? 이게 가능한 이유를 찾으려면 operator fun, invoke 키워드가 무엇이고 무슨 역할을 하는지 찾아보면 좋을 것 같아 찾아봤다.
 
먼저 operator fun에 대한 내용이 실린 코틀린 공식문서부터 확인한다.
 
https://kotlinlang.org/docs/operator-overloading.html

Operator overloading | Kotlin

kotlinlang.org

코틀린을 쓰면 타입에 대해 사전 정의된 연산자 세트에 대한 사용자 정의 구현을 제공할 수 있다. 이런 연산자에는 사전 정의된 기호 표현(+, *)과 우선순위가 있다...(중략)...연산자를 오버로드하려면 해당 함수를 operator 수정자로 표시하라
interface IndexedContainer {
    operator fun get(index: Int)
}
연산자 오버로드를 재엉의할 때 연산자를 생략할 수 있다
class OrdersList: IndexedContainer {
    override fun get(index: Int) { /*...*/ }
}

 
아래는 invoke 연산자와 관련하여 식(expression)이 어떻게 변환되는지 보여주는 표다.
 

 
아래부터는 operator fun과 invoke에 대해 찾아본 내용들이다.
 
https://stackoverflow.com/a/66785806

How to write a use case that retrieves data from Android framework with Context

I am migrating an application to MVVM and clean architecture, and I am missing one part of the puzzle. The problem domain: List all applications on device and display them in the Fragment / Activit...

stackoverflow.com

Koin을 통해 DI를 수행할 경우, usecase를 함수에서 클래스로 변환한다. 이것은 DI를 허용한다...(중략)...usecase는 재사용할 수 있는 기능을 사용하기 위한 게 아닌가? 여기서 operator fun이라는 걸 활용할 수 있다
class ListAllApplications(private val context: Context) {
  operator fun invoke(): List<DeviceApp> {
    val ans = mutableListOf<DeviceApp>()

    val packageManager: PackageManager = context.applicationContext.packageManager
    val packages = packageManager.getInstalledApplications(PackageManager.GET_META_DATA)
    for (applicationInfo in packages) {
        val packageName = applicationInfo.packageName
        ans.add(DeviceApp(packageName))
    }

    return ans
  }
}
invoke는 클래스의 인스턴스가 마치 함수인 것처럼 호출될 수 있게 하는 특수 함수다. 주입 가능한 생성자를 써서 클래스를 함수로 변환한다. 이를 통해 표준 함수 호출 구문을 써서 뷰모델에서 usecase를 계속 호출할 수 있다
class MyViewModel(private val listAllApplications: ListAllApplications): ViewModel {
  init {
    val res = listAllApplications()
  }
}

 
https://www.baeldung.com/kotlin/operator-overloading#10-invoke

코틀린 및 기타 언어에서 "함수이름(매개변수)" 구문을 통해 함수를 호출할 수 있다. invoke operator 함수를 써서 함수 호출 구문을 모방하는 것도 가능하다. 예를 들어 page[0] 대신 page(0)으로 첫 번째 요소에 접근하려면 아래의 확장 함수를 선언하고, 이를 써서 특정 페이지의 요소를 검색할 수 있다
operator fun <T> Page<T>.invoke(index: Int): T = elements()[index]

assertEquals(page(1), "Kotlin")
여기서 코틀린은 괄호를 적절한 개수의 매개변수를 사용해 invoke()에 대한 호출로 변환한다. 또한 원하는 수의 매개변수를 써서 invoke 연산자를 선언할 수 있다

 
https://kt.academy/article/kfde-operators

Operator overloading in Kotlin

How are operators defined for types in Kotlin, and how can we define our own operators.

kt.academy

invoke 연산자는 클린 아키텍처의 람다식 또는 usecase 객체 같은 함수를 나타내는 객체에 사용된다
class CheerUseCase {
    operator fun invoke(who: String) {
        println("Hello, $who")
    }
}

fun main() {
    val hello = {
        println("Hello")
    }
    hello() // Hello

    val cheerUseCase = CheerUseCase()
    cheerUseCase("Reader") // Hello, Reader
}

 
결과적으로, operator fun과 invoke 키워드는 모두 연산자 오버로딩을 사용하기 위한 키워드다.
이 키워드들을 사용하는 이유는 간결함과 가독성을 위해서인 것 같은데, 혹시 다른 이유가 있는지 궁금해서 찾아봤다.
 
https://stackoverflow.com/a/69411802

Why do we use invoke operator overloading in Kotlin Usecases?

I've started using use-cases in kotlin for clean coding practices but not clear why we use the invoke operator overloading. What's the point and why not call it execute() instead? interface

stackoverflow.com

invoke 연산자를 쓰면 아래의 이점들이 있다

- 계약(contract)이 없는 usecase에 의사 계약(pseudo-contract) 실행
- 더 자연스럽게 읽힘
- 호출의 유연성(loginUseCase() vs loginUseCase.invoke())

 
그렇다면 어떻게 사용할 수 있을까? 안드로이드 디벨로퍼에서 제공하는 코드 스니펫과, 이전 포스팅에서 사용했던 클래스를 조금 수정한 예시를 확인해 보겠다.
먼저 안드로이드 디벨로퍼에선 아래와 같이 말한다.
 
https://developer.android.com/jetpack/guide/domain-layer?hl=ko#use-cases-kotlin

도메인 레이어  |  Android 개발자  |  Android Developers

도메인 레이어 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 도메인 레이어는 UI 레이어와 데이터 레이어 사이에 있는 선택적 레이어입니다. 그림 1. 앱 아

developer.android.com

코틀린에서 operator 수정자와 같이 invoke()를 정의해서 usecase 클래스 인스턴스를 함수처럼 호출 가능하게 만들 수 있다
class FormatDateUseCase(userRepository: UserRepository) {

    private val formatter = SimpleDateFormat(
        userRepository.getPreferredDateFormat(),
        userRepository.getPreferredLocale()
    )

    operator fun invoke(date: Date): String {
        return formatter.format(date)
    }
}
위 예에서 FormatDateUseCase의 invoke()를 써서 클래스 인스턴스를 함수인 것처럼 호출할 수 있다. invoke()는 특정 시그니처로 제한되지 않는다. 매개변수를 개수 상관없이 취하고 모든 타입을 리턴할 수 있다. 개발자는 클래스의 서로 다른 시그니처로 invoke()를 오버로드할 수도 있다. 위 예시의 usecase를 호출하는 법은 아래와 같다
class MyViewModel(formatDateUseCase: FormatDateUseCase) : ViewModel() {
    init {
        val today = Calendar.getInstance()
        val todaysDate = formatDateUseCase(today)
        /* ... */
    }
}

 
마지막으로 내가 이전에 사용했던 코드를 바탕으로 어떻게 바꿀 수 있는지 확인해 본다. 코드는 이전에 코루틴 Flow에 대해 작성한 포스팅에서 가져왔다.
 
https://onlyfor-me-blog.tistory.com/478

[Android] Coroutine Flow란? MVVM + Flow + Retrofit 예제

최근 안드로이드 진영에서 비동기 처리에 자주 사용했던 라이브러리인 RxJava가 걷어내지고 그 자리를 코루틴의 flow라는 것이 대신한다고 들었다. 그래서 구글에서 Compose를 비롯해 여러 방식으로

onlyfor-me-blog.tistory.com

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import java.io.IOException

class GithubUseCase: BaseFlowResponse() {
    private val githubClient = ApiService.client

    operator fun invoke(queryString: String): Flow<ApiState<GithubData>> = flow {
        emit(flowCall { githubClient.getRepositories(queryString) })
    }.flowOn(Dispatchers.IO)
}

 
기존에 작성했던 getRepositories()의 로직을 재활용해서 invoke()를 작성했다.
다음은 이 클래스를 사용하는 뷰모델의 구현이다.
 

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.launch

class GithubViewModel(
    private val githubUseCase: GithubUseCase
): ViewModel() {
    var mGithubRepositories: MutableStateFlow<ApiState<GithubData>> = MutableStateFlow(ApiState.Loading())
    var githubRepositories: StateFlow<ApiState<GithubData>> = mGithubRepositories

    fun requestGithubRepositories(query: String) = viewModelScope.launch {
        mGithubRepositories.value = ApiState.Loading()
        githubUseCase(query)
            .catch { error ->
                mGithubRepositories.value = ApiState.Error("${error.message}")
            }
            .collect { values ->
                mGithubRepositories.value = values
            }
    }
}

 
조금 더 간결해지기는 했지만, 원래 예제 수준의 코드였기 때문에 큰 장점은 느끼지 못하겠다.
하지만 "객체명.함수명()" 형태가 아닌 "객체명()" 형태로도 그 객체의 함수를 호출할 수 있다는 것은 매력적으로 다가올 수 있는 요소다.

반응형
Comments