-
[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