욱'S 노트

Option을 사용해야하는 이유/Nested Nullability (Kotlin 함수형 프로그래밍 #4) 본문

Methdology/Functional Programming

Option을 사용해야하는 이유/Nested Nullability (Kotlin 함수형 프로그래밍 #4)

devsun 2025. 1. 15. 09:57
반응형

예전에 자바를 사용했었다면, NullpointerException을 겪었을 것이다. 널의 가장 큰 문제점은 클라이언트 코드에서 예상할 수 어렵다는데 있었다. 코틀린에서는 ?을 베이스로 null-safety 문제를 해결하고 있다. 코틀린에는 nullable type이 있지만, Arrow에 Option 타입이 있는 이유에 대해서 알아보자.

fun <A> List<A>.firstOrElse(default: () -> A): A = firstOrNull() ?: default()

fun example() {
   emptyList<Int?>().firstOrElse { -1 } shouldBe -1
   listOf(1, null, 3).firstOrElse { -1 } shouldBe 1
}

 

다음 코드를 예상해보자.

fun example() {
   listOf(null, 2, 3).firstOrElse { -1 } shouldBe null
}

 

그러나 결과는 다음과 같다.

Exception in thread "main" java.lang.AssertionError: Expected null but actual was -1

 

Nested Nullabilty Problem

코틀린에서 Nested Nullability 문제는 Nullable Type(?)이 중첩되었을 때 발생하는 혼란스러운 상황이나 이를 처리하는 방법과 관련된 문제를 말한다. 예를 들어, 코틀린에서는 타입이 중첩된 경우 다음과 같은 구조가 가능하다. null을 사용하면 애매모호한 상황에 빠질 수 있다.

  • List<String>?: null일 수 있는 리스트
  • List<String?>: null일 수 있는 요소들을 가진 리스트
  • List<String?>?: 리스트 자체도 null일 수 있고, 리스트 내부의 요소들도 각각 null일 수 있음
val map = hashMapOf<String, Int?>("key1" to 1, "key2" to null)
    
val val1: Int? = map.get("key1")
val val2: Int? = map.get("key2")
val val3: Int? = map.get("key3")

println(val1 == null) // false
println(val2 == null) // true
println(val3 == null) // true

 

 

Option

Option<A>는 A라는 타입의 옵셔널 컨테이너이다. A 타입의 값이 존재한다면 Option<A>는 A타입의 값을 포함한 Some<A>의 인스턴스이다. 값이 존재하지 않는다면 Option<A>는 None 오브젝트이다. 생성자나 확장 함수를 통해서 생성할 수 있다.

val some: Some<String> = Some("I am wrapped in something")
val none: None = None

val optionA: Option<String> = "I am wrapped in something".some()
val optionB: Option<String> = none<String>()

fun example() {
   some shouldBe optionA
   none shouldBe optionB
}

 

하지만 아직도 많은 라이브러리들이 nullable 타입으로 결과를 리턴하기 때문에 nullable type으로 부터 생성하는 방법을 제공한다. fromNullable 함수를 사용해서 A? 타입을 Option으로 리프트 할 수 있다.

val some: Some<String> = Some("I am wrapped in something")
val none: None = None

val optionA: Option<String> = "I am wrapped in something".some()
val optionB: Option<String> = none<String>()

fun example() {
   some shouldBe optionA
   none shouldBe optionB
}

 

리프트와 명시적으로 생성하는 것에는 차이점이 있으니 주의하자.

fun example() {
   val some: Option<String?> = Some(null)
   val none: Option<String?> = Option.fromNullable(null)

   some shouldBe null.some()
   none shouldBe None
}

 

Option 사용하기

가장 쉬운 접근 방식은 getOrNull을 사용하는 것이다.

fun example() {
   Some("Found value").getOrNull() shouldBe "Found value"
   None.getOrNull() shouldBe null
}

 

다른 방법은 getOrElse를 사용하는 방법이다.

fun example() {
   Some( "Found value").getOrElse { "No value" } shouldBe "Found value"
   None.getOrElse { "No value" } shouldBe "No value"
}

 

Option도 대수적 타입이기 때문에 when절에서 패턴매치시 exhaustive 검사가 자동적으로 수행된다.

fun example() {
   when(val value = 20.some()) {
      is Some -> value.value shouldBe 20
      None -> fail("$value should not be None")
   }

   when(val value = none<Int>()) {
      is Some -> fail("$value should not be Some")
      None -> value shouldBe None
   }
}

 

isSome과 isNone으로 Option 값이 값을 가지고 있는지 확인할 수 있다.

fun example() {
   Some(1).isSome() shouldBe true
   none<Int>().isNone() shouldBe true
}

 

?.let { } ?: false 값은 복잡한 널처리 대신에 특정 서술부를 기술할 수도 있다.

fun example() {
   Some(2).isSome { it % 2 == 0 } shouldBe true
   Some(1).isSome { it % 2 == 0 } shouldBe false
   none<Int>().isSome { it % 2 == 0 } shouldBe false
}

 

?.also { } or ?.also { if(it != null) { } }. 대신에 값이 존재할때의 필요한 사이드 이펙트를 실행할 수도 있다.

fun example() {
   Some(1).onSome { println("I am here: $it") }
   none<Int>().onNone { println("I am here") }

   none<Int>().onSome { println("I am not here: $it") }
   Some(1).onNone { println("I am not here") }
}

결론

  • 일반적으로 코틀린에서 nullable types을 사용하는 것을 선호할 것이다. 하지만, option이 더 이상적이다.
  • nested nullable issues나 null value를 지원하지 않는 Reactor나 RxJava와 함께 사용할 때도 그러하다.

 

참고자료 : https://arrow-kt.io/

 

Arrow

Idiomatic functional programming for Kotlin

arrow-kt.io

반응형