24.01.09 Invoke와 UseCase

KSang·2024년 1월 9일
0

TIL

목록 보기
29/101

Invoke와 UseCase

Invoke 인보크

참고

Invoke란?

이름 없이 간편하게 호출될 수 있는 함수

예시를 들면

class Test{
    operator fun invoke(str: String){
        print(str)
    }
}

fun main(){
    val test = Test()
    test("wow")
}

여기서 인보크가 아닌 다른 함수라면 Test클래스에 있는걸 사용하기위해 보통 Test.함수명(str) 형태로 하지만

인보크 함수는 Test(str)과 같이 함수 이름을 생략하면서 가능함

operator

선언부를 보면 operator가 붙어 있음

operator ( 연산자 )
코틀린은 이름을 부여한 함수임에도 불구하고 실행을 간편하게 할 수 있는 연산자라는 것을 제공한다. 이러한 연산자의 예시로는 +, - 부터 invoke까지 있는데, 이러한 연산자를 overloading할 수 있도록 제공하는 키워드가 바로 operator

예시

object Sample {
    operator fun plus(str: String):String {
        return this.toString() + str
    }
}

main() {
    Sample + " Hello~!" // [Sample의 주소값] Hello~!
}

실행해보면 [Smaple의 주소값]부분에는 실제로 주소값이 들어가고, 그 뒤에 바로 Hello~!라는 글자가 붙을 것이다. 즉 plus라는 이름으로 함수를 만들었지만 plus는 코틀린에서 연산자로 정의해 놓았으므로 plus연산자를 호출하기 위해 틀별히 + 기호를 사용해서 호출한다.

람다는 invoke 함수를 가진 객체

코틀린은 람다를 지원함 예를 들면

val toUpperCase = { str: String -> str.toUpperCase() }

모든 값, 또는 값을 담는 변수에는 타입이 있다. Int일 수도, String일 수도, 또는 우리가 만든 클래스 타입일 수도 있다. 그렇다면 위의 toUpperCase는 타입이 무엇일까?
Int도, String도 아니다. String을 받고, 다시 String을 반환하는 (String) -> String 타입이다.

더 정확히 따지면? 위 타입은 코틀린 표준 라이브러리에 정의된 Function1<P1, R>인터페이스 타입,
Function<P1, R>의 구현을 살펴보면 invoke(P1) : R 연산자 하나만 달랑 존재한다. 위에서 언급했듯이 invoke연산자는 이름 없이 호출할 수 있다.

val toUpperCase = object : Function1<String, String> {
    override fun invoke(p1: String): String {
        return p1.toUpperCase()
    }
}

UseCase

앞서 작성한 뷰모델 팩토리에서도 간단히 언급한 UseCase가 대체 뭘까?

usecase는 만들고 있는 서비스를 사용하는 유저가 이 서비스를 통해 하고자 하는 것을 말함

예를 들어 편의점에 가면 어떤 서비스가 있을까? 물건 구매, 교통 카드 충전,ATM기 이용, 택배편의점 이라는 시스템에 저 행동들이 각각 사용자가 요청할 수 있는 최소 요청 단위를 UseCase라고 볼 수 있다

그래서 전에 작성한 UseCase에서 오류가 있었는데 하나의 유즈케이스에서 여러가지 비즈니스 로직을 넣었음

근데 그렇게도 가능은하지만 좋은 아키텍처를 위해선 각 UseCase가 하나의 명확한 역활을 수행하도록 설계하는 것이 일반적임

여러 로직을 UseCase가 가지게 되면 단일 책임 원칙(Single Responsibility Principle)에 위배됨

또한 여러기능을 가지면 코드의 복잡성이 높아지고 테스트와 유지보수가 어려워지고, 각기능이 서로에게 영향을 미칠 가능성이 높아져, 하나의 기능에 문제가 생기면 다른 기능에도 영향을 미칠 수 있음

그렇기때문에 usecase는 하나의 명확한 역할을 수행하도록 설계하고 여러 UseCase를 조합해 복잡한 로직을 구현하는게 좋다

그래서 Invoke와 UseCase를 같이 사용해서 해결할 수 있다

Useless Use Cases

유즈 케이스 코드에 레포지토리 메소드 호출 코드만 있을때도 굳이 유즈 케이스를 써야할까?

써야된다고하는데 써야되는 이유를 알아보자

  1. 일관성
    일부 뷰모델만 유즈케이스를 호출하고 다른 뷰모델은 레포지토리를 직접 호출한다면 일관성이 없기 때문에
    처음 본 사람은 이해하기 힘들 수 있다
  2. 후에 어떻게 될지 모름
    클린 아키텍켜의 중요한 목적 중 하나는 요구사항이 변경될 때 쉽게 적응할 수 있는 코드 베이스를 제공하는 것
    그렇기 때문에 변경해야 하는 코드의 양이 최소여야함
  3. Screaming architecture
    Screaming architecture는 코드 구조로, 소프트웨어의 구조만 봐도 어떤 서비스인지 알 수 있어야한다고 함
    그렇기 때문에 명확해야하고, 이때문에 usecase를 써서 확실한 구조를 가져야 하는 것

Usecase에서 invoke를 사용하는 이유

다시 돌아와서 인보크로 어떻게 유즈케이스를 조합 할 수 있냐?를 정리하면

인보크는 객체를 함수처럼 호출 할 수 있으니, 이를 이용하면 UseCase의 메인 메소드를 invoke로 정의하고, UseCase를 함수처럼 호출할 수 있다.

예를 들어, LoginUseCase라는 UseCase가 있고 invoke메소드를 가지고며, 이 메소드는 사용자의 로그인을 처리한다고 치자

class LoginUseCase(private val userRepository: UserRepository) {
    suspend operator fun invoke(username: String, password: String): User {
        return userRepository.login(username, password)
    }
}

이제 이 UseCase는 함수처럼 호출될 수 있다

val loginUseCase = LoginUseCase(userRepository)
val user = loginUseCase("username", "password")

상당히 직관적으로 사용 할 수 있다.
이걸 뷰모델에서 구현하려고하면

class UserViewModel(private val loginUseCase: LoginUseCase, private val registerUseCase: RegisterUseCase) : ViewModel() {
    
    fun login(username: String, password: String) {
        viewModelScope.launch {
            val user = loginUseCase(username, password)
            // 로그인 결과를 처리하는 코드
        }
    }

    fun register(username: String, password: String) {
        viewModelScope.launch {
            val user = registerUseCase(username, password)
            // 회원가입 결과를 처리하는 코드
        }
    }
}

이런식으로 파라미터로 받아서 구현 할 수 있다

이렇게하면 UseCase를 재사용하고 테스트하기 좋고 코드도 깔끔하며 직관적으로 됨

그런데 UseCase를 여러개를 써야한다면??

여러개의 UseCase를 써야할 때 방법

UseCase를 전부 뷰모델에 집어 넣을 수 도 있다, 근데 그렇게되면 ViewModel이 복잡해지고 관리하기가 어려워 지기마련
이런 경우에는 다음과 같은 방법을 고려해볼 수 있다

  1. UseCase Grouping
    관련있는 UseCase들을 그룹화해서 하나의 큰 UseCase로 만들기
    서로 관련이 있거나, 서로 의존성을 가질 때 유용하게 사용 될 수 있다
    뷰모델에 주입해야하는 UseCase의 수가 줄어들어 복잡도가 감소하며
    중복코드를 제거하고 코드를 재사용 할 수도 있다
    비즈니스 로직의 일관성또 한 유지하는데 도움을준다
    하지만 테스트를 하기 어려워지고
    하나의 객체에 다양한 로직이 있게되면 단일책임의 원칙을 위배 할 수 있게됨

  2. UseCase Provider
    유즈케이스를 제공하는 역할을 하는 객체를 의미함
    이를 이용하면 여러 usecase를 효과적으로 관리하고 필요에 따라 쉽게 사용가능
    UseCase Provider를 사용하면 뷰모델이 직접 유즈케이스를 가지고 있지 않아도된다
    뷰모델은 필요한 유즈 케이스를 프로바이더로 부터 가져와서 사용하고, 이렇게 하면 뷰모델과 유즈케이스 사이의 결합도를 줄이며 뷰모델의 유연성이 향상된다
    재사용성또한 뛰어나며 유즈케이스의 생명주기를 효과적으로 관리 할 수 있다
    그러나 UseCase Provider를 도입하면 Provider를 결국 정의해야하고 관리해야하며
    테스트또한 힘든데 usecase의 인스턴스를 동적으로 생성하는 경우에는 모킹(Mocking)등의 추가적인 테스트 기법이 필요함

  3. Dependency Injection
    DI는 클래스간의 의존성을 외부에서 주입하는 디자인 패턴
    이를 통해 클래스는 필요한 의존성을 직접 생성하거나 관리 하지 않아도되고 코드의 유연성과 재사용성이 향상됨
    Dagger,Hilt,Koin 등의 라이브러리를 통해 사용이 가능하다
    테스트 하기도 좋아지는데 , 의존성을 모킹하거나 대체하는것이 쉬워짐
    단점으로는 복잡한 문법과 개념, 런타임에 의존성을 주입하기때문에 오류가 컴파일이나 타임이 아닌 런타임에 발생할 수 있다
    성능도 초기화시간이 증가하는 문제가 생길 수 있다

현업에서는 주로 DI방식을 이용한다고 한다고하는데

Dagger, Hilt, Koin 라이브러리가 있다

간략하게 설명하자면

Dagger: 강력하고 유연하지만 구성이 복잡함
Hilt: Dagger를 기반으로 하되 설정이 간소화된 라이브러리
Koin: 코틀린 특화의 간단하고 가벼운 DI라이브러리로 복잡한 설정없이 DI를 구현할 수 있다

Goolge이 공식적으로 Hilt를 지원한다고 해서 Android개발에서는 Hilt를 사용하는 추세가 늘어나고 있다고 한다.

0개의 댓글