Android에서의 코틀린 Sealed Interface

HEETAE HEO·2024년 3월 3일
post-thumbnail

해당 글에서는 Kotlin의 Sealed Interface를 활용하여 문자열 리소스를 효과적으로 관리할 수 있는 방법을 작성해보려고 합니다.

Sealed Interface란?

Kotlin에서 Sealed Interface는 제한된 계층 구조를 정의하는 메커니즘으로, 특정 시나리오에 대해 한정된 타입 집합을 지정할 수 있게 해줍니다. 이러한 기능은 when을 통해 모든 경우를 처리할 수 있도록 보장하므로, 타입 안정성을 높이고 앱의 동작 가능성 또한 향상 시킵니다.

해당 기능에 대해 더 알고 싶다면 아래의 Kotlin 공식 문서에서 확인할 수 있습니다.

Kotlin's official documentation on sealed classes

앱에서의 텍스트

Android 아키텍처에서 ViewModel은 앱의 데이터 계층과 UI 사이의 브릿지 역할을 하며, Android 프레임워크와 결합 없이 비즈니스 로직을 처리하는 역할을 하고 있습니다. Android의 테스트 기본 원칙과 ViewModel 패턴 (로케일 변경 및 AndroidViewModel 안티패턴)에 따르면, ViewModel 내에서 Android 리소스를 직접 사용하는 것을 피하는 것이 좋습니다.

바로 위의 문제를 해결하기 위해 StringResource Sealed Interface를 사용하면 되는 것입니다.

StringResource Sealed Interface는 문자열 리소스 처리를 추상화하는 방법을 제공하여 명확한 관심사 분리를 수행하게 해줍니다. 이 추상화는 아키텍처 원칙을 준수할 뿐만 아니라 ViewModel이 Android의 Context에 의존하지 않도록 하여 testable하고 유지보수성을 높이는데 기여합니다.

@Stable
sealed interface StringResource {
    fun resolve(context: Context): String

    data class Text(val text: String): StringResource {
        override fun resolve(context: Context): String {
            return text
        }
    }
    data class ResId(@StringRes val stringId: Int): StringResource {
        override fun resolve(context: Context): String {
            return context.getString(stringId)
        }
    }
    data class ResIdWithParams(@StringRes val stringId: Int, val params: List<Any>): StringResource {
        override fun resolve(context: Context): String {
            return context.getString(stringId, *params.toTypedArray())
        }
    }

    @Composable
    fun resolve(): String {
        return when (this) {
            is ResId -> stringResource(id = stringId)
            is ResIdWithParams -> stringResource(id = stringId, *params.toTypedArray())
            is Text -> text
        }
    }
}

위의 코드로 작성된 접근 방식은 문자열 리소스를 해결하는 로직을 캡슐화하여, ViewModel이 Android의 Context에 직접 의존하지 않고 문자열 표현과 상호작용할 수 있게 해줍니다. 이는 비즈니스 로직과 UI 관련 사항이 명확히 분리된 개발 모델의 예시로, 확장 가능하고 유지보수하기 쉬운 코드 베이스라고 할 수 있습니다.

위에서 설명한 클래스를 사용하여 ViewModel 내에서 사용할 수 있는 에러 포맷터를 구현할 수 있습니다.

// viewModel

class PricesViewModel @Inject constructor(
        errorFormatter: dagger.Lazy<ErrorFormatter>,
        pricesUseCase: dagger.Lazy<GetPricesUseCase>

): ViewModel() {
    val state = pricesUseCase.get().invoke()
            .map {
                State.Success(it)
            }
            .catch {
                val error = errorFormatter.get().format(it)
                emit(State.Error(error))
            }.stateInDefault(viewModelScope, State.Loading)

}
// inteface

interface ErrorFormatter {
    fun format(throwable: Throwable?) : StringResource
}
class ErrorFormatterDefault: ErrorFormatter {
    override fun format(throwable: Throwable?) : StringResource {
        return when (throwable) {
             is SocketTimeoutException-> StringResource.ResId(R.string.not_connected_to_internet)
            else -> StringResource.Text("Something went wrong!")
        }
    }

이렇게 하면 상태의 오류 케이스를 UI 레이어에서 쉽게 처리할 수 있으며, 오류 처리나 예외 파싱을 직접 다룰 필요가 없습니다.

if (state is State.Error) {
  ErrorView(state.resolve())
}

해당 접근 방식이 가지는 장점

  • 타입 안정성과 예측 가능성 : Sealed Interface는 모든 가능한 타입을 고려하도록 보장하므로, 런타임 오류의 가능성을 줄임으로 견고한 앱을 만들 수 있습니다.
  • 클린 아키텍처 준수 : Sealed Interface를 통해 문자열 리소스를 추상화함으로써, 앱의 로직과 UI 간의 명확한 분리를 유지하여 아키텍처가 꺠끗하게 유지됩니다. 이는 테스트와 유지보수를 더 쉽게 해줍니다.
  • ViewModel 무결성 : ViewModel이 Android 프레임워크에 직접적으로 의존하지 않고 순수한 기능을 유지할 수 있게 하여, 아키텍처의 원칙을 준수합니다.

StringResource 인터페이스를 사용한 접근 방식과 하드코딩한 텍스트와의 차이점

  • 유연성
    • 하드코딩된 텍스트 : 고정된 문자열로, 재사용하기가 어렵고, 다국어 지원(다양한 로케일)에 적합하지 않습니다.
    • StringResource 사용: StringResource 인터페이스를 사용하면 하드코딩된 텍스트 뿐만 아니라, 리소스 ID를 사용하여 Android 문자열 리소스를 활용할 수 있습니다. 이렇게 하면 다국어 지원과 텍스트 포맷팅이 필요할 때 매우 유용합니다.
  • 코드의 가독성과 유지보수성
    • 하드코딩된 텍스트 : 코드에 하드코딩된 텍스트가 많아지면, 코드를 읽고 유지보수하는 것이 어려워집니다.
    • StringResource 사용: StringResource 인터페이스를 사용하면 텍스트 리소스를 한 곳에서 관리할 수 있어, 코드의 가독성과 유지보수성이 향상됩니다.
  • 테스트 가능성
    • 하드코딩된 텍스트 : 하드코딩된 텍스트는 UI와 비즈니스 로직이 강하게 결합되어 있기 때문에, 특정 상황에서 텍스트가 올바르게 표시되는지 테스트하기 어렵습니다.
    • StringResource 사용 : ViewModel이 Context에 의존하지 않기 때문에, UI에 직접 연결되지 않은 상태에서 텍스트 처리 로직을 테스트할 수 있습니다. 이는 유닛 테스트 작성에 유리합니다.

결론

Kotlin의 Sealed Interface를 Android 개발에서 활용하는 것은 특히 문자열 리소스를 관리하는 데 있어 타입 안정성, 아키텍처 가이드 라인, 그리고 유지보수성을 달성하는 방법입니다. 문자열 리소스를 추상화하고 ViewModel의 설계를 준수함으로써, 개발자는 확장 가능하고 견고한 애플리케이션을 구축할 수 있습니다.

읽어주셔서 감사합니다!

참고 자료

https://kotlinlang.org/docs/sealed-classes.html#sealed-classes-and-when-expression
https://proandroiddev.com/kotlins-sealed-interfaces-in-android-0882a3d2afd1

profile
Android 개발 잘하고 싶어요!!!

0개의 댓글