Destructuring (구조 분해)

공식 문서

코틀린에서 리스트배열, Data Class 등의 값을 각각 변수에 담고 싶을 때 구조 분해를 사용할 수 있다.

fun main() {
	val li = listOf(1, 2, 3)
    val (n1, n2, n3) = li
    println("$n1, $n2, $n3") // 1, 2, 3
    
    val arr = charArrayOf('a', 'b', 'c')
    val (c1, c2, c3) = arr
    println("$c1, $c2, $c3") // a, b, c
    
    data class DoubleClass(val d1: Double, val d2: Double, val d3: Double)
    val dc = DoubleClass(3.2, 1.5, 5.43)
    val (d1, d2, d3) = dc
    println("$d1, $d2, $d3") // 3.2, 1.5, 5.43
}

파이썬을 사용해 본 사람이 있다면 unpack 과 비슷한 개념이라고 생각하면 될 것 같다. (파이썬과 다르게 코틀린에서는 괄호로 감싸야 함)

fun main() {
	val fruitPrice = mapOf("melon" to 10000, "banana" to 3000, "tomato" to 1000)
    for ((fruit, price) in fruitPrice) {
        println("$fruit 가격은 $price 원")
    }
    /*
    melon 가격은 10000 원
    banana 가격은 3000 원
    tomato 가격은 1000 원
    */
}

위와 같이 for 문으로 접근할 수도 있다. 코틀린에서 Map 은 각각의 Key-Value 셋을 Pair 로 관리한다.

이 말은 즉, Pair 도 구조 분해가 가능하고, 비슷한 타입인 Triple 역시 구조 분해가 가능하다.

fun main() {
	val science = Pair("science", 100)
    val pe = Triple("pe", 100, 80)
    
    val (subject1, score) = science
    val (subject2, testScore, activityScore) = pe
    
    println("$subject1 점수는 $score 점") // science 점수는 100 점
    println("$subject2 시험 점수는 $testScore, 활동 점수는 $activityScore 점") // pe 시험 점수는 100, 활동 점수는 80 점
}
fun main() {
    val pe = Triple("pe", 100, 80)
    
    val (subject2, _, activityScore) = pe
    
    println("$subject2 활동 점수는 $activityScore 점") // pe 활동 점수는 80 점
}

사용하지 않을 값은 _ 로 하면 값을 구조분해 하지 않고 넘어간다.

그 외에도 .map 같은 확장 함수에서도 활용할 수 있다.

구조 분해 최대 개수

구조 분해도 상황에 따라 무한히 할 수 있는 것은 아니다.

  • Pair - 2 개
  • Triple - 3 개
  • List, Array - 5 개
  • Data Class - 제한 없음

PairTriple 은 당연한 말이고, List, Array, Data Class 는 다른 개수를 가지고 있다.

내부를 확인해보자

Data Class - ComponentN

Data Class 를 만들어서 자바 코드로 확인해보자.

data class Person(
    val name: String,
    val age: Int,
    val married: Boolean
)

image

디컴파일 결과 다양한 내용이 있지만 표시해 둔 component1, 2, 3 이 있는 것을 볼 수 있다. 그리고 각각은 멤버 변수가 선언된 순서대로 각각을 반환하는 역할을 한다.

그렇다면 이것을 몇 개 까지 만들 수 있을까?

궁금해서 많은 수의 멤버 변수를 가지는 Data Class 를 만들어 보았다.

image

image

총 510 개의 멤버 변수를 가지고 있다. 디컴파일 해보자.

image

image

예상했던대로(?) 선언한 510 개 모두에 대해서 componentN() 메서드가 생긴다. 그리고 좀 더 알아본 결과 시스템이 허용하는 한 무제한 생길 수 있다.

왜 갑자기 componentN 이야기를 꺼냈냐면 componentN 이 바로 구조 분해를 하기 위한 메서드이기 때문이다.

확인해보자.

data class Person(
    val name: String,
    val age: Int,
    val married: Boolean
)

fun personTest() {
    val me = Person("mangbaam", 27, false)
    val (name, age, married) = me
}

위와 같이 코드를 작성하고 디컴파일 해보면 다음과 같이 나온다.

image

nameagevar1, var2 로 표현되긴 했지만 me.component1, 2, 3 을 통해서 값을 할당하는 것을 확인할 수 있다.

val me = Person("mangbaam", 27, false)
me.component1()
me.component2()
me.component3()

componentNpublic 메서드이기 때문에 위와 같이 직접 접근도 가능하다.

구조 분해는 componentN() 을 통해서 이루어진다

(보너스) Pair와 Triple 이 구조 분해가 가능한 이유

image

image

PairTriple 의 구현체는 Data Class 이다. 그래서 당연하게도 구조 분해가 가능한 것이다.

List, Array - ComponentN

image

image

ListArr 모두 Data Class 와 마찬가지로 componentN() 가 있다.

그런데 이상한 점은 li, arr 모두 값을 8개나 가지고 있는데 component는 5개만 존재한다.

image

그래서 구조 분해를 시도하니 위에서 확인했던 것과는 다르게 에러가 발생하고 있는 것을 볼 수 있다.

에러 내용을 보면 component6(), component7(), component8() 이 없다는 것 같다.

디컴파일 해보자

val li = listOf(1, 2, 3, 4, 5, 6, 7, 8)

val (n1, n2, n3, n4, n5) = li

컴파일 에러가 발생하면 디컴파일을 할 수 없기 때문에 구조 분해 개수를 줄이고 디컴파일을 해보았다.

image

이상한 점이 있다. 분명 Data Class 를 디컴파일 했을 때는 componentN() 으로 값을 할당하고 있었는데 List 를 구조 분해 해보니 get() 을 통해서 값을 할당하고 있다. 하지만 분명 component1() 과 같이 접근할 수는 있었다.

사실은 확장함수

image

image

사실 List 나 Array 는 자체적으로 componentN 메서드를 가지고 있는 것이 아니라 확장 함수로써 가지고 있다.

그리고 그 내용은 get() 을 통해서 가져오고 있기 때문에 디컴파일 해보면 get() 으로 나타나고 있던 것이다.

확장 함수가 정의된 곳은 List_Collections.kt, Array_Arrays.kt 이다.

Data ClassList, Array 가 다르게 작성된 이유는 아마도 ListArray 는 기존에 자바에서 제공되는 것을 사용하기 때문이고, Data class 는 코틀린에서 새롭게 설계되었기 때문에 더 유연한 것이 아닌가 생각한다.

componentN 직접 만들기

ListArray 에서 기본으로 만들어지는 componentN 은 1 ~ 5 이지만 더 많은 값을 구조 분해 하고싶은 경우가 있을 것이다.

코틀린 기본 라이브러리에서 1 ~ 5 번을 만든 것처럼 우리도 확장 함수를 사용해서 만들어볼 수 있다.

inline operator fun <T> List<T>.component6() = this[5]
operator fun <T> List<T>.component7() = this[6]

두 가지 방식으로 만들어 보았다. (inline 의 유무)

그리고 구조 분해를 해보자.

image

에러가 발생하지 않는다. 그럼 구조 분해를 해보자.

image

구조 분해 된 코드에서 var6(원래 코드에서 n6) 와 n7 의 차이를 확인해보자.

var6는 앞선 동작과 같이 get()을 통해서 값을 가져오지만 var7component7()을 호출하면서 값을 가져온다.

약간 주제에서 벗어난 이야기일 수 있지만 inline 키워드를 붙이면 함수를 호출하는 것이 아니라 함수에 정의된 기능을 코드에 직접 삽입하는 방식으로 이루어진다. 참조가 줄어들기 때문에 시스템 리소스 낭비를 줄일 수 있는 경우도 생긴다.

image

이번에는 똑같이 만드는데 자연스럽게 붙였던 operator 키워드를 빼고 만들어 보았다.

보이는 것과 같이 컴파일 에러가 뜨면서 operator 를 붙이라는 힌트를 보여준다.

구조 분해를 위해서는 operator 키워드가 필요하기 때문이다.

원하는 만큼 추가해서 사용해보자.


코틀린 코드를 자바 코드로 Decompile 하기

  1. 코틀린 코드를 작성하고 [Tools] - [Kotlin] - [Show Kotlin Bytecode] 를 선택한다

    image

  2. 오른쪽 창에 바이트 코드가 나타난다

    image

  3. 바이트 코드 창에서 상단에 있는 [Decompile] 버튼을 누른다. 그러면 새 창으로 디컴파일 된 자바 코드가 보인다.

    image