본문으로 바로가기
반응형

이 글은 Kotlin In Action을 참고 하였습니다.

더욱 자세한 설명이나 예제는 직접 책을 구매하여 확인 하시기 바랍니다

7.3 Collection과 range의 convention

컬렉션에서 가장 많이 쓰는 연산은 읽기, 넣기, 원소가 있는지 확인하기(contains) 입니다.
코틀린에서는 특이하게 a[b] 처럼 배열 형태로 collection을 읽을수 있으며 이 또한 convention으로 처리합니다.

7.3.1 Index operator

배열은 array[index]형태로 접근하며, collection에서 같은 방법을 제공하기 위해 index 연산자로 get set을 제공합니다.
data class Point(val x: Int, val y: Int)

operator fun Point.get(index: Int): Int {
    return when(index) {
        0 -> x
        1 -> y
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array) {
    val p = Point(10, 20)
    println(p[1])
}

syntax는 아래와 같습니다.

x[a, b] -> x.get(a, b)

즉 배열의 원소를 읽거나 쓰는것처럼 표기하면 내부적으로 get, set함수로 변환됩니다.

get의 인자로 Int가 꼭 들어올 필요는 없습니다. map 같은 경우 key의 type이 인자로 들어오겠죠?


data class MutablePoint(var x: Int, var y: Int)

operator fun MutablePoint.set(index: Int, value: Int) {
    when(index) {
        0 -> x = value
        1 -> y = value
        else ->
            throw IndexOutOfBoundsException("Invalid coordinate $index")
    }
}

fun main(args: Array) {
    val p = MutablePoint(10, 20)
    p[1] = 42
    println(p)
}

set 역시 get과 동일하게 적용하면 됩니다.

인자는 여러개가 될수 있으며, 맨 마지막 인자가 value가 되고 나머지들은 index가 됩니다.

※set은 당연히 property가 var로 선언된 경우에만 가능합니다.


x[a, b] = c -> x.set(a ,b, c)


7.3.2 in convention

앞에서 범위안에 있는지를 검사할때 사용되던 in 역시 convention이며 contains 함수로 연결됩니다.

data class Point(val x: Int, val y: Int)

data class Rectangle(val upperLeft: Point, val lowerRight: Point)

operator fun Rectangle.contains(p: Point): Boolean {
    return p.x in upperLeft.x until lowerRight.x &&
           p.y in upperLeft.y until lowerRight.y
}

fun main(args: Array) {
    val rect = Rectangle(Point(10, 20), Point(50, 50))
    println(Point(20, 30) in rect)
    println(Point(5, 5) in rect)
}

a in c -> c.contains(a)

※참고로 1..10 은 1~10 까지이며, 1 until 10 은 1~9까지 입니다.


7.3.3 rangeTo convention

위에서 사용한 .. 역시 연사자 이며 rangeTo 함수로 표현됩니다.
fun main(args: Array) {
    val n = 9
    println(0..(n + 1))
    (0..n).forEach { print(it) }
}

start..end -> start.rangeTo(end)

Comparable 인터페이스에는 기본적인 rangeTo가 구현되어 있어 따로 만들지 않아도 가능합니다.

위 예제처럼 rangeTo 함수는 연산자 우선순위가 낫지만 괄호로 묶어서 명시적으로 사용하는게 좋습니다.


7.3.4 iterator for loop

for문에서 in 연산자를 사용하여 loop를 구현 합니다.
따라서 7.3.2와는 다른 in 이며 이는 iterator 함수와 연결됩니다.
import java.util.Date
import java.time.LocalDate

operator fun ClosedRange.iterator(): Iterator =
        object : Iterator {
            var current = start

            override fun hasNext() =
                current <= endInclusive

            override fun next() = current.apply {
                current = plusDays(1)
            }
        }

fun main(args: Array) {
    val newYear = LocalDate.ofYearDay(2017, 1)
    val daysOff = newYear.minusDays(1)..newYear
    for (dayOff in daysOff) { println(dayOff) }
}


위 예제를 보면 실제 in 연산자는 iterator의 next()와 hasNext()로 이루어진것을 알수 있습니다.


7.4 Destructuring declaration 과 component 함수

fun printEntries(map: Map) {
    for ((key, value) in map) {
        println("$key -> $value")
    }
}

data class Point(val x: Int, val y: Int)

fun main(args: Array) {
    val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
    printEntries(map)

   val p = Point(10, 20)
    val (x, y) = p
    println(x)
    println(y)
}

Map을 iteration 할때 key, value를 동시에 받아올 수 있습니다.

이는 코틀린 표준 라이브러리에서 Map대한 확장 함수로 iterator를 지원하며, Map.Entry에 대한 확장 함수로 component1 (key)과 component2 (value)를 제공합니다.

또한 변수를 두개 이상 지정하고 property를 한번에 받아오는것도 가능하면 이것을 destructuring이라 합니다.

이는 내부적으로 componentN (N은 숫자)의 함수와 연결된 convention 입니다.

val (a, b) = p -> val a = p.component1

                   val b = p.component2

data class는 property 순서대로 componentN을 자동 생성해 줍니다.

일반 클래스라도 componentN()을 operator fun으로 선언하면 사용이 가능합니다.


특히 이런 구조분해는 함수의 리턴값이 여러개 일때 유용하게 쓰일 수 있습니다.

data class NameComponents(
        val name: String,
        val extension: String)

fun splitFilename(fullName: String): NameComponents {
    val (name, extension) = fullName.split('.', limit = 2)
    return NameComponents(name, extension)
}

fun main(args: Array) {
    val (name, ext) = splitFilename("example.kt")
    println(name)
    println(ext)
}

위 예제에서는 split 함수로 반환된 List 역시 component 함수로 받았습니다.

배열과 collection에서도 destructuring을 지원하나, 무한정 지원할 수는 없기 때문에 5개 까지만 허용합니다.


또한 Pair을 이용하거나, 코틀린에서 제공하는 Triple을 이용하는 방법도 있습니다.


7.5 Property의 delegation

property의 위임이란 property에 대한 get(), set() 동작을 특정 객체가 처리하도록 위임하는걸 말합니다.
이를 이용하면, property의 값을 field가 아니라 db table이나 브라우저의 세션, 맵등에 저장할 수 있습니다.

위임은 앞에서 class의 위임에 나온것 처럼 by를 이용합니다.

class Foo {
    var p: type by Delegate()
}

실제 이 코드는 아래와 같이 풀어 집니다.

class Delegate {
    operator fun getValue(param1,param2....) = {...}
    operator fun setValue(param1,param2....,value) = {...}
}

class Foo {
    private val delegate = Delegate()
    var p: Type
    set(value: Type) = delegate.setValue(param1,param2....,value)
    getValue() = delegate.getValue(param1,param2....)
}

즉 by를 통해서 property의 동작을 위임받는 class는 getValue(), setValue()를 반드시 구현해야 합니다. (convention임)


7.5. by lazy()를 이용한 초기화 지연

property 중에 초기화를 미뤄야 하는것들이 존재 할 수 있습니다.
예를 들면 데이터를 네트워크나, DB에서 읽어와 사용하고 한번만 사용하면 읽어와서 사용하는 경우 아래와 같이 코드를 구성할 수 있습니다.
class Email { /*...*/ }
fun loadEmails(person: Person): List {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}

class Person(val name: String) {
    private var _emails: List? = null

    val emails: List
       get() {
           if (_emails == null) {
               _emails = loadEmails(this)
           }
           return _emails!!
       }
}

fun main(args: Array) {
    val p = Person("Alice")
    p.emails // 이때 한번만 로그가 찍힌다. -> Load emails for....
    p.emails
}


한번만 읽기위해 var _emails라는 내부 property를 선언해서 사용했습니다.

emails 는 val type이기 때문에 변수를 두개 사용할 수 밖에 없습니다.

단 이 작업을 아래 한줄로 바꿀수 있습니다.

class Email { /*...*/ }
fun loadEmails(person: Person): List {
    println("Load emails for ${person.name}")
    return listOf(/*...*/)
}

class Person(val name: String) {
    val emails by lazy { loadEmails(this) }
}

fun main(args: Array) {
    val p = Person("Alice")
    p.emails
    p.emails
}

by lazy {....}를 사용하면 아래와 같은 동작을 보장합니다.

  • 한번만 초기화 한다
  • 초기화시 람다{...}의 구문이 사용된다.
  • Thread-safe 하다.
  • lock이 따로 필요하다면 넘길 수 있다.
  • Thread-safe할 필요가 없다면 없게 할수 있다.

7.5.3 Delegation property with observing pattern

Property가 변경될때 UI에 변경을 알리거나, 다른곳에 알림을 주기위해 PropertyChangeSupport와 PropertyChangeEvent 클래스를 사용하는 경우가 많습니다. 기본적으로는 event 발생시 subscriber에게 notify해주는 observer pattern이라고 보시면 됩니다.

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener

open class PropertyChangeAware {
    protected val changeSupport = PropertyChangeSupport(this)

    fun addPropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.addPropertyChangeListener(listener)
    }

    fun removePropertyChangeListener(listener: PropertyChangeListener) {
        changeSupport.removePropertyChangeListener(listener)
    }
}

class Person(
        val name: String, age: Int, salary: Int
) : PropertyChangeAware() {

    var age: Int = age
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
                    "age", oldValue, newValue)
        }

    var salary: Int = salary
        set(newValue) {
            val oldValue = field
            field = newValue
            changeSupport.firePropertyChange(
                    "salary", oldValue, newValue)
        }
}

fun main(args: Array) {
    val p = Person("Dmitry", 34, 2000)
    p.addPropertyChangeListener(
        PropertyChangeListener { event ->
            println("Property ${event.propertyName} changed " +
                    "from ${event.oldValue} to ${event.newValue}")
        }
    )
    p.age = 35
    p.salary = 2100
}

기본적인 방법입니다.

봉급과 나이가 바뀌면 listener에게 notify해주는 형태입니다.

Property 변경시 notify 해주도록 set을 custom하게 변경한 형태 입니다.


하지만 setter에 중복 코드가 많으니 이를 class로 추출해 보겠습니다.


open class PropertyChangeAware {
    // 위와동일
}

class ObservableProperty(val propName: String, var propValue: Int,
val changeSupport: PropertyChangeSupport) {
    fun getValue(): Int = propValue
    fun setValue(newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(propName, oldValue, newValue)
    }
}

class Person(val name: String, age: Int, salary: Int: PropertyChangeAware() {
    val _age = ObservableProperty("age", age, changeSupport)
    var age: Int
        get() = _age.getValue()
        set(value) { _age.setValue(value) }

    val _salary = ObservableProperty("salary", salary, changeSupport)
    var salary: Int
        get() = _salary.getValue()
        set(value) { _salary.setValue(value) }
}

fun main(args: Array) {
       // 위와동일
}
단순히 setter의 중복 부분을 class로 추출한 형태입니다.

하지만 여기서도 _age 객체나, _salary 객체를 추가적으로 만들어 사용해야 합니다.

이 코드를 얼마나 더 단순화 시킬수 있는지 아래 예제와 비교해 보지요.

import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
import kotlin.reflect.KProperty

open class PropertyChangeAware {
    // 위와 동일
}

class ObservableProperty(var propValue: Int, val changeSupport: PropertyChangeSupport) {
    operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue

    operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
        val oldValue = propValue
        propValue = newValue
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }
}
class Person(val name: String, age: Int, salary: Int: PropertyChangeAware() {

    var age: Int by ObservableProperty(age, changeSupport)
    var salary: Int by ObservableProperty(salary, changeSupport)
}

fun main(args: Array) {
    // 위와 동일
}

get/set convention에 따라 operator를 갖는 class를 만들었습니다.

이때 첫번째 인자로 receiver object를 받습니다. 따라서 person 객체를 받아오고 두번째 param으로 KProperty를 받아옵니다.

이는 추후에 (10장...) 자세히 언급되지만 여기서는 prop.name을 사용하여 메소드가 사용할 property 이름을 얻어온다고만 알고 있으면 됩니다.

따라서 인자에서 따로 propery name을 받아올 필요도 없습니다.


위 코드는 사실 컴파일러가 내부적으로 이전 코드와 같은 작업을 해줍니다.
따라서 개발자는 좀더 짧은 코드로 같은 동작을 하도록 만들 수 있습니다.


마지막으로 이같은 작업을 대신해 주는 kotlin 기본 라이브러리 class가 있습니다.
이미 정의된 ObservableProperty를 이용하면, operator를 재 선언할 필요가 없으며, 변경시 해야할 동작을 lambda로 넘겨주면 됩니다.
import java.beans.PropertyChangeSupport
import java.beans.PropertyChangeListener
import kotlin.properties.Delegates
import kotlin.reflect.KProperty

open class PropertyChangeAware {
  // 위와 동일

class Person(val name: String, age: Int, salary: Int) : PropertyChangeAware() {

    private val observer = {
        prop: KProperty<*>, oldValue: Int, newValue: Int ->
        changeSupport.firePropertyChange(prop.name, oldValue, newValue)
    }

    var age: Int by Delegates.observable(age, observer)
    var salary: Int by Delegates.observable(salary, observer)
}

fun main(args: Array) {
   // 위와 동일
}

by의 우항은 꼭 새로운 instance를 만들어낼 필요는 없습니다.

함수나, 다른 property일수도 있습니다만 결론적으로 생성된 클래스가 getValue와 setValue만 명확하게 제공하면 됩니다.


7.5.4 Delegation compile

by 키워드를 사용시 컴파일러는 우항의 객체를 숨겨진 객체에 저장합니다.
그리고 해당 객체에 접근시 <delegate>라는 이름을 쓰고, property의 정보를 담기위해 KProperty 객체를 사용합니다.
KProperty는 <property>로 표현됩니다.

 // 실제 코트
class Foo {
    var prop: Type by DelegationObj()
}

fun main) {
    val c = Foo()
}

// 컴파일된 표현
class Foo {
    private val  = DelegationObj()
    var prop: Type
        get() = .getValue(this, )
        set() = .setValue(this, , value)
}

val x = c.prop -> val x = <delegate>.getValue(c, <property>)

c.prop = y -> <delegate>.setValue(c, <property>, y)


by를 이용하면 간단한 표현으로 property 동작의 재구성이 가능해 집니다.

이는 property는 저장하는 위치를 맵, DB, network등에 할수 있도록 하거나, 읽을때 추가작업을 넣을때 편리하게 사용할 수 있습니다.


7.5.5 Map에 property 값 저장

Property에 값을 동적으로 처리하는 기능을 제공하는 object를 expando object라고 합니다.
class Person {
    private val _attributes = hashMapOf()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String
        get() = _attributes["name"]!!
}

fun main(args: Array) {
    val p = Person()
    val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
    for ((attrName, value) in data)
       p.setAttribute(attrName, value)
    println(p.name)
}

위 예제는 사람정보를 저장할때 이름은 기본정보로 저장하고, 추가적인 정보는 Map 담는 경우 입니다.

저장은 for 문으로 하고, p.name으로 이름을 가져올때는 "name"을 key로 갖는 정보를 정보를 가지고 옵니다.


다만 Map이나 MutableMap에서는 인터페이스에서 getValue와 setValue 모두를 제공하기 때문에 아래와 같이 더 간결하게 사용할 수 있습니다.

class Person {
    private val _attributes = hashMapOf()

    fun setAttribute(attrName: String, value: String) {
        _attributes[attrName] = value
    }

    val name: String by _attributes
}

fun main(args: Array) {
  //의와 동일
}

반응형