Kotlin ‐ 결과가 없을 가능성이 있는 경우 널 가능 또는 Result 반환 타입을 선호하라[Effective Kotlin Item 7] - thought-corner/Backend-PlayGround GitHub Wiki
결과가 없을 가능성이 있는 경우 널 가능 또는 Result 반환 타입을 선호하라
- 때때로 함수가 원하는 결과를 생성하지 못할 때가 있다.
- 서버로부터 데이터를 가져오려고 했지만, 인터넷 연결에 문제가 있는 경우
- 특정 조건에 맞는 첫 번째 요소를 가져오려고 시도했으나, 대상 리스트에 조건에 맞는 요소가 없는 경우
- 텍스트로부터 객체를 파싱하려고 할 때 텍스트 형식이 잘못된 경우
- 이러한 상황들을 처리하는 두 가지 메커니즘이 있다.
null 또는 Result.failure를 반환해서 실패는 나타낸다.
- 예외를 던진다.
- 예외는 비정상적이고 특수한 상황을 의미하며, 이런 상황을 처리하기 위해 사용되어야 하며 정보를 전달하겠다는 표준 방식으로는 사용해선 안 된다.
- 예외는 비정상적인 상황을 위해 설계되었기 때문에 JVM에서 예외는 명시적인 검사와 달리 빠르게 처리되지 않는다.
try~catch 블록 내부에서 구현한 코드는 컴파일러가 최적화하기 어렵다.
- 반면
null과 Result.failure는 둘 다 예상되는 오류를 표현하는 데 적합하다.
- 명시적이고 효율적이며 자연스러운 방식으로 오류를 처리한다.
- 따라서 오류가 예상되는 경우에는
null또는 Result.failure를 반환하고 오류가 예상되지 않는 상황에서는 예외를 던져야 한다.
반환 타입으로 Result 사용
- 성공 또는 실패가 될 수 있는 결과를 반환할 때는 코틀린 표준 라이브러리의
Result클래스를 사용한다.
- 실패에는 오류에 대한 정보를 가지고 있는 예외가 포함된다.
- 실패 시 추가 정보를 전달해야 하는 함수에서는 널 가능 타입 대신
Result를 사용한다.
// ⭕ Good
fun main() {
val result = divide(10, 0)
// 방법 1: 성공/실패 각각의 동작 정의
result.onSuccess { value ->
println("결과: $value")
}.onFailure { error ->
println("에러 발생: ${error.message}")
}
// 방법 2: 기본값 설정 (실패 시 0 반환)
val value = result.getOrDefault(0)
// 방법 3: 에러 발생 시 처리 (예: 0을 반환하거나 특정 로직 수행)
val safeValue = result.getOrElse {
println("오류 발생으로 기본값 0을 사용합니다.")
0
}
}
null이나 Result를 사용해 오류를 처리하는 것이 try~catch블록을 사용하는 것보다 더 간단하며, 더 안전하다.
- 예외는 놓치기 쉬울 뿐만 아니라 애플리케이션 전체를 중지시키는 위험도 있지만,
null이나 Result 객체는 명시적으로 처리해야 하며 애플리케이션의 흐름을 중단시키지 않는다.
- 널 가능 타입의 결과와
Result 객체의 차이는 실패했을 때 추가적인 정보를 전달해야 한다면 Result 객체를, 그럴 필요가 없다면 null을 사용하는 것이 좋다.
Result 클래스에는 다음과 같이 결과를 처리하는 데 사용할 수 있는 다양한 메서드가 존재한다.
isSuccess()와 isFailure() : 결과가 성공인지 실패인지 확인할 때 사용하는 프로퍼티
onSuccess()와 onFailure() : 결과가 각각 성공 또는 실패인 경우 해당 람다식을 호출하는 메서드
getOrNull() : 결과가 성공일 때 값을 반환하고, 실패했을 때 null을 반환하는 메서드
getOrThrow() : 결과가 성공일 때 값을 반환하고, 실패했을 때 해당 실패에 대한 예외를 던지는 메서드
getOrDefault() : 결과가 성공일 때 값을 반환하고, 실패했을 때 인수로 제공된 함수를 호출하고 그 결과를 반환하는 메서드
getOrElse() : 결과가 성공일 때 값을 반환하고, 실패했을 때 인수로 제공된 함수를 호출하고 그 결과를 반환하는 메서드
exceptionOrNull() : 결과가 실패하면 예외를 반환하고, 그렇지 않으면 null을 반환하는 메서드
map : 성공 값을 변환하는 메서드
recover : 예외 객체 값을 성공 값으로 변환하는 메서드
fold : 단일 메서드 내에서 성공/실패 모두를 핸들링하는 메서드
- 예외를 던지는 함수를
Result를 반환하는 함수로 변환하려면 runCatching을 사용한다.
fun getA(): Result<T> = runCatching { getAThrowing() }
반환 타입으로 null 사용
- 코틀린에서
null은 값이 없음(lack of value)을 나타내는 표시이다.
- 함수가
null을 반환한다는 것은 값을 반환할 수 없다는 것을 의미한다.
null은 함수가 기대한 값을 반환할 수 없음을 나타내는 데 사용된다.
- 함수에서 실패가 발생했을 추가 정보를 전달할 필요가 없다면
Result 대신 null 가능 타입을 사용하면 된다.
List<T>.getOrNull(Int) : 주어진 인덱스에 값이 없을 때 null을 반환한다.
String.toIntOrNull() : String을 Int로 올바르게 파싱하지 못할 때 null을 반환한다.
Iterable<T>.firstOrNull(() -> Boolean) : 인수로 전달된 조건식에 맞는 요소가 없을 때 null을 반환한다.
방어적 프로그래밍과 공격적 프로그래밍
- 예외는 프로그램의 정상적인 실행 흐름의 일부가 되어선 안 된다.
- 데이터베이스나 네트워크로부터 데이터를 가져올 때처럼 성공 또는 실패할 수 있는 작업을 수행할 때는
Result또는 널 가능 타입을 사용해야 한다.
- 성공 또는 실패할 수 있는 작업을 수행할 때 실패를 처리하는 것 또한 프로그램의 정상적인 실행 흐름이므로 실패를 안전하게 처리하여 프로그램의 안전성을 높여야 한다.
- 이것이 바로 방어적 프로그래밍 개념을 구현한 것이다.
- 반면 개발자가 잘못된 인수를 사용해 메서드를 호출하거나 잘못된 상태에 있는 객체의 메서드를 호출하는 등의 실수를 할 때 이런 상황을 묵인하는 것은 위험할 수 있는데, 이로 인해 전혀 예상하지 못한 문제가 발생할 수 있기 때문이다.
- 이런 접근법은 공격적 프로그래밍이라고 한다.