서론
다온길 프로젝트를 진행하며 네트워크 에러처리에 대한 부분을 구현하며 상속에 대한 문제점을 알게 되었습니다. 그 과정을 기록하며 상속의 문제점과 상속을 회피할 수 있는 몇 가지 대안을 알아보겠습니다. 이번 글은 이펙티브 코틀린 36장 "상속보다 컴포지션을 사용하라"를 기반하여 작성됩니다.
1. 기존 코드를 살펴보자
📌 Domain Layer
sealed class NetworkError : Throwable(){
abstract override val message: String
}
data object ConnectError : NetworkError() {
override val message: String
get() = "서버에 연결할 수 없습니다. \n인터넷 연결을 확인한 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
data object TimeoutError : NetworkError() {
override val message: String
get() = "서버 응답 시간이 초과되었습니다. \n잠시 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
data object UnknownHostError : NetworkError() {
override val message: String
get() = "서버를 찾을 수 없습니다. \n인터넷 연결 상태를 확인해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
data class HttpError(val code: Int) : NetworkError() {
override val message: String
get() = when (code) {
400 -> "잘못된 요청입니다.\n요청 내용을 확인하고 다시 시도해주세요. \n계속 문제가 발생하면 고객 지원에 문의하세요."
401 -> "인증이 필요합니다.\n로그인 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
403 -> "접근이 금지되었습니다.\n권한을 확인하고 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
404 -> "요청한 리소스를 찾을 수 없습니다.\nURL을 확인하고 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
else -> "서버 오류가 발생했습니다.\n잠시 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
}
data object UnknownError : NetworkError() {
override val message: String
get() = "알 수 없는 오류가 발생했습니다. \n고객 지원에 문의해주세요."
}
📕 왜 이렇게 만들었나요 ? 🤔
클린 아키텍처에서 Domain Layer는 앱의 핵심 비즈니스 로직, 규칙들을 구현합니다.이에 기반하여 에러 상태에 관한 비즈니스 규칙을 Domain Layer 에 정의해주었습니다. Domain Layer는 네트워크 에러 상황에 대비해 사용자에게 보여줄 메시지들을 위한 규칙을 가지고 있습니다.
이를 통해 얻는 이점은 두 가지입니다.
- 관심사를 분리 할 수 있습니다.
- 이를 통해 Presentation Layer에선 에러에 대한 정보 없이 오직 메시지만 알면 됩니다.
또한 Throwable 을 상속하는 에러 Class 들은 매우 많기 때문에 이에 모든 상태를 대응하는 것은 불가능 하다고 판단,
개발 과정에서 자주 만났던 에러들 위주로 처리를 해주었습니다. 만약 앱 출시 이후 추가적인 에러 상태에 대해 대응이 필요하다면 sealed class 에 추가해주기만 하면 되기 때문에 쉽게 확장 가능합니다.
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Error<T>(val error: NetworkError) : Result<T>()
}
fun <T> Result<T>.onSuccess(action: (T) -> Unit): Result<T> {
return when (this) {
is Result.Success<T> ->{
action(value)
this
}
is Result.Error<T> -> {
this
}
}
}
fun <T> Result<T>.onError(action: (NetworkError) -> Unit): Result<T> {
return when (this) {
is Result.Success<T> ->{
this
}
is Result.Error<T> -> {
action(error)
this
}
}
}
📕 왜 이렇게 만들었나요 ? 🤔
Kotlin에서 제공하는 Result Class도 존재하지만 해당 클래스의 경우 Throwable 을 인자로 받습니다. 즉, 위에서 만들어준 NetworkError Class를 사용할 수 없기 때문에 별도의 Result Class를 만들었습니다.
성공과 에러 상태일 때를 관리하는 Success와 Error는 다음과 같은 이유로 만들었습니다.
프로젝트를 진행하면서 아키텍처를 벤치마킹한 프로젝트의 코드입니다.
해당 코드에서 개선하고 싶은 점은 두 가지였습니다.
- 모든 에러에 대해서 common_error로만 보여주어 사용자 입장에서 어떤 에러인지 정확히 파악할 수 없다.
- ViewModel 에 매번 API 호출(UseCase)에 결과에 대한 코드가 매번 반복되어 보일러 플레이트 코드가 늘어난다.
📌 Data Layer
// DataSource
open class BaseDataSource {
protected inline fun <T> execute(block: () -> T): Result<T> = runCatching {
Result.Success(block())
}.getOrElse {
Result.Error(handleNetworkError(it))
}
fun handleNetworkError(e: Throwable): NetworkError {
return when (e) {
is ConnectException -> ConnectError
is SocketTimeoutException -> TimeoutError
is UnknownHostException -> UnknownHostError
is HttpException -> HttpError(e.code())
else -> UnknownError
}
}
}
class PlaceDataSource @Inject constructor(
private val placeService: PlaceService
): BaseDataSource() {
suspend fun searchByList(request: ListSearchRequest) = execute {
response = placeService.searchByList(
// 생략
)
}
}
// Repository
class PlaceRepositoryImpl @Inject constructor(
private val placeDataSource: PlaceDataSource
) : PlaceRepository {
override suspend fun getSearchPlaceResultByList(request: ListSearchOption)
: Result<ListSearchResultList> {
return placeDataSource.searchByList(request.toRequestModel())
}
}
📕 왜 이렇게 만들었나요 ? 🤔
BaseDataSource는 DataSource는 서버와 API 통신을 할 때 일어나는 다양한 에러 상황을 Domain에 정의한 비즈니스 규칙을 통해 공통으로 에러를 처리하여 handleNetworkError을 통해 Throwable을 NetworkError Class로 매핑하기 위한 메소드를 만들었습니다.
📌 Presentation Layer
open class BaseViewModel : ViewModel() {
private val _errorMessage = MutableStateFlow<String?>(null)
val errorMessage get() = _errorMessage.asStateFlow()
protected open fun handleNetworkError(exception: NetworkError) {
val errorState: String? = when (exception) {
is ConnectError -> {
ConnectError.message
}
is TimeoutError -> {
TimeoutError.message
}
is UnknownHostError -> {
UnknownHostError.message
}
is HttpError -> {
HttpError(exception.code).message
}
is UnknownError -> {
UnknownError.message
}
}
_errorMessage.value = errorState
}
}
@HiltViewModel
class SearchListViewModel @Inject constructor(
private val placeRepository: PlaceRepository,
): BaseViewModel() { //생략 }
@AndroidEntryPoint
class SearchListFragment : Fragment(R.layout.fragment_search_list) {
private val sharedViewModel: SharedViewModel by viewModels(ownerProducer = { requireParentFragment() })
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentSearchListBinding.bind(view)
repeatOnViewStarted {
viewModel.errorMessage.collect { msg ->
msg?.let { mainAdapter.submitErrorMessage(it) }
}
}
}
}
📕 왜 이렇게 만들었나요 ? 🤔
앞서 언급한 매번 반복되는 코드 즉, ViewModel에서 에러 상태를 저장하고 StateFlow 또한 모든 ViewModel에
존재해야하기 때문에 이 상태 또한 공통으로 관리하기 위해 이렇게 구현했습니다.
2. 문제점을 파악하자
❌ 상속의 문제점
상속은 공통 로직을 분리하거나 재사용하기 위해 사용합니다. 저는 매번 DataSource와 ViewModel에서 매번 반복되는 로직을 분리해 한번에, 또 일관성 있게 처리하기 위해 사용했습니다. 이렇게만 본다면 상속은 보일러 플레이트를 줄이고 코드의 중복을 제거할 수 있는 좋은 기술이라고 생각할 수 있습니다. 저 또한 그렇게 생각했기에 상속을 적극적으로 사용했습니다. 하지만 제가 전혀 모르고 있던 상속에 대한 이론이 있었습니다. 바로 'is-a', 'has-a' 라는 상속의 관계성입니다.
📌 Has - A(포함) 관계
HAS-A는 포함관계를 의미 (Ex. 짱구는 맥북을 가지고 있다)를 의미하며 한 클래스가 다른 클래스를 소유한 형태로 다른 클래스의 프로퍼티나 메소드를 받아들여 사용합니다.
📌 IS - A (상속) 관계
상속 그 자체의 의미로 A는 B이다를 의미합니다. 이 형태는 하위 클래스가 하위 클래스에 종속되는 특징을 가져 클래스 간의 결합도를 높이는 원인이 됩니다.
상속은 IS - A의 관계성을 가지고 있기 때문에, 제가 만든 DataSource 코드를 예로 들었을 때 Base DataSource를 상속하는 DataSource 들은 각자 다른 역할, 다른 책임들을 가지고 있기 때문에 이 모든 DataSource를 하나의 Class를 상속시켜 관리하는 것은 객체지향관점에서 잘못된 행위였습니다.
또한 당장에 클린 아키텍처의 관점으로만 봐도 구성요소의 결합도를 낮추고 의존성을 분리해야 하는데 상속을 사용한다는 것은 그 자체로 결합도와 의존도를 높이는 행위이기 때문에 이 부분에 있어서 개선점이 필요했습니다.
이펙티브 코틀린에서 제시한 기본적인 상속의 문제점은 다음과 같습니다.
- 코틀린은 다중 상속을 지원하지 않습니다. 즉, 하나뿐인 상속의 기회를 잃기 때문에 상속은 신중하게 사용해야 합니다. 또한 상속으로 여러 기능을 구현하려면 분리할 모든 기능을 하나의 슈퍼 클래스에 구현해야 합니다.
- 상속을 사용해 행위를 추출하면 많은 함수를 갖는 거대한 BaseXXX 클래스를 만들게 되고 굉장히 깊고 복잡한 계층 구조가 형성됩니다.
- 상속은 슈퍼 클래스의 메서드, 제약, 행위 등 모든 것을 가져옵니다. 이를 통해 계층 구조를 만들기에도 좋은 도구이지만 일부분을 재사용하기 위한 목적으로는 적합하지 않습니다.
- 불필요한 함수를 갖는 클래스가 만들어지며 이 행위는 인터페이스 분리 원칙을 위반합니다.
- 슈퍼 클래스의 행위를 서브 클래스에서 깨버리는 일이 일어날 수 도 있어 리스코프 치환 원칙에도 위반됩니다.
- 다른 개발자와 협업을 한다면 다른 개발자가 클래스의 작동 방식을 이해하기 위해 슈퍼 클래스를 여러 번 확인하는 상황이 만들어질 수 있습니다.
❌ 에러 상태 Class의 문제점
앞서 초기 설계된 NetworError Class의 HttpError Class 를 보면 위 코드와 같이 에러 코드에 따라 분기하는 구문이 있습니다.
이게 이상하다고 느끼신 분들도 계실겁니다.
data class HttpError(val code: Int) : NetworkError() {
override val message: String
get() = when (code) {
400 -> "잘못된 요청입니다.\n요청 내용을 확인하고 다시 시도해주세요. \n계속 문제가 발생하면 고객 지원에 문의하세요."
401 -> "인증이 필요합니다.\n로그인 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
403 -> "접근이 금지되었습니다.\n권한을 확인하고 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
404 -> "요청한 리소스를 찾을 수 없습니다.\nURL을 확인하고 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
else -> "서버 오류가 발생했습니다.\n잠시 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
}
Domain Layer에선 에러 코드에 대한 참조가 불가능하며 해서도 안됩니다.
이를 자세히 살펴보겠습니다.
- HttpException은 Throwable의 하위 Class이기 때문에 handleNetworkError 에서 NetworError Class로 변환이 가능합니다.
fun handleNetworkError(e: Throwable): NetworkError {
return when (e) {
is ConnectException -> ConnectError
is SocketTimeoutException -> TimeoutError
is UnknownHostException -> UnknownHostError
is HttpException -> HttpError(e.code())
else -> UnknownError
}
}
- 하지만, HttpException은 Retrofit 라이브러리에서 제공하는 예외 클래스 입니다.
- 즉, Kotlin, Java에 대한 의존성만을 가져야하는 Domain Layer에선 참조가 불가능합니다.
- Domain Layer에선 이 에러코드에 대해 어떠한 메시지를 보여주겠다는 규칙, 예를 들어 HttpException에서 토큰과 관련된 401 code가 전달되었을 때
"로그인 문제가 발생했습니다 문제가 반복 된다면 다시 로그인해주세요."
라는 메시지를 사용자게 보여주겠다는 “규칙” 만이 존재해야 합니다. 기존 코드가 가능했던 이유는 사실 아직 모듈을 레이어별로 나누지 않고 app 모듈에서 패키지만 나누어 가능했습니다. 다온길 프로젝트는 처음부터 클린 아키텍처로 만들지 않고 패키지만 Data, Domain, Presentation 으로 나누어 작업 후, 클린 아키텍처로 구조 변경하여 마이그레이션 하였습니다.
이는 단순히 클린 아키텍처를 “따라하는” 것이 아닌 기존의 코드를 마이그레이션 하는 과정을 통해 이러한 문제점들을 직접 겪으며 해결하는 과정을 통해 진짜 아키텍처와 그 구조에 대한 이해를 하기 위함이었습니다.
❌ 부적절한 안내 메시지
sealed class NetworkError : Throwable(){
abstract override val message: String
}
data object ConnectError : NetworkError() {
override val message: String
get() = "서버에 연결할 수 없습니다. \n인터넷 연결을 확인한 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
data object TimeoutError : NetworkError() {
override val message: String
get() = "서버 응답 시간이 초과되었습니다. \n잠시 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
data object UnknownHostError : NetworkError() {
override val message: String
get() = "서버를 찾을 수 없습니다. \n인터넷 연결 상태를 확인해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
data class HttpError(val code: Int) : NetworkError() {
override val message: String
get() = when (code) {
400 -> "잘못된 요청입니다.\n요청 내용을 확인하고 다시 시도해주세요. \n계속 문제가 발생하면 고객 지원에 문의하세요."
401 -> "인증이 필요합니다.\n로그인 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
403 -> "접근이 금지되었습니다.\n권한을 확인하고 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
404 -> "요청한 리소스를 찾을 수 없습니다.\nURL을 확인하고 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
else -> "서버 오류가 발생했습니다.\n잠시 후 다시 시도해주세요. \n문제가 지속되면 고객 지원에 문의하세요."
}
}
data object UnknownError : NetworkError() {
override val message: String
get() = "알 수 없는 오류가 발생했습니다. \n고객 지원에 문의해주세요."
}
현재 에러 상태 Class는 사용자에게 고객 센터에 문의할 것을 가이드하고 있습니다. 하지만 저희는 고객센터가 없기 때문에 이 부분에서도 수정이 필요했습니다.
3 코드를 개선해 보자
3-1. Domain Layer
NetworkError와 HttpException을 각각 분리했습니다. 에러 메시지를 에러의 이름을 나타내는 title과 사용자에게 가이드할 message로 분리했습니다.
3-2. Data Layer
앞서 살펴본 에러들을 도메인에서 선언한 규칙대로 매핑하기 위한 클래스를 top-level 함수로 선언해주었습니다.
💡 top-level 함수란 클래스나 객체에 속하지 않고 파일의 최상위 수준에 정의된 함수를 의미합니다. 파일 내 어디서든 직접 호출할 수 있으며, 특정 클래스의 인스턴스가 필요하지 않습니다. 이는 전역 함수와 비슷한 개념으로, 유틸리티 함수나 헬퍼 함수를 정의할 때 유용합니다.
기존에 상속되던 BaseDataSource를 제거하고 함수를 import 하면 그대로 동일한 방식으로 사용 가능합니다.
3-3. Presentation Layer
다음으로 ViewModel을 개선해 보겠습니다. ViewModel에선 View에서 구독할 StateFlow가 필요하기 때문에 인터페이스로는 불가능하다고 생각했습니다. 그래서 Gemini에게 답을 구했고 두 가지 방법을 제시받았습니다.
상속을 사용하지 않고 공통 로직을 분리하며 에러 상태를 관리하는 변수를 포함하는 방법은 몇 가지가 있습니다.
그중에서 **컴포지션(Composition)* * 과 델리게이션(Delegation) 패턴을 활용하는 방법을 소개해 드리겠습니다.
1. 컴포지션과 인터페이스 활용
ViewModel에서 공통으로 사용하는 로직을 담당하는 ViewModelHelper 클래스를 생성합니다. ViewModelHelper 클래스는 에러 상태를 관리하는 변수와 공통 함수를 포함합니다. 각 ViewModel은 ViewModelHelper 클래스를 멤버 변수로 갖고, 필요한 기능을 위임합니다. 인터페이스를 활용하여 ViewModelHelper의 기능을 명시적으로 정의합니다.
2. 델리게이션 패턴 활용
공통 로직을 담당하는 ViewModelDelegate 클래스를 생성합니다. ViewModelDelegate 클래스는 에러 상태를 관리하는 변수와 공통 함수를 포함합니다. 각 ViewModel은 ViewModelDelegate 클래스를 멤버 변수로 갖고, 델리게이션 패턴을 사용하여 기능을 위임합니다.
저는 두 방법 중 델리게이션 패턴을 사용했습니다. 델리게이트 패턴은 어떤 객체가 다른 객체의 행동을 대신 처리하도록 위임하는 패턴입니다. 이는 다음과 같은 목적을 가집니다:
- 코드 분리: 서로 다른 책임을 가진 코드를 분리하여 각 클래스가 단일 책임을 갖도록 합니다.
- 재사용성: 공통 기능을 델리게이트 객체로 추출하여 여러 객체에서 재사용할 수 있도록 합니다.
기존 BaseViewModel의 코드를 ViewModelDelegate Class로 목적에 맞게 변경해 주었습니다.
class NetworkErrorDelegate @Inject constructor() {
private val _networkState = MutableStateFlow<NetworkState>(NetworkState.Loading)
val networkState: StateFlow<NetworkState> get() = _networkState.asStateFlow()
fun handleNetworkError(exception: NetworkError) {
val errorState = when (exception) {
is ConnectError -> "${ConnectError.title} \n ${ConnectError.message}"
is TimeoutError -> "${TimeoutError.title} \n ${TimeoutError.message}"
is UnknownHostError -> "${UnknownError.title} \n ${UnknownHostError.message}"
is HttpException -> when (exception) {
is BadRequestError -> "${BadRequestError.title} \n ${BadRequestError.message}"
is AuthenticationError -> "${AuthenticationError.title} \n ${AuthenticationError.message}"
is AuthorizationError -> "${AuthorizationError.title} \n ${AuthorizationError.message}"
is NotFoundError -> "${NotFoundError.title} \n ${NotFoundError.message}"
is ServerError -> "${ServerError.title} \n ${ServerError.message}"
}
is UnknownError -> "${UnknownError.title} \n ${UnknownError.message}"
}
_networkState.value = NetworkState.Error(errorState)
}
fun handleNetworkSuccess(){
_networkState.value = NetworkState.Success
}
fun handleNetworkLoading(){
_networkState.value = NetworkState.Loading
}
}
sealed class NetworkState{
data object Loading: NetworkState()
data object Success: NetworkState()
data class Error(val msg: String): NetworkState()
}
Hilt의 필드 주입을 사용해 ViewModel에 Delegate Class를 주입해 주었습니다. 생성자 주입이 아니라 필드 주입을 사용한 이유는 사실 큰 목적이 있다기보다는 필드 주입도 사용해보고 싶어서 사용했습니다. 😎 (맨날 같은 것만 하면 재미없잖아요?)
@HiltViewModel
class SearchListViewModel @Inject constructor(
private val areaCodeRepository: AreaCodeRepository,
private val sigunguCodeRepository: SigunguCodeRepository,
private val placeRepository: PlaceRepository,
): ViewModel() {
@Inject
lateinit var viewModelDelegate: ViewModelDelegate
init {
viewModelScope.launch {
loadAreaCodes()
loadPlaces()
}
}
val errorMessage: StateFlow<String?> get() = viewModelDelegate.errorMessage
private fun loadPlaces() = viewModelScope.launch((Dispatchers.IO)) {
listOptionState.collect { listOption ->
networkErrorDelegate.handleNetworkLoading()
placeRepository.getSearchPlaceResultByList(listOption.toDomainModel())
.onSuccess { result ->
modifyUiState(result)
}
.onError { e ->
networkErrorDelegate.handleNetworkError(e)
}
}
}
}
이를 View에서 사용할 땐 Loading 상태일 땐 프로그래스바를 출력하고 API 호출에 성공한다면 서비스 페이지를, 실패한다면 에러 페이지를 보여주도록 구현하였습니다.
@AndroidEntryPoint
class SearchListFragment : Fragment(R.layout.fragment_search_list) {
private val sharedViewModel: SharedViewModel by viewModels(ownerProducer = { requireParentFragment() })
private val viewModel: SearchListViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val binding = FragmentSearchListBinding.bind(view)
with(binding) {
repeatOnViewStarted {
supervisorScope {
//
launch {
viewModel.networkState.collect { networkState ->
when (networkState) {
is NetworkState.Loading -> {
searchListProgressBar.visibility = View.VISIBLE
}
is NetworkState.Success -> {
rvSearchResult.visibility = View.VISIBLE
noSearchResultContainer.visibility = View.GONE
searchListProgressBar.visibility = View.GONE
}
is NetworkState.Error -> {
rvSearchResult.visibility = View.GONE
noSearchResultContainer.visibility = View.VISIBLE
textMsg.text = networkState.msg
}
}
}
}
}
}
}
5. 테스트 해보자
의도대로 실제 동작하는지 확인해보기 위해 Repository에서 에러를 반환하도록 만들었습니다.
class PlaceRepositoryImpl @Inject constructor(
private val placeDataSource: PlaceDataSource
) : PlaceRepository {
override suspend fun getSearchPlaceResultByList(request: ListSearchOption)
: Result<ListSearchResultList> {
//return placeDataSource.searchByList(request.toRequestModel())
return Result.Error(ConnectError)
}
}
에러 메시지를 잘 출력하는 것을 확인할 수 있습니다. 🥰
5. 개선 사항 회고
🌈 자체 서버 에러처리
2001 | 존재하지 않는 리소스입니다. |
2003 | 권한이 없는 요청입니다. |
2006 | 이미 존재하는 리소스입니다. |
4001 | 존재하지 않는 여행지입니다. |
4008 | 모든 좌표값이 요청되지 않았습니다. |
4009 | 존재하지 않는 여행지리뷰입니다. |
5005 | 예상치 못한 에러가 발생하여 이미지 추가가 되지 않습니다. |
- 현재 에러처리 방식은 Throwable을 상속하는 에러들만 처리하고 있어 우리 서버에서 내려주는 에러에 대해선 대응이 불가능하기 때문에 이에 대한 개선이 필요합니다.
🌈 중복 에러 처리
- StateFlow는 내부적으로 distinctUntilchanged() 를 사용하기 때문에 중복된 값을 방출하지 않는다.
- 에러 상태를 홀딩하는 라이브러리를 StateFlow로 사용한 이유는 사실 특별히 없었다…
- 에러 메시지를 화면에 보여주기만 하면 된다고 생각했고 그저 습관처럼 사용했다,,,
- 화면 전환시 데이터를 받아오는 로직에 대한 1회성 에러 처리에선 문제가 안됐다.
- 하지만 북마크 버튼 클릭 같이 다발성으로 발생할 수 있는 에러는 동일한 에러 상태를 계속 해서 반복적으로 방출할 수 있다.
- 이 부분을 생각 못했다 (진작에 LiveData 쓸껄,,,)
- 이에 대한 대안으로 인터넷이 연결 상태를 옵저빙 하면서 버튼 클릭 가능 상태를 토글하도록 구현했다.→ 요건 인터넷 연결 상태에 관한 에러처리로 다른 에러에 대해선 여전히 동일한 문제가 존재
- → 지금이라도 LiveData로 바꿀까 …?
- → 어디까지나 대안일 뿐 근본적인 해결 방안은 아님…
'Android' 카테고리의 다른 글
안드로이드 리사이클러뷰 성능 개선 일지 2편(부제 : DiffUtil Deep Dive) (0) | 2024.08.04 |
---|---|
안드로이드 리사이클러뷰 성능 개선 일지 1편(부제: Recyclerview Deep Dive) (0) | 2024.08.03 |
[Android]프로젝트를 클린 아키텍처로 마이그레이션해보자 (0) | 2024.07.12 |
버튼 중복 클릭을 막아보자 (Android ThrottleFirst) (1) | 2024.06.24 |
ViewLifeCycleOwner 제대로 알고 사용해보자 (1) | 2024.04.28 |