일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 큐 자바 코드
- 안드로이드 유닛테스트란
- 서비스 vs 쓰레드
- Rxjava Observable
- ANR이란
- rxjava disposable
- 객체
- 서비스 쓰레드 차이
- 멤버변수
- 안드로이드 os 구조
- 자바 다형성
- android retrofit login
- 안드로이드 유닛 테스트 예시
- jvm이란
- 안드로이드 라이선스
- ar vr 차이
- 스택 큐 차이
- 2022 플러터 설치
- rxjava hot observable
- 2022 플러터 안드로이드 스튜디오
- 안드로이드 레트로핏 crud
- 플러터 설치 2022
- 스택 자바 코드
- android ar 개발
- 안드로이드 라이선스 종류
- 안드로이드 유닛 테스트
- 안드로이드 레트로핏 사용법
- jvm 작동 원리
- 클래스
- rxjava cold observable
- Today
- Total
나만을 위한 블로그
[Android] UseCase에서 invoke를 사용하는 이유 (with 클린 아키텍처) 본문
안드로이드 관련한 내용을 찾아보다가 아래 포스팅을 발견했다.
https://mashup-android.vercel.app/mashup-11th/heejin/useCase/useCase/
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 수정자로 표시하라
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
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
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
invoke 연산자를 쓰면 아래의 이점들이 있다
- 계약(contract)이 없는 usecase에 의사 계약(pseudo-contract) 실행
- 더 자연스럽게 읽힘
- 호출의 유연성(loginUseCase() vs loginUseCase.invoke())
그렇다면 어떻게 사용할 수 있을까? 안드로이드 디벨로퍼에서 제공하는 코드 스니펫과, 이전 포스팅에서 사용했던 클래스를 조금 수정한 예시를 확인해 보겠다.
먼저 안드로이드 디벨로퍼에선 아래와 같이 말한다.
https://developer.android.com/jetpack/guide/domain-layer?hl=ko#use-cases-kotlin
코틀린에서 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
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
}
}
}
조금 더 간결해지기는 했지만, 원래 예제 수준의 코드였기 때문에 큰 장점은 느끼지 못하겠다.
하지만 "객체명.함수명()" 형태가 아닌 "객체명()" 형태로도 그 객체의 함수를 호출할 수 있다는 것은 매력적으로 다가올 수 있는 요소다.
'Android' 카테고리의 다른 글
[Android] 딥링크로 url을 받았을 때 쿼리 파라미터에 & 기호가 여러 개 포함된 경우 처리 방법 (0) | 2023.11.13 |
---|---|
[Android] onNewIntent()란? (0) | 2023.11.13 |
[Android] 폴더블 기기 펼침 여부 확인하는 방법 (Jetpack WindowManager) (0) | 2023.11.08 |
[Android] WebView Cache 전략 설정 방법 (0) | 2023.10.22 |
[Android] 리사이클러뷰 UI test 작성하기 (0) | 2023.09.24 |