
해당 글에서는 Kotlin의 Sealed Interface를 활용하여 문자열 리소스를 효과적으로 관리할 수 있는 방법을 작성해보려고 합니다.
Kotlin에서 Sealed Interface는 제한된 계층 구조를 정의하는 메커니즘으로, 특정 시나리오에 대해 한정된 타입 집합을 지정할 수 있게 해줍니다. 이러한 기능은 when을 통해 모든 경우를 처리할 수 있도록 보장하므로, 타입 안정성을 높이고 앱의 동작 가능성 또한 향상 시킵니다.
해당 기능에 대해 더 알고 싶다면 아래의 Kotlin 공식 문서에서 확인할 수 있습니다.
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())
}
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