ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Kotlin in Action: 2/e] 9장
    스터디 2026. 5. 24. 20:56

    연산자 오버로딩과 다른 관계

    - 연산자 오버로딩

    - 관례: 여러 연산을 지원하기 위해 특별히 이름이 붙은 메서드

    - 위임 프로퍼터

     

    관례: 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법

    - 특정 함수를 구현하면 -> 언어의 기능을 사용할 수 있다

    9.1 산술 연산자

    9.1.1 이항 산술 연산 오버로딩

    - operator 키워드를 붙여서 관례를 따르는 함수임을 표현한다

    - 내부적으로는 a.plus(b)가 실행된다

    data class Point(val x: Int, val y: Int)
    
    operator fun Point.plus(other: Point): Point {
        return Point(x + other.x, y + other.y)
    }
    
    fun main() {
        val p1 = Point(10, 20)
        val p2 = Point(30, 40)
        println(p1 + p2)
        // Point(x=40, y=60)
    }

     

    두 피연산자의 타입이 다른 연산자 정의하기

    - 자동으로 교환법칙을 지원하지 않기 때문에 필요시 Double 타입에서도 구현해야 한다

    operator fun Point.times(scale: Double): Point {
        return Point((x * scale).toInt(), (y * scale).toInt())
    }
    
    fun main() {
        val p = Point(10, 20)
        println(p * 1.5)
        // Point(x=15, y=30)
    }

     

    operator 함수도 오버로딩해서 이름이 같고 파라미터가 다른 연산자를 여러개 만들 수 있다

     

    9.1.2 복합 대입 연산자 오버로딩

    - 복합 대입 연산자는 +=, -= 과 같이 대임과 산술 연산을 하나로 합친 연산자이다

    - a += b 는 a = a.plus(b)와 a.plusAssign(b) 중 하나로 해석된다

    - 불변 객체에는 plus, 변경 가능한 객체에는 plusAssign를 사용하도록 설계하자

     

    9.1.3 단항 연산자 오버로딩

    - 단항 연산자는 인자가 없다

    data class Point(val x: Int, val y: Int)
    
    operator fun Point.unaryMinus(): Point {
        return Point(-x, -y)
    }
    
    fun main() {
        val p = Point(10, 20)
        println(-p)
        // Point(x=-10, y=-20)
    }

     

     

    9.2 비교 연산자

    9.2.1 동등성 연산자: equals

    - a == b 는 a?.equals(b) ?: (b == null)로 해석된다

    - 데이터 클래스의 경우 자동으로 equals를 생성해준다

    - Any에 정의된 메서드이므로 override를 해야한다(override하는 메서드에 operator가 붙어있어서 operator는 붙이지 않아도 된다)

    - Any에서 상속받은 equals가 확장함수보다 우선순위가 높기 때문에 equals를 확장함수로 정의할 수는 없다

    - ===은 같은 객체를 가리키는지 비교하기 때문에 오버로딩 할 수 없다

    class Point(val x: Int, val y: Int) {
        override fun equals(other: Any?): Boolean {
            if (other === this) return true
            if (other !is Point) return false
            return other.x == x && other.y == y
        }
    }
    
    fun main() {
        println(Point(10, 20) == Point(10, 20))
        // true
        println(Point(10, 20) != Point(5, 5))
        // true
        println(null == Point(1, 2))
        // false
    }

     

    9.2.2 순서 연산자: compareTo

    - 자바에서는 Comparable 인터페이스를 구현해서 element1.compareTo(element2)를 사용해야 한다

    - a >= b 는 a.compareTo(b) >= 0으로 해석된다

    - 코틀린에서도 Comparable의 compareTo를 구현하면 된다

    class Person(
        val firstName: String, val lastName: String,
    ) : Comparable<Person> {
    
        override fun compareTo(other: Person): Int {
            return compareValuesBy(
                this, other,
                Person::lastName, Person::firstName
            )
        }
    }
    
    fun main() {
        val p1 = Person("Alice", "Smith")
        val p2 = Person("Bob", "Johnson")
        println(p1 < p2)
        // false
    }

     

    9.3 컬렉션과 범위에 쓸 수 있는 관례

    9.3.1 인덱스로 원소 접근: get, set

    - a[b] 형태로 사용하는 것을 인덱스 접근 연산자라고 부른다

    - x[a, b] 는 x.get(a, b)로 해석된다(get의 파라미터로 Int가 아닌 타입도 가능하다)

    - x[a, b] = c 는 x.set(a, b, c)로 해석된다

    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() {
        val p = Point(10, 20)
        println(p[1])
        // 20
    }
    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() {
        val p = MutablePoint(10, 20)
        p[1] = 42
        println(p)
        // MutablePoint(x=10, y=42)
    }

     

    9.3.2 어떤 객체가 컬렉션에 들어있는지 검사: in

    - a in c 는 c.contains(a)로 해석된다

    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..<lowerRight.x &&
                p.y in upperLeft.y..<lowerRight.y
    }
    
    fun main() {
        val rect = Rectangle(Point(10, 20), Point(50, 50))
        println(Point(20, 30) in rect)
        // true
        println(Point(5, 5) in rect)
        // false
    }

     

    9.3.3 객체로부터 범위 만들기: rangeTo, rangeUntil

    - start..end는 start.rangeTo(end)로 해석된다

    - 만약 Comparablea 인터페이스를 구현했다면 자동으로 만들어진다(Comparable의 확장함수로 표준 라이브러리에서 구현해두었다)

     

    9.3.4 자신의 타입에 대해 루프 수행: iterator

    - for (x in list) { ... }와 같은 반복문은 내부적으로 list.iterator()를 호출해서 사용한다

    - iterator를 구현한다면 반복문에 사용할 수 있다

     

    import java.time.LocalDate
    
    operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
        object : Iterator<LocalDate> {
            var current = start
    
            override fun hasNext() =
                current <= endInclusive
    
            override fun next(): LocalDate {
                val thisDate = current
                current = current.plusDays(1)
                return thisDate
            }
        }
    
    fun main() {
        val newYear = LocalDate.ofYearDay(2042, 1)
        val daysOff = newYear.minusDays(1)..newYear
        for (dayOff in daysOff) {
            println(dayOff)
        }
        // 2041-12-31
        // 2042-01-01
    }

    - rangeTo는 ClosedRange 인스턴스를 반환

    - ClosedRange<LocalDate>의 확장함수 iterator가 정의되어 있어 for 루프에서 사용

     

    9.4 구조 분해 선언: component

    - val (a, b) = p 는 각각 val a = p.component1(), val b = p.component2()로 해석된다

    - data 클래스의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로 componentN 함수를 만들어준다

    - Pair, Triple을 사용해도 되지만 의미를 표현하기 어렵다

    data class NameComponents(
        val name: String,
        val extension: String,
    )
    
    fun splitFilename(fullName: String): NameComponents {
        val result = fullName.split('.', limit = 2)
        return NameComponents(result[0], result[1])
    }
    
    fun main() {
        val (name, ext) = splitFilename("example.kt")
        println(name)
        // example
        println(ext)
        // kt
    }

     

    - 컬렉션의 경우 표준 라이브러리에서 맨 앞의 다섯 원소에 대해 componentN을 제공한다

    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)
    }

     

    9.4.1 구조 분해 선언과 루프

    fun printEntries(map: Map<String, String>) {
        for ((key, value) in map) {
            println("$key -> $value")
        }
    }
    
    fun main() {
        val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
        printEntries(map)
        // Oracle -> Java
        // JetBrains -> Kotlin
    }

     

    9.4.2 _ 문자를 사용해 구조 분해 값 무시

    - 필요없다면 _을 사용해서 무시 가능하다

     

    코틀린 구조 분해의 한계와 단점

    - 구조 분해가 componentN 함수에 대한 순차적 호출로 변환되기 때문에 데이터 클래스의 프로퍼티 순서를 바꾸면 문제가 될 수 있다

    - 이름 기반 구조 분해는 아직 도입 전

     

    9.5 프로퍼티 접근자 로직 재활용: 위임 프로퍼티(delegated property)

    - 프로퍼티는 위임을 사용하 지산의 값을 필드가 아니라 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있다

    - 위임: 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하도록 맡기는 디자인 패턴

    - 위임 객체: 작업을 처리하는 도우미 객체

     

    9.5.1 위임 프로퍼티의 기본 문법과 내부 동작

    - var p: Type by Delegate()

     

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

    - Delegate 클래스는 getValue, setValue 메서드를 제공해야 하며, 변경 가능한 프로퍼티만 setVAlue를 사용한다

    - 위임 객체는 선택적으로 provideDelegate 함수 구현을 제공할 수도 있다

     

    class Delegate {
        operator fun getValue(...) {...}
        operator fun setValue(..., value: Type) {...}
        operator fun provideDelegate(...): Delegate {...}
    }
    
    class Foo {
        var p: Type by Delegate()
    }
    
    fun main() {
        val foo = Foo() 		// delegate.provideDelegate
        val oldValue = foo.p 	// delegate.getValue
        foo.p = newValue 		// delegate.setValue
    }

     

    9.5.2 위임 프로퍼티 하용: by lazy()를 사용한 지연 초기화

    - 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화할 때 쓰이는 패턴

    - 초기화 과정에 자원을 많이 사용하거나 객체를 사용할 때마다 꼭 초기화하지 않아도 되는 프로퍼티에 대해 사용한다

    class Email { /*...*/ }
    
    // 이게 만약 오래걸리는 작업이라면
    fun loadEmails(person: Person): List<Email> {
        println("Load emails for ${person.name}")
        return listOf(/*...*/)
    }
    
    class Person(val name: String) {
        private var _emails: List<Email>? = null
    
        val emails: List<Email>
            get() {
            	// 최초 접근시에만 이메일을 가져온다
                if (_emails == null) {
                    _emails = loadEmails(this)
                }
                return _emails!!
            }
    }
    
    fun main() {
        val p = Person("Alice")
        p.emails
        // Load emails for Alice
        p.emails
    }

     

    위임 프로퍼티를 이용한다면?

    - lazy 함수는 관례에 맞는 시그니처의 getValue가 들어있는 객체를 반환한다

    - lazy 함수의 인자는 값을 초기화하 할 람다다

    - lazy 함수는 기본적으로 스레드 안전하다(필요함하면 락을 전달하거나 동기화를 생략하게 할 수도 있다)

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

     

     

    9.5.3 위임 프로퍼티 구현

    - 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내고 싶다면? -> observable

     

    그냥 구현해보자

    fun interface Observer {
        fun onChange(name: String, oldValue: Any?, newValue: Any?)
    }
    
    open class Observable {
        val observers = mutableListOf<Observer>()
        fun notifyObservers(propName: String, oldValue: Any?, newValue: Any?) {
            for (obs in observers) {
                obs.onChange(propName, oldValue, newValue)
            }
        }
    }
    
    
    class Person(val name: String, age: Int, salary: Int) : Observable() {
        var age: Int = age
            set(newValue) {
                val oldValue = field
                field = newValue
                notifyObservers(
                    "age", oldValue, newValue
                )
            }
    
        var salary: Int = salary
            set(newValue) {
                val oldValue = field
                field = newValue
                notifyObservers(
                    "salary", oldValue, newValue
                )
            }
    }
    
    fun main() {
        val p = Person("Seb", 28, 1000)
        p.observers += Observer { propName, oldValue, newValue ->
            println(
                """
                Property $propName changed from $oldValue to $newValue!
                """.trimIndent()
            )
        }
        p.age = 29
        // Property age changed from 28 to 29!
        p.salary = 1500
        // Property salary changed from 1000 to 1500!
    }

     

    프로퍼티의 값이 변경될 때 통지하는 부분을 클래스로 분리한다면

    class ObservableProperty(
        val propName: String,
        var propValue:
        Int,
        val observable: Observable,
    ) {
        fun getValue(): Int = propValue
        fun setValue(newValue: Int) {
            val oldValue = propValue
            propValue = newValue
            observable.notifyObservers(propName, oldValue, newValue)
        }
    }
    
    class Person(val name: String, age: Int, salary: Int) : Observable() {
        val _age = ObservableProperty("age", age, this)
        var age: Int
            get() = _age.getValue()
            set(newValue) {
                _age.setValue(newValue)
            }
    
        val _salary = ObservableProperty("salary", salary, this)
        var salary: Int
            get() = _salary.getValue()
            set(newValue) {
                _salary.setValue(newValue)
            }
    }

     

    프로퍼티 관례에 맞게 수정한 후 위임 프로퍼티를 사용한다면

    import kotlin.reflect.KProperty
    
    class ObservableProperty(var propValue: Int, val observable: Observable) {
        operator fun getValue(thisRef: Any?, prop: KProperty<*>): Int = propValue
    
        operator fun setValue(thisRef: Any?, prop: KProperty<*>, newValue: Int) {
            val oldValue = propValue
            propValue = newValue
            observable.notifyObservers(prop.name, oldValue, newValue)
        }
    }
    
    class Person(val name: String, age: Int, salary: Int) : Observable() {
        var age by ObservableProperty(age, this)
        var salary by ObservableProperty(salary, this)
    }

     

    표준 라이브러리를 사용한다면

    import kotlin.properties.Delegates
    import kotlin.reflect.KProperty
    
    class Person(val name: String, age: Int, salary: Int) : Observable() {
        private val onChange = {
                property: KProperty<*>, oldValue: Any?,
                newValue: Any?,
            ->
            notifyObservers(property.name, oldValue, newValue)
        }
    
        var age by Delegates.observable(age, onChange)
        var salary by Delegates.observable(salary, onChange)
    }

     

    9.5.4 위임 프로퍼티는 커스텀 접근자가 있는 감춰진 프로퍼티로 변환된다

    class C {
    	var prop: Type by MyDelegate()
    }
    
    val x = c.prop
    // val x = <delegate>.getValue(c, <property>)
    
    c.prop = x
    // <delegate>.setValue(c, <property>, x)

     

    9.5.5 맵에 위임해서 동적으로 애트리뷰트 접근

    class Person {
        private val _attributes = mutableMapOf<String, String>()
    
        fun setAttribute(attrName: String, value: String) {
            _attributes[attrName] = value
        }
    
        var name: String
            get() = _attributes["name"]!!
            set(value) {
                _attributes["name"] = value
            }
    }
    
    fun main() {
        val p = Person()
        val data = mapOf("name" to "Seb", "company" to "JetBrains")
        for ((attrName, value) in data)
            p.setAttribute(attrName, value)
        println(p.name)
        // Seb
        p.name = "Sebastian"
        println(p.name)
        // Sebastian
    }

     

    위임 프로퍼티를 사용한다면
    - 표준 라이브러리가 Map, MutableMap 인터페이스에 대해 getValue, setValue를 제공한다

    - p.name -> _attributes.getValue(p, prop)

    class Person {
        private val _attributes = mutableMapOf<String, String>()
    
        fun setAttribute(attrName: String, value: String) {
            _attributes[attrName] = value
        }
    
        var name: String by _attributes
    }

     

    9.5.6 실전 프레임워크가 위임 프로퍼티를 활용하는 방법

    - name, age는 Column 클래스의 getValue, setValue를 사용하게 된다

    object Users : IdTable() {							// 데이터베이스 테이블
        val name = varchar("name", length = 50).index() // 테이블 컬럼
        val age = integer("age")
    }
    
    class User(id: EntityID) : Entity(id) {
        var name: String by Users.name
        var age: Int by Users.age
    }

     

    '스터디' 카테고리의 다른 글

    [Kotlin in Action: 2/e] 14장  (0) 2026.06.28
    [Kotlin in Action: 2/e] 13장  (0) 2026.06.20
    [Kotlin in Action: 2/e] 4장  (0) 2026.04.25
    [Kotlin in Action: 2/e] 3장  (0) 2026.04.25
Designed by Tistory.