핵심 개념
문제 상황
우리가 구현한 함수가 항상 원하는 결과를 준다면 좋겠지만 그렇지 않은 경우도 있을 것이다.
책에서 들었던 몇가지 예시는 아래와 같다.
- 서버에서 데이터를 가져오려고 했지만 실패
- 조건에 맞는 데이터를 찾지 못했거나 형식이 맞지 않음
이 경우를 처리하는 방법은 두 가지가 있다.
결과 부족을 처리하는 두 가지 방법
1. null 또는 Failure 반환
- null: 결과가 없음을 나타내기 위해 간단하게 null을 리턴한다.
- Failure: sealed class를 사용해 실패 상태를 명확하게 표현한다.
예를 들어 Success와 Failure 클래스를 사용하여 성공과 실패를 구분한다. - 장점: Failure를 사용하면 실패 원인을 담을 수 있고, when을 통해 명확하게 상태를 분리하여 처리할 수 있다.
- 단점: null은 명시적이지 않으며 null 체크를 강제하지 않아 NPE(Null Pointer Exception)의 위험이 있다.
inline fun <reified T> String.readObjectOrNull(): T? {
if (incorrectSign) return null
return result
}
inline fun <reified T> String.readObject(): Result<T> {
if (incorrectSign) return Failure(JsonParsingException())
return Success(result)
}
2. 예외를 throw
- 예외 처리를 사용해 잘못된 상황을 나타낸다.
예를 들어 JSON 파싱 실패 시 throw JsonParsingException()을 사용한다. - 예외를 명확하게 사용할 때는 오류가 예상치 못한 상황이라는 점을 강조한다.
- 장점: 예외를 던지면 호출자에게 오류가 즉시 전달되며 처리가 강제된다.
- 단점: 예외는 프로그램 흐름을 방해하고, 무분별하게 사용하면 코드의 가독성과 성능이 저하될 수 있다.
fun parseObject(input: String): Person {
if (input.isEmpty()) {
throw IllegalArgumentException("Input cannot be empty")
}
return parse(input)
}
정리: 언제 어떤 방법을 사용해야 할까?
- null이나 Failure 반환
- 예상 가능한 상황(예: 데이터가 없을 때)을 처리할 때 사용한다.
- Failure를 사용하면 실패 원인과 추가 정보를 명확하게 담을 수 있어 더 유용하다.
- 예외를 throw
- 예상치 못한 상황이나 심각한 오류를 처리할 때 사용한다.
- 예외는 성능 비용이 크므로 일반적인 실패 상황에서는 사용하지 않는 것이 좋다.
+ 왜 sealed class 를 사용하는가?
sealed class는 유한한 하위 클래스를 허용하며, 상태를 명확하게 표현할 수 있다.
예를 들어
- Success: 성공 결과를 담는다.
- Failure: 실패 원인과 추가 정보를 포함한다.
1. 데이터 로드 실패
fun fetchData(): Result<Data> {
return if (networkError) {
Failure(NetworkException("Unable to fetch data"))
} else {
Success(Data("Sample Data"))
}
}
2. 안전한 호출 처리
- 기존 null 방식
val data = fetchDataOrNull()
val result = data?.process() ?: "default value"
- Result를 사용할 경우
val result = when (val data = fetchData()) {
is Success -> data.result.process()
is Failure -> "default value"
}
실제 코드에서의 사용 사례
private suspend fun <T : Any, R> executePostRequest(
endpoint: String,
requestBody: T,
requestSerializer: SerializationStrategy<T>,
responseDeserializer: DeserializationStrategy<R>,
): Result<R> {
val requestBodyJson = json.encodeToString(requestSerializer, requestBody)
val response =
try {
decisionWebClient.post()
.uri("$url/$endpoint")
.accept(MediaType.APPLICATION_JSON)
.header("Content-Type", "application/json")
.body(BodyInserters.fromValue(requestBodyJson))
.retrieve()
.bodyToMono(String::class.java)
.retryWhen(Retry.backoff(1, Duration.ofSeconds(1)))
.awaitSingle()
} catch (e: Exception) {
return Result.failure(e)
}
return try {
val cleanedJsonString = response.replace("\\n", "").replace("\\\"", "\"")
val parsedResponse = json.decodeFromString(responseDeserializer, cleanedJsonString)
Result.success(parsedResponse)
} catch (e: SerializationException) {
handleErrorResponse(response, endpoint)
}
}
private fun <R> handleErrorResponse(
response: String,
endpoint: String,
): Result<R> {
return try {
val errorResponse = json.decodeFromString<ErrorResponseDto>(response)
val errorMessage = errorResponse.reason?.ifBlank { errorResponse.message }
Result.failure(IllegalStateException("$endpoint API error: $errorMessage"))
} catch (serializationException: SerializationException) {
Result.failure(serializationException)
}
}
'Study OR Book > Book' 카테고리의 다른 글
[이펙티브 코틀린] 아이템32: 추상화 규약을 지켜라 (0) | 2025.02.03 |
---|---|
[이펙티브 코틀린] 아이템29: 외부 API를 랩(wrap)해서 사용하라 (0) | 2025.01.21 |
[이펙티브 코틀린] 아이템19: knowledge를 반복하여 사용하지 말라 (0) | 2025.01.15 |
[이펙티브 코틀린] 아이템17: 이름 있는 아규먼트를 사용하라 (0) | 2025.01.08 |
[이펙티브 코틀린] 아이템3: 최대한 플랫폼 타입을 사용하지 말라 (1) | 2024.12.11 |