일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 서비스 쓰레드 차이
- rxjava hot observable
- jvm 작동 원리
- Rxjava Observable
- 안드로이드 유닛 테스트
- 안드로이드 레트로핏 사용법
- android ar 개발
- rxjava disposable
- 안드로이드 레트로핏 crud
- jvm이란
- 멤버변수
- 안드로이드 유닛테스트란
- rxjava cold observable
- 안드로이드 os 구조
- 큐 자바 코드
- 서비스 vs 쓰레드
- 자바 다형성
- 객체
- android retrofit login
- 안드로이드 라이선스
- 클래스
- 안드로이드 유닛 테스트 예시
- 2022 플러터 안드로이드 스튜디오
- 플러터 설치 2022
- 2022 플러터 설치
- ANR이란
- ar vr 차이
- 스택 자바 코드
- 안드로이드 라이선스 종류
- 스택 큐 차이
- Today
- Total
나만을 위한 블로그
[Android] Result와 Result.fold() 알아보기 본문
API 통신을 하다 보면 응답을 Result로 받을 수 있다. Result가 뭔지 모른다면 아래를 참고한다.
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/-result/
Result
Result MembersMembers & Extensions Types Properties Functions Returns the encapsulated result of the given transform function applied to the encapsulated value if this instance represents success or the original encapsulated Throwable exception if it is fa
kotlinlang.org
성공적인 결과를 T 타입의 값으로 캡슐화하거나 임의의 Throwable한 예외로 실패를 캡슐화하는 차별적 결합(discriminated union)이다
@JvmInline
value class Result<out T> : Serializable
https://www.baeldung.com/kotlin/result-class
(중략)...오류 처리에서 중요한 역할을 하는 기능 중 하나가 Result 타입이다. Result는 코틀린 표준 라이브러리의 일부로 성공 또는 실패할 수 있는 작업 결과를 처리하기 위해 도입됐다. Result의 인스턴스를 생성하는 방법은 2개 있다
- 성공적인 결과를 나타내려면 Result.success() 사용
- 실패한 결과를 나타내려면 Result.failure() 사용
코틀린에서 예외 대신 Result 객체를 사용하는 건 함수형 프로그래밍 원칙에 부합한다. 성공적 실패를 명시적으로 처리해서 예측 가능성, 코드 견고성을 높인다. 예외와 달리 Result 객체는 개발자가 오류를 명시적으로 처리하게 장려해서 명확하고 통제된 오류 관리에 기여한다
또한 Result 객체는 테스트를 간소화하고 함수형 구성 관행에 잘 맞으므로 더 모듈적이고 유연한 코드 설계가 가능하다. 복구할 수 없는 오류는 예외를 쓸 수 있지만 Result 객체는 제어된 오류 처리, composability가 우선시되는 경우에 대안을 제공한다
< Result는 왜 중요한가? >
개발자가 성공, 실패 케이스를 모두 성실하게 처리하도록 유도해서 잠재적 오류를 간과할 위험을 최소화해서 앱 개발에 도움을 준다. 또한 Result는 nullable한 리턴 타입에 비해 성공 또는 실패를 의도적으로 표현할 수 있는 이점을 제공한다. 이는 코드 가독성을 향상시키고...(중략)...코드를 더 쉽게 추론할 수 있게 해 준다...(중략)
Result는 어떤 작업의 성공 또는 실패를 명시적으로 표현(처리)하게 해서 비동기 작업을 좀 더 깔끔하게 관리하고 싶을 때 고려할 수 있는 방법이다. 비동기 작업은 api 호출, DB 조회 등을 말한다. 컨벤션이 잘 되어 있고 함수가 잘 쪼개져 있다면 가독성도 덤으로 가져갈 수 있다.
아래는 Result의 아주 간단한 예시다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = this::class.simpleName
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val result: Result<String> = fetchData(success = false)
val message = result
.onSuccess { data -> Log.d(TAG, "## [fold] success : $data") }
.onFailure { e -> Log.d(TAG, "## [fold] failed : $e") }
Log.d(TAG, "## [fold] 결과 확인 : $message")
}
private fun fetchData(success: Boolean): Result<String> {
return if (success) {
Result.success("데이터 로드 성공")
} else {
Result.failure(Exception("데이터 로드 실패"))
}
}
}
// >> [fold] success : 데이터 로드 성공
// >> [fold] 결과 확인 : Success(데이터 로드 성공)
쓸데없는 코드는 지우고 Result를 쓰는 부분만 가져왔다. 먼저 fetchData()에서 어떤 비동기 처리를 했다고 가정하고 성공이라면 Result.success(), 실패면 Result.failure()에 각각 다른 문자열을 넣어서 리턴한다.
그 후 값을 받길 원하는 곳에서 fetchData()의 리턴값을 변수에 담은 다음, onSuccess와 onFailure를 체이닝 형태로 호출하고 그 안에서 각각 성공, 실패 시 어떤 처리를 할지 적는다. 지금은 예시기 때문에 간단하게 로그만 썼다.
true 대신 false를 넣으면 아래 로그가 표시된다.
## [fold] failed : java.lang.Exception: 데이터 로드 실패
## [fold] 결과 확인 : Failure(java.lang.Exception: 데이터 로드 실패)
특이한 걸 볼 수 있다. 성공 때처럼 메시지만 띡 표시되는 게 아니라 java.lang.Exception이 표시되는 걸 볼 수 있다.
onSuccess와 onFailure의 공식문서를 확인해 본다. 참고로 두 함수는 모두 코틀린 1.3부터 도입됐다.
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/on-success.html
onSuccess
Performs the given action on the encapsulated value if this instance represents success. Returns the original Result unchanged. Since Kotlin1.3
kotlinlang.org
이 인스턴스가 성공을 나타내는 경우 캡슐화된 값에 지정된 작업을 수행한다. 원본 결과를 바꾸지 않고 리턴한다
/**
* Performs the given [action] on the encapsulated value if this instance represents [success][Result.isSuccess].
* Returns the original `Result` unchanged.
*/
@InlineOnly
@SinceKotlin("1.3")
public inline fun <T> Result<T>.onSuccess(action: (value: T) -> Unit): Result<T> {
contract {
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
}
if (isSuccess) action(value as T)
return this
}
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/on-failure.html
onFailure
Performs the given action on the encapsulated Throwable exception if this instance represents failure. Returns the original Result unchanged. Since Kotlin1.3
kotlinlang.org
이 인스턴스가 실패를 나타내는 경우 캡슐화된 Throwable 예외에 대해 지정된 작업을 수행한다. 바뀌지 않은 원본 결과를 리턴한다
/**
* Performs the given [action] on the encapsulated [Throwable] exception if this instance represents [failure][Result.isFailure].
* Returns the original `Result` unchanged.
*/
@InlineOnly
@SinceKotlin("1.3")
public inline fun <T> Result<T>.onFailure(action: (exception: Throwable) -> Unit): Result<T> {
contract {
callsInPlace(action, InvocationKind.AT_MOST_ONCE)
}
exceptionOrNull()?.let { action(it) }
return this
}
각 함수의 원본 코드는 아래 깃허브에서 가져왔다.
https://github.com/JetBrains/kotlin/blob/rrr/2.1.0/core-docs/libraries/stdlib/src/kotlin/util/Result.kt#L331
kotlin/libraries/stdlib/src/kotlin/util/Result.kt at rrr/2.1.0/core-docs · JetBrains/kotlin
The Kotlin Programming Language. . Contribute to JetBrains/kotlin development by creating an account on GitHub.
github.com
앞에서 Result는 성공적인 결과를 T 타입의 값으로 캡슐화하거나 임의의 Throwable 예외로 실패를 캡슐화한다고 했었다.
Result는 내부적으로 success, failure를 하나의 객체로 래핑하는 클래스기 때문에, 캡슐화된 성공 결과인 T 타입의 값은 Result.success(value), 캡슐화된 Throwable 예외는 Result.failure(exception)의 내부에 저장된다.
그래서 Result 객체 안에 성공한 데이터와 실패 원인, 예외정보가 포함돼 있다. 이 점 때문에 T 타입의 결과 또는 Throwable 예외로 성공, 실패를 각각 캡슐화한다고 말하는 것이다.
캡슐화가 무엇인지 알고 있다면 이해가 쉽겠지만, 모르더라도 캡슐화가 뭔지 이해하면 된다. 위키백과 링크를 쭉 읽어보고 다른 블로그들도 확인하면 캡슐화가 뭔지는 대강 알 수 있을 것이다.
https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)
Encapsulation (computer programming) - Wikipedia
From Wikipedia, the free encyclopedia Bundling of data In software systems, encapsulation refers to the bundling of data with the mechanisms or methods that operate on the data. It may also refer to the limiting of direct access to some of that data, such
en.wikipedia.org
그래서 onSuccess, onFailure를 쓰는 것까진 좋다. 그럼 캡슐화가 돼 있다면 Result의 인스턴스에서 값을 꺼내서 쓰는 것도 가능할 것이다.
현재 코틀린은 Result에서 값을 꺼내기 위한 4가지 함수를 제공한다.
- getOrNull : 성공 시 캡슐화된 값을 리턴, 실패 시 null을 리턴하는 함수
- getOrThrow : 성공 시 캡슐화된 값을 리턴, 실패 시 캡슐화된 Throwable 예외 던짐
- getOrElse : 성공 시 캡슐화된 값을 리턴, 실패 시 캡슐화된 Throwable 예외에 대한 onFailure 함수의 결과를 리턴. onFailure 함수가 던진 모든 Throwable 예외를 다시 던진다는 것에 주의하라
- getOrDefault : 성공 시 캡슐화된 값을 리턴, 실패 시 기본값(defaultValue) 리턴
각 함수를 사용한 예시는 아래와 같다. 이전 코드를 재활용했다.
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val TAG = this::class.simpleName
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val result: Result<String> = fetchData(success = false)
val message = result
.onSuccess { data -> /*Log.d(TAG, "## [fold] success : $data")*/ }
.onFailure { e -> /*Log.d(TAG, "## [fold] failed : $e")*/ }
// Log.d(TAG, "## [fold] 결과 확인 : $message")
val a = message.getOrNull()
val b = try {
message.getOrThrow()
} catch (e: Exception) {
Log.d(TAG, "## [fold] catch - exception : $e")
}
val c = message.getOrElse { "실패해서 리턴되는 기본값을 여기에 명시" }
val d = message.getOrDefault("getOrDefault() default value")
Log.d(TAG, "## [fold] getOrNull : $a")
Log.d(TAG, "## [fold] getOrThrow : $b")
Log.d(TAG, "## [fold] getOrElse { \"\" } : $c")
Log.d(TAG, "## [fold] getOrDefault : $d")
}
private fun fetchData(success: Boolean): Result<String> {
return if (success) {
Result.success("데이터 로드 성공")
} else {
Result.failure(Exception("데이터 로드 실패"))
}
}
}
// >> [fold] catch - exception : java.lang.Exception: 데이터 로드 실패
// >> [fold] getOrNull : null
// >> [fold] getOrThrow : 1
// >> [fold] getOrElse { "" } : 실패해서 리턴되는 기본값을 여기에 명시
// >> [fold] getOrDefault : getOrDefault() default value
흥미로운 건 getOrThrow()의 리턴값이 1이라는 것이다. String만 쓰는데 1은 어디서 튀어나온 값인가?
커뮤니티에 물어보니 변수 b에 try-catch의 결과값을 대입하는데 getOrThrow()가 예외를 리턴하면서 catch에 진입한다. 이 때 Log.d()가 호출되는데 이 함수는 로깅될 수 있으면(isLoggable) 양의 정수를 리턴하는데 이것의 영향으로 1을 리턴하는 것 같다고 한다. 이게 중요한 게 아니니 넘어간다.
레트로핏으로 서버와 통신할 때 Result를 사용하고 싶다면 어떻게 할 수 있을까?
예전에 hilt와 레트로핏 기반의 멀티 모듈 프로젝트를 구성하는 예제를 포스팅했는데 예제 수준이지만 이 글을 참고할 수 있다.
https://onlyfor-me-blog.tistory.com/1127
[Android] Hilt + Retrofit + Flow + Coil + 멀티 모듈 구조 프로젝트 - 2 -
※ 모든 코드는 예시 코드기 때문에 실제로 사용하려면 반드시 리팩토링 후 사용한다※ 컴포즈 안 쓴다 이전 포스팅에서 이어지는 글이다. https://onlyfor-me-blog.tistory.com/1121 [Android] Hilt + Retrofit +
onlyfor-me-blog.tistory.com
코드만 간단하게 확인한다.
interface ApiService {
@GET("albums")
suspend fun getAlbums(): Response<List<AlbumResponse>>
@GET("photos")
suspend fun getPhotos(): Response<List<PhotoResponse>>
@GET("todos")
suspend fun getTodos(): Response<List<TodoResponse>>
}
리턴 타입을 Response로 설정하는 게 중요하다.
이렇게 레트로핏 인터페이스를 정의하면 레포지토리 인터페이스와 이것의 구현 클래스를 아래처럼 정의할 수 있다.
interface PhotoRepository {
fun getPhotos(): Flow<Result<List<PhotoEntity>>>
}
class PhotoRepositoryImpl @Inject constructor(
private val apiService: ApiService,
private val photoMapper: PhotoMapper,
): PhotoRepository {
override fun getPhotos(): Flow<Result<List<PhotoEntity>>> = performApiCall(
funcName = "getPhotos()",
apiCall = { apiService.getPhotos() }
).map { result ->
result.mapCatching { response: List<PhotoResponse> ->
response.map(photoMapper::mapToDomain)
}
}
}
repository에서 리턴 타입을 Result로 설정한 다음 impl에서 result에 mapCatching을 사용해 정상 처리된 경우 photoMapper.mapToDomain()을 호출하고, 실패 시 앱 크래시가 발생하지 않게 Result.failure()로 자동 변환되어 getPhotos()의 onFailure를 호출하는 곳에서 처리할 수 있다.
예제대로라면 뷰모델이 될 것이다.
@HiltViewModel
class PhotoViewModel @Inject constructor(
private val getPhotosUseCase: GetPhotosUseCase
): ViewModel() {
private val _photos = MutableStateFlow<List<PhotoEntity>>(emptyList())
val photos: StateFlow<List<PhotoEntity>> = _photos.asStateFlow()
private val _error = MutableStateFlow<String?>(null)
val error = _error.asStateFlow()
fun loadPhotos() = viewModelScope.launch(Dispatchers.IO) {
getPhotosUseCase().collect { result ->
result.fold(
onSuccess = { photos ->
_photos.value = photos
},
onFailure = { e ->
_error.value = e.message
}
)
}
}
}
저 포스팅에서도 강조하고 있지만 예제 수준의 코드기 때문에 실제로 사용하기 전 반드시 리팩토링하고, 사용해야 하는 구조라면 사용하는 걸 추천한다.
이제 뷰모델 예시에서도 사용하는 Result.fold()에 대해 확인한다.
https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/fold.html
fold
Returns the result of onSuccess for the encapsulated value if this instance represents success or the result of onFailure function for the encapsulated Throwable exception if it is failure. Note, that this function rethrows any Throwable exception thrown b
kotlinlang.org
이 인스턴스가 성공을 나타내는 경우 캡슐화된 값에 대한 onSuccess의 결과를 리턴하고 실패면 캡슐화된 Throwable 예외에 대한 onFailure 함수의 결과를 리턴한다. 이 함수는 onSuccess 또는 onFailure 함수에 의해 던져진 모든 Throwable 예외를 다시 던진단 것에 주의하라
inline fun <R, T> Result<T>.fold(onSuccess: (value: T) -> R, onFailure: (exception: Throwable) -> R): R
Result.fold()는 Result 타입 값을 onSuccess, onFailure 2가지 케이스로 나눠 처리할 수 있게 해주는 함수다.
처음에 확인했던 코드를 fold()를 사용하도록 바꾸면 아래와 같이 된다.
override fun onCreate(savedInstanceState: Bundle?) {
val result: Result<String> = fetchData(success = false)
val message = result.fold(
onSuccess = { data -> "성공" },
onFailure = { e -> "실패" }
)
}
이 때 message는 String 타입이 된다. 왜냐면 onSuccess, onFailure 모두에서 성공 또는 실패 문자열을 리턴하기 때문이다. 로그만 남겨둔다면 Int 타입이 되는 걸 확인할 수 있다.
만약 onSuccess에선 100, onFailure에선 그대로 "실패"를 리턴시키더라도 true면 100이 로그캣에 출력되고 false면 "실패"가 출력된다. 이 점을 활용해서 성공, 실패 시 처리를 좀 더 다양하게 처리할 수 있을 것이다.
지금까지 보면 알겠지만 onSuccess, onFailure를 체이닝 형태로 쓰는 것과 fold()를 쓰는 것 사이에 딱히 큰 차이는 없어 보이는데 뭘 써야 할지, 뭘 쓰면 좋을지 망설여질 수 있다.
근데 진짜 별 차이 없으니 원하는 거 쓰면 된다. 왜냐면 스크롤을 위로 올려서 보면 알겠지만 onSuccess, onFailure, fold 모두 inline 키워드가 붙은 인라인 함수라서, 호출 시 호출된 곳에 함수 본문을 펼쳐서(inline) 넣기 때문에 함수 호출 오버헤드가 사라지고 런타임 비용이 거의 없기 때문이다. 인라인 함수가 뭔지 모른다면 공식문서를 훑어보면 될 것이다.
https://kotlinlang.org/docs/inline-functions.html
Inline functions | Kotlin
kotlinlang.org
때문에 프로젝트 내 또는 팀 내 컨벤션이 정해져 있다면 그걸 따르면 된다. 혼자 만드는 프로젝트라면 원하는 거 쓰면 된다.
'Android' 카테고리의 다른 글
[Android] 일반적인 UseCase 패턴 실수 (0) | 2025.03.26 |
---|---|
[Android] @JvmOverloads란? (0) | 2025.03.03 |
[Android] ListAdapter란? ListAdapter 사용법 (0) | 2025.02.19 |
[Android] Unable to delete directory build 에러 해결 (0) | 2025.02.18 |
[Android] 텍스트에 밑줄 추가하는 법 (0) | 2025.02.11 |