Kotlin 분해 선언(destructuring declarations)을 사용하여 Pair 및 Triple 요소 분해

Pair 클래스와 Triple 클래스

Kotlin 은 2개의 값, 혹은 3개의 값을 보관 유지하기 위한 간단한 클래스로서 Pair 클래스와 Triple 클래스를 제공하고 있다.

Pair 객체의 첫 번째 요소와 두 번째 요소는 각각 first, second라는 속성으로 접근할 수 있다.

val pair = "one" to 1
println(pair.first)
println(pair.second)

실행 결과:

one
1

Triple 객체의 요소도 마찬가지로 first, second, third라는 속성으로 접근할 수 있다.

val triple = Triple("one", "two", "three")
println(triple.first)   //=> "one"
println(triple.second)  //=> "two"
println(triple.third)   //=> "three"

실행 결과:

one
two
three

분해 선언에서 객체의 각 속성을 별도의 변수에 할당

Kotlin의 **분해 선언(destructuring declarations)**이라는 형태로 변수를 정의하면, 객체의 각 속성이 보유한 값을 한 번에 여러 개의 변수에 할당할 수 있다. 분해 선언의 방법은 간단하며, 대입문의 왼쪽에 여러 개의 변수를 괄호로 묶어 작성하면 된다.

다음 예제에서는 Pair 객체의 first 속성과 second 속성의 값을 각각 x, y라는 변수로 대입하고 있다.

val pair = 100 to 200  // Pair(100, 200)와 동일

val (x, y) = pair  // 분해 선언 대입
println(x)
println(y)

실행 결과:

100
200

Triple 객체에 대해서도 동일하다.

val triple = Triple(100, 200, 300)
val (x, y, z) = triple

Pair 객체를 이용한 다항식 함수 구현하기

함수가 PairTriple을 반환하는 경우에도 동일하게 여러 변수에 직접 대입할 수 있다. 이는 마치 여러 개의 반환값을 반환하는 함수(다항식)처럼 동작한다.

// 간단한 Pair 반환 함수
fun getPosition(): Pair<Int, Int> = 100 to 200

// 반환된 Pair의 각 속성을 별도의 변수로 받는다.
val (x, y) = getPosition()
println(x)
println(y)

실행 결과:

100
200

단, 이렇게 함수의 반환값을 분해 선언으로 받을 때는 변수의 순서를 바꾸지 않도록 주의해야 한다. 예를 들어, 다음과 같이 변수 이름을 반대로 하면 찾기 어려운 버그가 발생할 수 있다.

val (y, x) = getPosition()

따라서 Pair 객체를 다항식 함수 구현에 사용하는 것은 오히려 안티패턴이라고 할 수 있다. Kotlin에서는 데이터 클래스를 쉽게 정의할 수 있으므로, 일반적으로 다음과 같이 반환값을 나타내는 타입을 정의하는 것이 안전하다.

data class Position(val x: Int, val y: Int)
fun getPosition(): Position = Position(100, 200)

val pos: Position = getPosition()
val (x, y) = getPosition()

마지막 라인에서는 앞에서 설명한 예제와 마찬가지로 분해 선언을 통해 대입을 하고 있지만, 변수 이름을 뒤집었을 경우 IDE(개발 환경)에서 경고 메시지를 표시해주기 때문에 오류가 발생할 확률이 훨씬 낮아진다.

분해 선언의 구조

데이터 클래스를 통한 componentN의 자동 구현

val triple = Triple(100, 200, 300)
val (x, y, z) = triple

위와 같은 분해 선언은 실제로 내부적으로는 아래와 같이 처리된다(실제로 이렇게 작성해도 동작한다).

val triple = Triple(100, 200, 300)
val x = triple.component1()
val y = triple.component2()
val z = triple.component3()

이렇게 동작하기 위해서는 Triple 클래스가 componentN이라는 함수를 제공해야 하는데, Triple 클래스의 정의를 살펴봐도 그런 구현을 찾아볼 수 없다.

public data class Triple<out A, out B, out C>(
    public val first: A,
    public val second: B,
    public val third: C
)

실은 Kotlin에서는 클래스를 데이터 클래스로 정의하면 해당 프로퍼티에 접근하기 위한 componentN 연산자 함수를 자동으로 정의해주도록 되어 있다. 즉, 데이터 클래스의 프로퍼티는 기본적으로 분해 선언을 통한 대입이 가능하다.

분해 선언의 대표적인 사용 예로는 컬렉션 계열 클래스의 withIndex() 함수가 있다.

val arr = arrayOf("one", "two", "three")
for ((index, value) in arr.withIndex()) {
    println("$index -> $value")
}

실행 결과:

0 -> one
1 -> two
2 -> three

withIndex() 함수는 IndexedValue라는 클래스의 객체를 순서대로 반환하는데, 이 클래스도 데이터 클래스로 정의되어 있기 때문에 그 속성(indexvalue)을 분해 선언으로 가져올 수 있다.

data class IndexedValue<out T>(public val index: Int, public val value: T)

데이터 클래스 이외의 componentN 구현

데이터 클래스가 아닌 객체를 분해 선언의 오른쪽에 배치하려면 componentN 계열의 함수를 자체적으로 구현해야 한다.

다음 코드는 Map의 요소를 루프 처리하는 예시인데, 여기서 keyvalue 쌍을 추출할 때도 분해 선언의 메커니즘을 활용하고 있다.

val map = mapOf(1 to "one", 2 to "two")

for ((key, value) in map) {
    println("$key -> $value")
}

MapMap.Entry는 데이터 클래스가 아닌 인터페이스이기 때문에 componentN 계열의 함수가 구현되어 있지 않아 그대로는 분해 선언의 오른쪽에 배치할 수 없다. 그래서 Kotlin 표준 라이브러리에서는 Map 인터페이스와 Map.Entry 인터페이스를 다음과 같이 확장하여 분해 선언을 이용한 루프 처리가 가능하도록 하였다.

inline operator fun <K, V> Map<out K, V>.iterator(): Iterator<Map.Entry<K, V>> = entries.iterator()
inline operator fun <K, V> Map.Entry<K, V>.component1(): K = key
inline operator fun <K, V> Map.Entry<K, V>.component2(): V = value

즉, 위에서 설명한 루프 처리는 다음과 같은 코드와 동일하다.

for (entry in map.entries) {
    val key = entry.component1()
    val value = entry.component2()
    println("$key -> $value")
}



최종 수정 : 2024-03-17