- 관례(convention): 어떤 언어 기능과 미리 정해진 이름의 함수를 연결해주는 기법
- 예: 어떤 클래스 안에
plus
라는 이름의 메소드를 정의하면 그 클래스의 인스턴스에 대해 +
연산자를 사용할 수 있다. - 이유: 기존 자바 클래스를 코틀린 언어에 적용하기 위함
산술 연산자 오버로딩#
이항 산술 연산 오버로딩#
- 연산자를 오버로딩하는 함수 앞에는 꼭
operator
키워드가 있어야 한다.
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
- 프로젝트 안에서 직접 작성한 클래스에 대해 관례를 따르는 확장 함수를 만들어도 역시 잘 작동한다.
- 오버로딩 가능한 이항 산술 연산자
식 | 함수 이름 |
---|
a * b | times |
a / b | div |
a % b | mod(1.1부터 rem) |
a + b | plus |
a - b | minus |
- 직접 정의한 함수를 통해 구현하더라도 연산자 우선순위는 언제나 표준 숫자 타입에대한 연산자 우선순위와 같다.
- 연산자 함수와 자바
- 자바를 코틀린에서 호출하는 경우에는 함수 이름이 코틀린의 관례에 맞아 떨어지기만 하면 항상 연산자 식을 사용해 그 함수를 호출할 수 있다.
- 코틀린 연산자가 자동으로 교환 법칙을 지원하지는 않는다.
- 비트 연산자에 대해 특별한 연산자 함수를 사용하지 않는다.
- 중의 연산자 표기법을 지원하는 일반 함수를 사용해 비트 연산을 수행한다.
shl
: 왼쪽 시프트(자바 <<
)shr
: 오른쪽 시프트(부호 비트 유지, 자바 >>
)ushr
: 오른쪽 시프트(0으로 부호 비트 설정, 자바 >>>
)and
: 비트 곱(자바 &
)or
: 비트 합(자바 |
)xor
: 비트 배타 합(자바 ^
)inv
: 비트 반전(자바 ~
)
복합 대입 연산자 오버로딩#
var p1 = Point(1, 2)
p1 += Point(2, 3)
+=
연산이 객체에 대한 참조를 다른 참조로 바꾸기보다 원래 객체 내부 상태를 변경하고 싶은 경우는, 반환 타입이 Unit
인 plusAssign
함수를 정의하면 된다.- 이론적으로
+=
를 plus
, plusAssign
양쪽으로 컴파일할 수 있다.- 두 함수가 모두 정의되어 있고,
+=
에 사용가능한 경우 컴파일러는 오류를 보고한다. var
를 val
로 바꿔서 plusAssign
적용이 불가능하게 할 수 도 있다.- 가능하면
plus
와 plusAssign
연산을 동시에 정의하지 말라.
- 코틀린 표준 라이브러리는 컬렉션에 대해 두 가지 접근 방법을 제공한다.
+
, -
는 항상 새로운 컬렉션을 반환+=
, -=
연산자는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태 변화- 읽기 전용 컬렉션에서
+=
와 -=
는 변경을 적용한 복사본을 반환
단한 연산자 오버로딩#
식 | 함수 이름 |
---|
+a | times |
-a | div |
!a | mod(1.1부터 rem) |
++a , a++ | plus |
--a , a-- | minus |
비교 연산자 오버로딩#
동등성 연산자: equals#
==
연산자 호출을 equals
메소드 호출로 컴파일한다.!=
연산자를 사용하는 식도 equals
호출로 컴파일된다.==
와 !=
는 내부에 인자가 널인지 검사하므로 다른 연산과 달리 널이 될 수 있는 값에도 적용할 수 있다.

equals
함수는 Any
의 equals
를 오버라이드하면서 operator
를 붙이지 않아도 된다. Any
의 equals
에는 operator
가 붙어있다.
순서 연산자: compareTo#
- 자바에서
Comparable
인터페이스를 구현해야 클래스를 비교할 수 있다.Complarable
에 들어있는 compareTo
메소드는 한 객체와 다른 객체의 크기를 비교해 정수로 나타내준다.
- 코틀린도 똑같은
Comparable
인터페이스를 지원한다. - 비교 연산자(
<
, >
. <=
, >=
)는 compareTo
호출로 컴파일된다. - 코틀린 표준 라이브러리
comparaValuesBy
- 인자로 넣은 여러 개의 식으로 두 객체의 대소를 알려주는 0이 아닌 값이 처음 나올때까지 인자로 받은 함수를 차례로 호출해 두 값을 비교하며, 모든 함수가 0을 반환하면 0을 반환한다.
class Person(
val firstName: String, val lastName: String
) : Comparable<Person> {
override fun compareTo(other: Person): Int {
return compareValuesBy(this, other, Person::lastName, Person::firstName)
}
}
컬렉션과 범위에 대해 쓸 수 있는 관례#
인덱스로 원소에 접근: get과 set#
get()
메소드에 operator
변경자를 붙이면, 각괄호를 사용한 연산을 할 수 있다.
data class Point(val x: Int, val y: Int) {
operator fun get(index: Int): Int {
return when (index) {
0 -> x
1 -> y
else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
}
val p1 = Point(100, 200)
println(p1[1])
- 2차원 행렬이나 배열을 표현하는 클래스에서
operator fun get(rowIndex: Int, colIndex: Int)
를 정의 하면 matrix[row, col]
로 그 메소드를 호출할 수 있다. set()
도 비슷하게 지정이 가능하다.
operator fun set(index: Int, value: Int) {
when (index) {
0 -> x = value
1 -> y = value
else -> throw IndexOutOfBoundsException("Invalid coordinate $index")
}
}
in 관례#
in
연산자와 대응 하는 함수는 contains()
다.
class Rectangle(val upperLeft: Point, val lowerRight: Point) {
operator fun contains(p: Point): Boolean {
return p.x in upperLeft.x until lowerRight.x &&
p.y in upperLeft.y until lowerRight.y
}
}
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect)
rangeTo 관례#
..
연산자는 rangeTo()
함수를 간략하게 표현하는 방법이다.- 이 연산자는 아무 클래스에나 정의할 수 있다.
- 하지만
Comparable
인터페이스를 구현하면 rangeTo()
를 정의할 필요가 없다.- 코틀린 표준 라이브러리에는 모든
Comparable
객체에 대해 적용 가능한 rangeTo
함수가 들어있다.
operator fun <T: Comparable<T>> T. rangeTo (that: T) : ClosedRange<T>
for 루프를 위한 iterator 관례#
for
루프 안에서 in
연산자는 의미가 다르다.iterator()
함수를 호출해서 이터레이터를 얻은 다음, hasNext()
와 next()
호출을 반복하는 식으로 변환한다.
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
object : Iterator<LocalDate> {
var current = start
override fun hasNext(): Boolean {
return current <= endInclusive
}
override fun next(): LocalDate {
return current.apply {
current = plusDays(1)
}
}
}```
```kotlin
val newYear = LocalDate.ofYearDay(2017, 1)
for (dayOff in newYear.minusDays(1)..newYear) {
println(dayOff)
}
구조 분해 선언과 component 함수#
- 구조 분해 선언(destructuring declaration): 복합적인 값을 분해해서 여러 다른 변수를 한꺼번에 초기화할 수 있다.
- 구조분해 선언은
componentN()
이라는 함수를 호출하는 관례를 사용한다. - data 클래스의 주 생성자에 들어있는 프로퍼티에 대해서는 컴파일러가 자동으로
componentN
함수를 만들어준다. - 컬렉션의 경우 코틀린 표준 라이브러리에서 맨 앞 다섯 원소에 대한
componentN
을 제공한다.- 다만 컬렉션 크기를 벗어나는 위치의 원소에 대한 구조 분해 선언을 사용하면
IndexOutOfBoundsException
등의 예외가 발생한다.
- 표준 라이브러리의
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)
println(ext)
}
구조 분해 선언과 루프#
- 구조 분해 선언은 변수 선언이 들어갈 수 있는 장소라면 어디든 사용 가능하다.
- 아래는 iterator와 componentN 두 가지 관례를 사용했다.
fun printEntries(map: map<String, String>) {
for ((key, value) in map) {
println("$key -> $value")
}
}
프로퍼티 접근자 로직 재활용: 위임 프로퍼티#
- 위임: 객체가 직접 자신의 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인 패턴
- 위임 객체(delegate): 작업을 처리하는 도우미 객체
위임 프로퍼티 소개#
- 위임 프로퍼티의 일반적인 문법은 다음과 같다.
p
프로퍼티는 접근자 로직을 Delegate
클래스의 객체에게 위임한다.
class Foo {
var p: Type by Delegate()
}
- 다음과 같이 컴파일러는 숨겨진 도우미 프로퍼티를 만들고 그 프로퍼티를 위임 객체의 인스턴스로 초기화 한다.
p
프로퍼티는 바로 그 위임 객체에게 자신의 작업을 위임한다.- 프로퍼티 위임 관례에 따르는 Delegate 클래스는
getValue
와 setValue
(변경이 가능한 경우) 메소드를 제공해야 한다. - 관례를 사용하는 다른 경우와 마찬가지로
getValue
와 setValue
는 멤버 메소드이거나 확장 함수일 수 있다.



위임 프로퍼티 사용: by lazy()를 사용한 프로퍼티 초기화 지연#
- 지연 초기화(lazy initialization): 객체의 일부분을 초기화하지 않고 남겨뒀다가 실제로 그 부분의 값이 필요할 경우 초기화하는 패턴
- 뒷받침하는 프로퍼티(backing property)라는 기법을 사용한다.
_emails
라는 프로퍼티는 값을 저장하고, 다른 프로퍼티인 emails
는 _emails
라는 프로퍼티에 대한 읽기 연산을 제공한다.


- 위임 프로퍼티를 사용하면 위 코드가 더 간단해진다.
- 위임 프로퍼티는 데이터를 저장할 떄 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다.
lazy
함수를 통해 이를 사용할 수 있다.lazy
함수의 인자는 값을 초기화할 때 호출할 람다다.lazy
함수는 기본적으로 스레드 안전하다. 하지만 필요에 따라 동기화에 사용할 락을 함꼐 전달할 수도 있다.

위임 프로퍼티 구현#
- 어떤 객체의 프로퍼티가 바뀔 때마다 리스너에게 변경 통지를 보내기 구현
- 자바에서는
PropertyChangeSupport
와 PropertyChangeEvent
클래스를 사용해 구현한다.PropertyChangeSupport
: 리스너의 목록을 관리PropertyChangeEvent
: 이벤트가 들어오면 목록의 모든 리스너테에 이벤트를 통지한다.- 자바 빈 클래스의 필드에
PropertyChangeSupport
인스턴스를 저장하고 프로퍼티 변경 시 그 인스턴스에게 처리를 위임하는 방식으로 구현한다.
- 모든 클래스에
PropertyChangeSupport
를 추가하고 싶지 않으므로, 변경 통지를 할 클래스의 부모 타입이 될 PropertyChangeAware
클래스를 만든다. - 변경 통지를 위임할
ObservableProperty
클래스를 만든다.
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
open 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 DelegatedPerson(
val name: String, age: Int
) : PropertyChangeAware() {
val _age = ObservableProperty("age", age, changeSupport)
var age: Int
get() = _age.getValue()
set(value) { _age.setValue(value) }
}
- 위임 프로퍼티를 사용할 수 있도록 다음과 같이 수정한다.
open class PropertyChangeAware {
protected val changeSupport = PropertyChangeSupport(this)
fun addPropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.addPropertyChangeListener(listener)
}
fun removePropertyChangeListener(listener: PropertyChangeListener) {
changeSupport.removePropertyChangeListener(listener)
}
}
open class ObservableProperty(
var propValue: Int,
val changeSupport: PropertyChangeSupport
) {
operator fun getValue(p: DelegatedPerson, prop: KProperty<*>): Int = propValue
operator fun setValue(p: DelegatedPerson, prop: KProperty<*>, newValue: Int) {
val oldValue = propValue
propValue = newValue
changeSupport.firePropertyChange(prop.name, oldValue, newValue)
}
}
class DelegatedPerson(
val name: String, age: Int
) : PropertyChangeAware() {
var age: Int by ObservableProperty(age, changeSupport)
}
- 코틀린 표준 라이브러리에서 비슷한 클래스를 제공해준다.
Delegates.observable(property, observer)
- observer: 프로퍼티 값 변경을 통지할 때, 로직을 작성하는 람다.
class DelegatedPerson(
val name: String, age: 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)
}
위임 프로퍼티 컴파일 규칙#

MyDelegate
클래스의 인스턴스를 감춰진 프로퍼티에 저장하며 <delegate>
라는 이름으로 부른다.- 프로퍼티를 표현하기 위해
KProperty
타입의 객체를 사용하며 <property>
라고 부른다.


- 이 매커니즘은 프로퍼티 값이 저장될 장소를 바꿀 수도 있고, 프로퍼티를 읽거나 쓸 때 벌어질 일을 변경할 수도 있다.
프로퍼티 값을 맵에 저장#
- 확장 가능한 객체: 자신의 프로퍼티를 동적으로 정의할 수 있는 객체
- 맵을 통해 위임 프로퍼티를 사용하면 구현이 가능하다.
- 정보를 모두 맵에 저장하되 그 맵을 통해 처리하는 프로퍼티를 통해 필수 정보를 제공할 수 있다.
Map
과 MutableMap
인터페이스에 대해 getValue
와 setValue
확장 함수를 제공한다.getValue
에서 맵에 프로퍼티 값을 저장할 때는 자동으로 프로퍼티 이름을 키로 활용한다.
class ExpandoPerson {
private val _attributes = hashMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
val name: String by _attributes
}
프로퍼티에서 위임 프로퍼티 활용#
- 객체 프로퍼티를 저장하거나 변경하는 방법을 바꿀 수 있으면 프레임워크를 개발할 때 유용하다.
Users
객체는 데이터베이스 테이블을 표현한다.User
의 상위 클래스인 Entity
클래스는 데이터베이스 칼럼을 엔티티 속성값으로 연결해주는 매핑이 있다.Column
객체(Users.name
. Users.age
)를 위임 객체로 사용한다.

- 프레임워크는
Column
객체 안에 getValue
와 setValue
메소드를 정의해주기만 하면 된다.User
객체를 변경하면 그 객체는 dirty 상태로 변하고, 나중에 적절히 데이터베이스에 변경 내용을 반영한다.

- 이 예제의 완전한 구현은 Exposed 프레임워크 소스코드에서 볼 수 있다.