9장 연산자 오버로딩과 다른 관례(convention)
관례(Convention)란?
특정 함수 이름과 언어 기능을 연결하는 약속
9.1 산술 연산자 오버로딩
기본 개념
연산자를 오버로딩하는 함수 앞에는 반드시 operator가 있어야 함
→ 실수로 관례에서 사용하는 함수 이름을 사용하는 경우를 막아줌
data class Point(val x: Int, val y: Int) {
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
}
val p1 = Point(10, 20)
val p2 = Point(30, 40)
val p3 = p1 + p2 *// Point(40, 60) - plus 함수 호출됨!*
지원되는 산술 연산자들
plus, times, divide 등: 이항 산술 연산

class Vector(val x: Int, val y: Int) {
operator fun plus(other: Vector) = Vector(x + other.x, y + other.y) *// +*
operator fun minus(other: Vector) = Vector(x - other.x, y - other.y) *// -*
operator fun times(scale: Int) = Vector(x * scale, y * scale) *// **
operator fun div(scale: Int) = Vector(x / scale, y / scale) *// /*
operator fun mod(scale: Int) = Vector(x % scale, y % scale) *// %*
}
val v = Vector(10, 20)
println(v * 3) *// Vector(30, 60)*
println(v + Vector(1, 2)) *// Vector(11, 22)*
다른 타입과의 연산
*// [두 피연산자의 타입이 다른 연산사 정의 ]
// 포인트 * 배율 (Int * Double)*
operator fun Point.times(scale: Double): Point {
return Point((x * scale).toInt(), (y * scale).toInt())
}
println(Point(10, 20) * 1.5) *// Point(15, 30)
// 자동 교환법칙 ( p*1.5 = 1.5*p) 적용 안됨 주의, 추가 구현 필요
// operator fun Double.times(p: Point): Point*
*// [결과 타입이 피연산자 타입과 다른 연산자 정의 ]
// 문자 * 횟수 = 문자열 (Char * Int = String)*
operator fun Char.times(count: Int): String {
return toString().repeat(count)
}
println('A' * 5) *// "AAAAA"*
비트 연산자
shl: 왼쪽 시프트(자바 <<)
shr: 오른쪽 시프트(부호 비트 유지, 자바 >>)
ushr: 오른쪽 시프트(0으로 부호 비트 설정, 자바 >>>)
and: 비트 곱(자바 &)
or: 비트 합(자바 I)
xor: 비트 배타 합(자바 ^)
inv: 비트 반전(자바 ~)
fun main() {
println(0x0F and 0xF0)// 0
println(0x0F or 0xF0) // 255
println(0xl shl 4) // 16
복합 대입 연산자 (+=, -=, =, /=)
var point = Point(1, 2)
point += Point(3, 4) *// point = point + Point(3, 4)와 동일*
println(point) *// Point(4, 6)
// 또는 직접 구현*
operator fun Point.plusAssign(other: Point) {
*// 원본 객체 수정 (mutable한 경우)*
}
- 추가..상황 설명+= 연산자가 동작하는 두 가지 방법
방법 2: plusAssign 함수 사용 (기존 객체 수정)표준 라이브러리의 구현*// a += b가 이렇게 변환됨* a = a.plus(b) *// 새로운 객체를 만들어서 대입 // 예시* val list1 = listOf(1, 2, 3) *// 읽기 전용 리스트* val list2 = list1 + 4 *// 새로운 리스트 [1, 2, 3, 4] 생성*
실제 동작 비교어떤 방식이 선택될까?operator fun <T> MutableCollection<T>.plusAssign(element: T) { this.add(element) *// 기존 컬렉션에 원소 추가* }- plusAssign이 있으면 → plusAssign 사용 (기존 객체 수정)
- plusAssign이 없으면 → plus 사용 (새 객체 생성)
- 둘 다 있으면 → 컴파일 오류val confused = Confused()
- ```kotlin
class Confused {
operator fun plus(other: Confused) = Confused() // plus 정의
operator fun plusAssign(other: Confused) = Unit // plusAssign도 정의
}
커스텀 컬렉션정리class TextBuilder { private val content = StringBuilder() operator fun plusAssign(text: String) { content.append(text) } override fun toString() = content.toString() } val builder = TextBuilder() builder += "Hello " builder += "World!" println(builder) *// "Hello World!"*- += 연산자는
plusAssign또는plus함수로 처리됨 - 가변 컬렉션에서는
plusAssign이 우선 → 기존 객체 수정 - 읽기 전용 컬렉션에서는
plus사용 → 새 객체 생성 - 메모리 효율성:
plusAssign>plus - 둘 다 정의하면 컴파일 오류 → 하나만 정의할 것
- ```kotlin
class ImmutablePoint(val x: Int, val y: Int) {}
}operator fun plusAssign(other: MutablePoint) { x += other.x y += other.y } 코틀린은 **불변 객체는 새로 만들고, 가변 객체는 수정**하는 방식으로 각각의 특성에 맞게 `+=` 연산자를 처리 함- class MutablePoint(var x: Int, var y: Int) {
operator fun plus(other: ImmutablePoint) = ImmutablePoint(x + other.x, y + other.y)class NumberContainer { private val numbers = mutableListOf<Int>() operator fun plusAssign(number: Int) { if (number > 0) { *// 양수만 추가* numbers.add(number) } } operator fun minusAssign(number: Int) { numbers.removeAll { it == number } } fun getNumbers() = numbers.toList() } val container = NumberContainer() container += 5 *// 추가됨* container += -3 *// 추가 안 됨 (양수가 아님)* container += 10 *// 추가됨* container -= 5 *// 제거됨* println(container.getNumbers()) *// [10]*- StringBuilder 스타일
*// 모든 복합 대입 연산자들* operator fun <T> MutableCollection<T>.plusAssign(element: T) { add(element) } *// +=* operator fun <T> MutableCollection<T>.minusAssign(element: T) { remove(element) } *// -=* operator fun <T> MutableList<T>.timesAssign(n: Int) { */* 반복 추가 */* } *// *=// 사용 예시* val numbers = mutableListOf(1, 2, 3) numbers += 4 *// [1, 2, 3, 4]* numbers -= 2 *// [1, 3, 4]* val chars = mutableListOf('A') chars *= 3 *// ['A', 'A', 'A'] (만약 구현되어 있다면)*fun plusAssignExample() { val list = mutableListOf(1, 2, 3) *// 기존 리스트를 수정 → 메모리 효율적* list += 4 *// 기존 리스트에 4 추가* list += 5 *// 기존 리스트에 5 추가* list += 6 *// 기존 리스트에 6 추가* println(list) *// [1, 2, 3, 4, 5, 6]* }## **메모리 효율성 비교** ### **plus 방식 (새 객체 생성)** ```kotlin fun plusExample() { var list = listOf(1, 2, 3) *// 매번 새로운 리스트 생성 → 메모리 사용량 증가* list += 4 *// 새 리스트 [1, 2, 3, 4] 생성* list += 5 *// 새 리스트 [1, 2, 3, 4, 5] 생성* list += 6 *// 새 리스트 [1, 2, 3, 4, 5, 6] 생성* println(list) *// [1, 2, 3, 4, 5, 6]* }- 우선순위 규칙
fun main() { *// 1. 읽기 전용 컬렉션 + plus 함수* var readOnlyList = listOf(1, 2, 3) readOnlyList += 4 *// readOnlyList = readOnlyList.plus(4)와 동일* println(readOnlyList) *// [1, 2, 3, 4] - 새로운 리스트가 생성됨* *// 2. 가변 컬렉션 + plusAssign 함수* val mutableList = mutableListOf(1, 2, 3) mutableList += 4 *// mutableList.plusAssign(4)와 동일* println(mutableList) *// [1, 2, 3, 4] - 기존 리스트가 수정됨* }- MutableCollection에 정의된 plusAssign
*// a += b가 이렇게 변환됨* a.plusAssign(b) *// 기존 객체를 직접 수정 // 예시* val mutableList = mutableListOf(1, 2, 3) *// 가변 리스트* mutableList += 4 *// 기존 리스트에 4 추가 (새 객체 생성 안 함)*- 방법 1: plus 함수 사용 (새 객체 생성)
fun main() { val numbers = mutableListOf<Int>() *// 빈 가변 리스트 생성* numbers += 42 *// 42를 리스트에 추가* println(numbers[0]) *// 42 출력* }- += 연산자의 두 가지 처리 방식
단항 연산자
operator fun Point.unaryMinus() = Point(-x, -y) *// -p*
operator fun Point.unaryPlus() = Point(+x, +y) *// +p*
operator fun Point.not() = Point(!x, !y) *// !p (boolean이라면)// 증감 연산자*
operator fun BigDecimal.inc() = this + BigDecimal.ONE
operator fun BigDecimal.dec() = this - BigDecimal.ONE
var bd = BigDecimal.ZERO
println(bd++) *// 0 (출력 후 증가)*
println(++bd) *// 2 (증가 후 출력)*
- 추가 ..
2. inc/dec는 새 객체 반환3. 컴파일러가 전위/후위 자동 처리*// 좋은 예: 직관적이고 예측 가능* data class Money(val amount: Int) { operator fun unaryMinus() = Money(-amount) *// 음수 금액 (부채)* } *// 나쁜 예: 의미가 불분명* data class Person(val name: String) { operator fun unaryMinus() = Person(name.reversed()) *// 의미 불분명* } *// inc() 함수 하나만 정의하면* operator fun Counter.inc() = Counter(count + 1) *// 컴파일러가 자동으로 처리* val counter = Counter(5) val a = counter++ *// 후위: 이전 값 반환 후 증가* val b = ++counter *// 전위: 증가 후 새 값 반환**// 불변 객체에서 inc/dec는 항상 새 객체 반환* operator fun Point.inc() = Point(x + 1, y + 1) val p1 = Point(1, 2) val p2 = ++p1 *// p1과 p2는 다른 객체!*- 1. 의미있게 사용하기
9.2 비교 연산자
동등성 연산자 (==, !=)
기본 원리
a == b // 이 코드가...
↓
a?.equals(b) ?: (b == null) // 이렇게 변환
코틀린이 == 연산자 호출을 equals 메서드 호출로 컴파일
!= 연산자도 ****equals 메서드 호출로 컴파일
// != 연산자는 자동으로 equals의 반대로 처리됨
a != b // 이 코드가...
↓
!(a == b) // 이렇게 변환됨
// 따라서 != 를 별도로 구현할 필요 없음
fun main() {
val p1 = Point(1, 2)
val p2 = Point(3, 4)
println(p1 == p2) // false
println(p1 != p2) // true (자동으로 == 결과를 반전)
}
== 연산자의 null 처리
fun main() {
val a: String? = "hello"
val b: String? = "hello"
val c: String? = null
val d: String? = null
println(a == b) // true (둘 다 "hello")
println(a == c) // false ("hello" != null)
println(c == d) // true (null == null)
println(c == null) // true (null == null)
}
// a == b 가 실행될 때:
// 1. a가 null인가?
// - null이면: b == null을 반환
// - null이 아니면: a.equals(b) 호출
// 실제 구현 (의사코드)
fun safeEquals(a: Any?, b: Any?): Boolean {
return when {
a == null -> b == null // a가 null이면 b도 null인지 확인
else -> a.equals(b) // a가 null이 아니면 equals 호출
}
}
class Person(val name: String, val age: Int) {
override fun equals(other: Any?): Boolean {
if (other !is Person) return false
return name == other.name && age == other.age
}
override fun hashCode(): Int = name.hashCode() * 31 + age
}
*// a == b는 a?.equals(b) ?: (b == null)로 변환됨*
val p1 = Person("김철수", 25)
val p2 = Person("김철수", 25)
println(p1 == p2) *// true*
참조 동등성 (===) vs 구조적 동등성 (==)
fun main() {
val str1 = "hello"
val str2 = "hello"
val str3 = String("hello".toCharArray()) *// 새 객체 생성*
*// 구조적 동등성 (==) - 내용 비교*
println(str1 == str2) *// true (내용이 같음)*
println(str1 == str3) *// true (내용이 같음)*
*// 참조 동등성 (===) - 객체 동일성 비교*
println(str1 === str2) *// true (문자열 풀에서 같은 객체)*
println(str1 === str3) *// false (다른 객체)*
*// === 연산자는 오버로딩할 수 없음!*
}
동등성 연산자 (==, !=) 관련 정리
- == 연산자는 null 안전한 equals 호출
- === 연산자는 참조 동등성 (오버로딩 불가)
- 오버로딩 불가 하다는 것이 뭘까?
class Point(val x: Int, val y: Int) { *// 가능 - 다른 연산자들은 오버로딩 가능* operator fun plus(other: Point) = Point(x + other.x, y + other.y) operator fun equals(other: Any?): Boolean = *// ...* *// 불가능 - === 연산자는 오버로딩 불가 // operator fun identityEquals(other: Any?): Boolean = // 컴파일 에러* } *// === 연산자는 언제나 JVM 레벨에서 참조 비교만 함* val p1 = Point(1, 2) val p2 = Point(1, 2) val p3 = p1 println(p1 === p2) *// 항상 false (다른 객체)* println(p1 === p3) *// 항상 true (같은 객체) // 이 동작을 바꿀 수 없음!*
- != 연산자는 자동으로 == 결과 반전
- equals 오버라이드 시 operator 키워드 불필요/ override 필요
- 다른 연산자 오버로딩 관례와 달리 equals는 Any에 정의된 메서드
순서 비교 연산자 (<, >, <=, >=)
자바 vs 코틀린의 비교 연산
- 자바:
Comparable인터페이스의compareTo()메서드를 명시적으로 호출해야 함 *// 자바에서는 이렇게 써야 함* element1.compareTo(element2)- 코틀린: 비교 연산자(
<,>,<=,>=)를 사용하면 자동으로compareTo()호출로 컴파일됨 *// 코틀린에서는 이렇게 쓸 수 있음* p1 < p2 *// 실제로는 p1.compareTo(p2) < 0 으로 컴파일됨*
class Person(
val firstName: String,
val lastName: String
) : Comparable<Person> {
override fun compareTo(other: Person): Int {
// compareValuesBy를 사용한 간편한 구현
return compareValuesBy(this, other,
Person::lastName, // 먼저 성으로 비교
Person::firstName // 성이 같으면 이름으로 비교
)
}
}
fun main() {
val p1 = Person("Alice", "Smith")
val p2 = Person("Bob", "Johnson")
println(p1 < p2) // false (Smith > Johnson이므로, 사전순 J가 S보다 앞)
}
// 간편하지만 느린 방법
return compareValuesBy(this, other, Person::lastName, Person::firstName)
// 복잡하지만 빠른 방법
override fun compareTo(other: Person): Int {
val lastNameComparison = lastName.compareTo(other.lastName)
if (lastNameComparison != 0) return lastNameComparison
return firstName.compareTo(other.firstName)
}
9.3 컬렉션과 범위 관례
자주 사용하는 두 가지 핵심 연산:
- 인덱스 접근:
a[b](원소 읽기/쓰기) - 멤버십 검사:
in연산자 (원소가 포함되어 있는지 확인)
인덱스 접근 (get, set)
get 관례: x[a, b] → x.get(a, b)
set 관례: x[a, b] = c → x.set(a, b, c)
kotlinclass StringTable {
private val data = mutableMapOf<Pair<Int, Int>, String>()
//Pair(행, 열)를 키, 문자열을 값으로 저장 : Pair(0, 1) → "Hello"
operator fun get(row: Int, col: Int): String? {
return data[Pair(row, col)] // 맵에서 키로 찾아서 반환
}
operator fun set(row: Int, col: Int, value: String) {
data[Pair(row, col)] = value // 맵에 키-값 쌍으로 저장
}
}
fun main() {
val table = StringTable()
// 1단계: (0,1) 위치에 "Hello" 저장
table[0, 1] = "Hello"
// 내부적으로: data[Pair(0, 1)] = "Hello"
// 2단계: (0,1) 위치의 값을 읽어오기
println(table[0, 1])
// 내부적으로: data[Pair(0, 1)] 조회 → "Hello" 반환
}
col 0 col 1 col 2
row 0 ? "Hello" ?
row 1 ? ? ?
row 2 ? ? ?
table[0, 1] // 사용자가 이렇게 쓰면
↓
table.get(0, 1) // 컴파일러가 이렇게 변환
↓
data[Pair(0, 1)] // 실제로는 맵에서 키로 검색
일반적인 방법:
table.setValue(0, 1, "Hello")
val value = table.getValue(0, 1)
operator를 사용한 방법:
table[0, 1] = "Hello"
val value = table[0, 1]
멤버십 검사 (in)
in 관례: a in b → b.contains(a)
data class Rectangle(val upperLeft: Point, val lowerRight: Point)
operator fun Rectangle.contains(point: Point): Boolean {
return point.x in upperLeft.x..<lowerRight.x &&
point.y in upperLeft.y..<lowerRight.y
}
val rect = Rectangle(Point(10, 20), Point(50, 50))
println(Point(20, 30) in rect) *// true*
범위 생성 (rangeTo, rangeUntil)
범위 관례: a..b → a.rangeTo(b), a..<b → a.rangeUntil(b)
start..end → start.rangeTo(end) //마지막 숫자 포함
start..<end → start.rangeUntil(end) //마지막 숫자 포함하지 않음
범위 연산자는 우선순위가 낮으므로 괄호 사용 권장
// 좋은 예 - 괄호 사용
println(0..(n + 1)) // 명확함
(0..n).forEach { print(it) } // 범위의 메서드 호출시 괄호 필수
// 나쁜 예
println(0..n + 1) // 혼동 가능
0..n.forEach { } // 컴파일 에러!
*// 날짜 범위 만들기*
import java.time.LocalDate
val today = LocalDate.now()
val vacation = today..today.plusDays(10) *// rangeTo 호출*
val weekdaysOnly = today..<today.plusDays(5) *// rangeUntil 호출*
println(today.plusDays(3) in vacation) *// true*
이터레이션 (iterator)
iterator 관례: for (x in obj) → obj.iterator()를 통한 반복
기본 변환
for (x in list) { ... }
↓
val iterator = list.iterator()
while (iterator.hasNext()) {
val x = iterator.next()
...
}
문자열 이터레이션: 코틀린이 제공하는 확장 함수
operator fun CharSequence.iterator(): CharIterator
fun main() {
for (c in "abc") {
println(c) // a, b, c 출력
}
}
날짜 범위 이터레이션: 날짜 범위 반복
operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> {
return object : Iterator<LocalDate> {
var current = start
override fun hasNext() = current <= endInclusive
override fun next(): LocalDate {
val result = current
current = current.plusDays(1)
return result
}
}
}
*// 사용*
val dateRange = LocalDate.of(2024, 1, 1)..LocalDate.of(2024, 1, 3)
for (date in dateRange) {
println(date) *// 2024-01-01, 2024-01-02, 2024-01-03*
}
9.4 구조 분해 선언
구조 분해 선언: 복합적인 값을 분해해서 여러 변수에 한꺼번에 초기화하는 기능
기본 사용법
data class Point(val x: Int, val y: Int)
val point = Point(10, 20)
val (x, y) = point *// component1(), component2() 호출*
println("x: $x, y: $y") *// x: 10, y: 20
// 직접 구현할 경우*
class Person(val name: String, val age: Int) {
operator fun component1() = name
operator fun component2() = age
}
val person = Person("김철수", 25)
val (name, age) = person
데이터 클래스의 componentN 함수 자동 생성
data class Person(
val name: String, *// component1()*
val age: Int, *// component2()*
val city: String *// component3()*
)
// 컴파일러가 내부적으로 이런 함수들을 자동 생성
class Person(val name: String, val age: Int, val city: String) {
operator fun component1(): String = name
operator fun component2(): Int = age
operator fun component3(): String = city
// 기타 데이터 클래스 함수들도 자동 생성
// equals(), hashCode(), toString(), copy()
}
일반 클래스 - 수동 구현 필요
class Point(val x: Int, val y: Int) {
*// 직접 작성해야 함*
operator fun component1() = x
operator fun component2() = y
}
fun main() {
val p = Point(10, 20)
val (x, y) = p *// 동작함*
}
데이터 클래스 - 자동 생성
data class Point(val x: Int, val y: Int)
*// componentN 함수들이 자동으로 생성됨!*
fun main() {
val p = Point(10, 20)
val (x, y) = p *// 바로 동작함*
}
- 주의 ) 프로퍼티 개수 제한 list, array → 5개
// 코틀린 표준 라이브러리는 component5()까지만 제공
val list = listOf(1, 2, 3, 4, 5, 6)
val (a, b, c, d, e) = list // 가능
// val (a, b, c, d, e, f) = list // component6() 없음
함수에서 여러 값 반환
data class Result(val success: Boolean, val message: String, val data: Any?)
fun processData(): Result {
return Result(true, "성공", listOf(1, 2, 3))
}
val (success, message, data) = processData()
if (success) {
println("처리 성공: $message")
}
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
}
9.4.1 반복문에서 구조 분해
val map = mapOf("Alice" to 25, "Bob" to 30)
for ((name, age) in map) {
println("$name: $age세")
}
*// 람다에서도 사용 가능*
map.forEach { (name, age) ->
println("$name: $age세")
}
9.4.2 문자로 일부 값 무시하기
data class User(val id: Long, val name: String, val email: String, val city: String)
val user = User(1, "김철수", "kim@email.com", "서울")
val (id, name, _, _) = user *// email과 city는 무시*
위치 기반의 한계: 순서에 의존하므로 리팩터링 시 주의 필요
9.5 위임 프로퍼티(Delegated Properties)
- 무거운 작업을 나중에 하고 싶을 때 →
lazy - 값 변경을 감지하고 싶을 때 →
observable - 설정값을 맵으로 관리하고 싶을 때 →
map
9.5.1 기본 문법과 내부 동작
기본문법/컴파일러가 생성하는 코드
class Foo {
var p: Type by Delegate()
}
*// ↓ 컴파일러가 실제로 생성하는 코드*
class Foo {
private val delegate = Delegate() *// 위임 객체*
var p: Type
get() = delegate.getValue(this, ::p)
set(value) = delegate.setValue(this, ::p, value)
}
위임 객체가 구현해야 하는 관례
class Delegate {
operator fun getValue(thisRef: Any?, property: KProperty<*>): Type {
*// getter 로직*
}
operator fun setValue(thisRef: Any?, property: KProperty<*>, value: Type) {
*// setter 로직*
}
}
9.5.2 지연 초기화: by lazy()
문제 상황 - 수동 구현
class Person(val name: String) {
private var _emails: List<Email>? = null *// 뒷받침하는 프로퍼티*
val emails: List<Email>
get() {
if (_emails == null) {
_emails = loadEmails(this) *// 비용이 많이 드는 작업*
}
return _emails!!
}
}
문제점:
- 코드가 복잡함
- 스레드 안전하지 않음
- 여러 지연 프로퍼티가 있으면 중복 코드 발생
해결책 - by lazy() 사용
class Person(val name: String) {
val emails by lazy { loadEmails(this) } *// 간단하고 스레드 안전!*
}
장점:
- 스레드 안전: 기본적으로 동기화 보장
- 한 번만 초기화: 최초 접근 시에만 람다 실행
- 간결함: 보일러플레이트 코드 제거
9.5.3 위임 프로퍼티 직접 구현
1단계: 수동 구현 (옵저버 패턴)
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) *// 중복 코드!*
}
}
2단계: 도우미 클래스 추출
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)
}
}
3단계: 위임 프로퍼티용 관례 구현
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) *// 프로퍼티 이름을 자동으로 가져옴*
}
}
4단계: 위임 프로퍼티 사용
class Person(val name: String, age: Int, salary: Int) : Observable() {
var age by ObservableProperty(age, this)
var salary by ObservableProperty(salary, this)
}
5단계: 표준 라이브러리 활용
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.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 }
}
위임 프로퍼티 사용
class Person {
private val _attributes = mutableMapOf<String, String>()
fun setAttribute(attrName: String, value: String) {
_attributes[attrName] = value
}
var name: String by _attributes *// 맵을 위임 객체로 사용*
}
동작 원리:
- 표준 라이브러리가
Map과MutableMap에 대해getValue/setValue확장 함수 제공 - 프로퍼티 이름이 자동으로 맵의 키로 사용됨
9.5.6 프레임워크에서의 활용 (데이터베이스 ORM)
*// 테이블 정의*
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
}
*// 프레임워크가 제공하는 위임 구현*
operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
*// 데이터베이스에서 값 읽기*
}
operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
*// 데이터베이스에 값 쓰기, dirty 상태 관리*
}
사용 예:
val user = User(someId)
user.age += 1 *// 자동으로 데이터베이스 업데이트*
10장 고차 함수: 람다를 파라미터와 반환값으로 사용
10.1 고차 함수
고차 함수 : 고차 함수는 람다나 함수 참조를 인자로 넘길 수 있거나, 람다나 함수 참조를 반환하는 함수
코틀린에서는 람다나 함수 참조를 사용해 함수를 값으로 표현할 수 있음
// 예시: filter는 술어 함수를 인자로 받는 고차 함수
list.filter { it > 0 }
10.1.1 함수 타입은 람다의 파라미터 타입과 반환 타입을 지정한다
함수 타입 선언 문법
// 기본 형태: (파라미터 타입들) -> 반환 타입
// 컴파일러가 타입을 알아서 추론
// 람다에서 타입 명시
val sum = { x: Int, y: Int -> x + y }
val action = { println(42) }
// 명시적으로 함수 타입 적기
// 변수 타입 명시 (람다에서는 생략)
val sum: (Int, Int) -> Int = { x, y -> x + y }
val action: () -> Unit = { println(42) }
예시
// 두 개의 Int를 받아서 Int를 반환
val calculator: (Int, Int) -> Int = { x, y -> x + y }
// 아무것도 받지 않고 Unit(아무것도) 반환
val printer: () -> Unit = { println("안녕!") }
// String 하나 받아서 Boolean 반환
val checker: (String) -> Boolean = { it.length > 5 }
// 여러 파라미터
val formatter: (String, Int, Boolean) -> String = { name, age, isStudent ->
"$name님은 ${age}살이고 학생여부: $isStudent"
}
널 가능한 함수 타입
// 함수가 널을 반환할 수 있음
var canReturnNull: (Int, Int) -> Int? = { x, y -> null }
// 함수 타입 자체가 널일 수 있음 (괄호 주의!)
var funOrNull: ((Int, Int) -> Int)? = null
10.1.2 인자로 전달 받은 함수 호출
파라미터 이름 지정
"어떤 계산 방법"을 받아서 (operation 파라미터) 결과 계산해서 리턴하는 함수
fun twoAndThree(operation: (operandA: Int, operandB: Int) -> Int) {
val result = operation(2, 3)
println("The result is $result")
}
fun main() {
// 원하는 다른 이름 사용해도 OK! operandA 아니고 a 해도 됨
twoAndThree { a, b -> a + b } // 출력: The result is 5
twoAndThree { a, b -> a * b } // 출력: The result is 6
twoAndThree { a, b -> a - b } // 출력: The result is -1
twoAndThree { a, b -> a / b } // 출력: The result is 0 (정수 나눗셈)
// 더 복잡한 계산
twoAndThree { a, b -> a * a + b * b }
// 출력: The result is 13 (2²+3² = 4+9 = 13)
}
filter 함수 구현 예시
fun String.filter(predicate: (Char) -> Boolean): String
// ↑ ↑ ↑ ↑ ↑
// 수신객체 파라미터명 입력타입 출력타입 반환타입
fun String.filter(predicate: (Char) -> Boolean): String {
return buildString {
for (char in this@filter) { // 문자열의 각 문자 확인
if (predicate(char)) { // 조건 함수 호출
append(char) // 조건에 맞으면 추가
}
}
}
}
fun main() {
println("able".filter { it in 'a'..'z' }) // "able"
}
10.1.3 자바에서 코틀린 함수 타입 사용
코틀린 함수 타입은 내부적으로 FunctionN 인터페이스로 구현
// 코틀린 선언
fun processTheAnswer(f: (Int) -> Int) {
println(f(42))
}
// 자바 호출
processTheAnswer(number -> number + 1); // 43
문제와 해결방법
수신 객체 명시: strings.forEach가 아니라 CollectionsKt.forEach(strings, ...)
Unit 반환: Unit.INSTANCE를 명시적으로 반환해야 함
import kotlin.collections.CollectionsKt;
import kotlin.Unit;
public class JavaExample {
public static void main(String[] args) {
List<String> strings = new ArrayList<>();
strings.add("Hello");
strings.add("World");
// 코틀린의 forEach를 자바에서 사용
CollectionsKt.forEach(strings, s -> {
System.out.println(s);
return Unit.INSTANCE; // Unit을 명시적으로 반환해야 함
});
}
}
함수 타입의 내부 구조
FunctionN 인터페이스
코틀린 함수 타입은 내부적으로 FunctionN 인터페이스로 구현
*// 코틀린 함수 타입*
val myFunction: (Int) -> String = { "Number: $it" }
*// 실제로는 이런 인터페이스의 구현체*
interface Function1<P1, R> {
operator fun invoke(p1: P1): R
}
파라미터 개수별 인터페이스
*// 파라미터 없음*
val func0: () -> String *// Function0<String>// 파라미터 1개*
val func1: (Int) -> String *// Function1<Int, String>// 파라미터 2개*
val func2: (Int, String) -> Boolean *// Function2<Int, String, Boolean>// 파라미터 3개*
val func3: (Int, String, Boolean) -> Unit *// Function3<Int, String, Boolean, Unit>*
실제 동작 과정
함수 호출의 내부
fun processTheAnswer(f: (Int) -> Int) {
println(f(42)) *// 실제로는 f.invoke(42)*
}
*// 컴파일러가 내부적으로 변환하는 코드*
fun processTheAnswer(f: Function1<Int, Int>) {
println(f.invoke(42))
}
10.1.4 함수 타입의 파라미터에 대해 기본값을 지정할 수 있고, 널이 될 수도 있다
기본값이 있는 함수 타입 파라미터
// 문법
parameter: (ParamType) -> ReturnType = { 기본 람다 }
// 예시
transform: (T) -> String = { it.toString() }
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: (T) -> String = { it.toString() } // 기본값 지정
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
result.append(transform(element))
}
result.append(postfix)
return result.toString()
}
// 사용법
val letters = listOf("Alpha", "Beta")
println(letters.joinToString()) // Alpha, Beta
println(letters.joinToString { it.lowercase() }) // alpha, beta
널 가능한 함수 타입 파라미터
// 선언
parameter: ((ParamType) -> ReturnType)? = null
// 안전한 호출
parameter?.invoke(argument) ?: 기본값
transform?.invoke(element)→ transform이 null이 아니면 호출?: element.toString()→ transform이 null이면 기본 toString() 사용
fun <T> Collection<T>.joinToString(
separator: String = ", ",
prefix: String = "",
postfix: String = "",
transform: ((T) -> String)? = null
): String {
val result = StringBuilder(prefix)
for ((index, element) in this.withIndex()) {
if (index > 0) result.append(separator)
val str = transform?.invoke(element) ?: element.toString()
result.append(str)
}
result.append(postfix)
return result.toString()
}
10.1.5 함수를 함수에서 반환하기
상황에 맞는 도구를 골라주는 함수
fun 함수이름(파라미터): (입력타입) -> 출력타입 {
return { 입력 -> 처리로직 }
}
enum class Delivery { STANDARD, EXPEDITED }
class Order(val itemCount: Int)
fun getShippingCostCalculator(delivery: Delivery): (Order) -> Double {
if (delivery == Delivery.EXPEDITED) {
// 빠른 배송: 기본 6달러 + 아이템당 2.1달러
return { order -> 6 + 2.1 * order.itemCount }
}
// 일반 배송: 아이템당 1.2달러
return { order -> 1.2 * order.itemCount }
}
fun main() {
val calculator = getShippingCostCalculator(Delivery.EXPEDITED)
println("Shipping costs ${calculator(Order(3))}") // Shipping costs 12.3
}
getShippingCostCalculator()→ 배송 방법에 맞는 계산 함수 반환- 반환된 함수를
calculator변수에 저장 calculator(Order(3))→ 실제 계산 실행
활용 - UI 필터링
data class Person(
val firstName: String,
val lastName: String,
val phoneNumber: String?
)
class ContactListFilters {
var prefix: String = ""
var onlyWithPhoneNumber: Boolean = false
// 설정에 맞는 필터 함수를 반환
fun getPredicate(): (Person) -> Boolean {
// 이름이나 성이 prefix로 시작하는지 확인하는 함수
val startsWithPrefix = { p: Person ->
p.firstName.startsWith(prefix) || p.lastName.startsWith(prefix)
}
if (!onlyWithPhoneNumber) {
return startsWithPrefix // 전화번호 조건 없음
}
// 전화번호도 있어야 함
return { startsWithPrefix(it) && it.phoneNumber != null }
}
}
//사용
fun main() {
val contacts = listOf(
Person("Dmitry", "Demerov", "123-4567"),
Person("Svetlana", "Isakova", null)
)
val contactListFilters = ContactListFilters()
contactListFilters.prefix = "Dm"
contactListFilters.onlyWithPhoneNumber = true
// getPredicate()가 반환한 함수를 filter에 전달
val filtered = contacts.filter(contactListFilters.getPredicate())
println(filtered)
// [Person(firstName=Dmitry, lastName=Demerov, phoneNumber=123-4567)]
}
10.1.6 람다로 중복 제거하기
일반적인 중복 제거
data class SiteVisit(val path: String, val duration: Double, val os: OS)
enum class OS { WINDOWS, LINUX, MAC, IOS, ANDROID }
// Before: 하드코딩된 필터
val averageWindowsDuration = log
.filter { it.os == OS.WINDOWS }
.map(SiteVisit::duration)
.average()
// After: 파라미터화
fun List<SiteVisit>.averageDurationFor(os: OS) =
filter { it.os == os }.map(SiteVisit::duration).average()
고차 함수를 사용한 더 유연한 해결책
fun List<SiteVisit>.averageDurationFor(predicate: (SiteVisit) -> Boolean) =
filter(predicate).map(SiteVisit::duration).average()
fun main() {
// 복잡한 조건도 처리 가능
println(log.averageDurationFor { it.os in setOf(OS.ANDROID, OS.IOS) })
println(log.averageDurationFor { it.os == OS.IOS && it.path == "/signup" })
}
10.2 인라인 함수를 사용해 람다의 부가 비용 없애기
문제: 람다 비용이 클 수도 있다
// 겉보기에는 간단해 보이지만...
people.filter { it.age < 30 }
// 실제로는 이렇게 컴파일됨
people.filter(new Function1<Person, Boolean>() {
public Boolean invoke(Person it) {
return it.age < 30;
}
});
- 람다마다 새로운 클래스 생성
- 객체 생성 비용 발생
- 메모리 사용량 증가
→ 해결책 :inline 키워드
10.2.1 인라이닝이 작동하는 방식
inline 키워드를 사용하면 함수 호출을 함수 본문으로 대체해 성능을 향상시킵니다.
// 인라인 함수 정의
inline fun <T> synchronized(lock: Lock, action: () -> T): T {
lock.lock()
try {
return action()
} finally {
lock.unlock()
}
}
//사용
fun foo(l: Lock) {
println("Before sync")
synchronized(l) {
println("Action")
}
println("After sync")
}
// 컴파일 후 실제로는 다음과 같음:
fun foo(l: Lock) {
println("Before sync")
// synchronized 함수가 여기에 복사됨
l.lock()
try {
println("Action") // 람다 내용도 여기에 복사됨
} finally {
l.unlock()
}
println("After sync")
}
장점:
함수 호출 없음 , 객체 생성 없음, 성능은 직접 작성한 코드와 동일
10.2.2 인라인 함수의 제약
안 되는 경우: 람다를 저장하는 경우
// 불가능: 람다를 변수에 저장
class FunctionStorage {
var myStoredFunction: ((Int) -> Unit)? = null
inline fun storeFunction(f: (Int) -> Unit) {
myStoredFunction = f // 컴파일 에러
// 람다를 저장하면 인라이닝 불가능
}
}
// 가능: noinline으로 특정 파라미터만 제외
inline fun foo(
inlined: () -> Unit, // 인라이닝됨
noinline notInlined: () -> Unit // 인라이닝 안됨
) {
inlined() // 인라인됨
notInlined() // 일반 함수 호출
}
10.2.3 컬렉션 연산 인라이닝
filter는 인라인 함수임
data class Person(val name: String, val age: Int)
val people = listOf(Person("Alice", 29), Person("Bob", 31))
// 인라인 함수들이므로 성능 걱정 없음
println(people.filter { it.age < 30 })
// filter와 map을 연쇄 사용
println(people.filter { it.age > 30 }.map(Person::name))
// 하지만 중간 리스트가 생성됨
// 큰 데이터의 경우 시퀀스 고려
people.asSequence()
.filter { it.age > 30 }
.map(Person::name)
.toList()
10.2.4 언제 함수를 인라인으로 선언할지 결정
인라인 사용 권장 사항
- 사용하기 좋은 경우: 람다를 인자로 받는 작은 함수
- 피해야 할 경우: 큰 함수 (코드 중복 발생)
인라인의 장점
- 함수 호출 오버헤드 제거
- 람다 객체 생성 비용 제거
- 비로컬 return 가능
*// 람다를 받는 함수 - 효과 큼!*
inline fun repeat(times: Int, action: (Int) -> Unit) {
for (i in 0 until times) {
action(i)
}
}
*// 일반 함수 - JVM이 알아서 최적화해줌*
fun add(a: Int, b: Int): Int = a + b
10.2.5 withLock, use, useLines로 자원 관리를 위해 인라인된 람다 사용
withLock 사용
val lock = ReentrantLock()
lock.withLock {
// 보호되는 코드
}
// withLock 구현
fun <T> Lock.withLock(action: () -> T): T {
lock()
try {
return action()
} finally {
unlock()
}
}
use 함수 활용 : 자동 리소스 해제 / useline : 더 간편한 파일 처리
fun readFirstLineFromFile(fileName: String): String {
BufferedReader(FileReader(fileName)).use { br ->
return br.readLine() //자동으로 파일 닫기
}
}
// 더 간결한 방법
fun readFirstLineFromFile(fileName: String): String {
return Path(fileName).useLines { it.first() } // 첫 번째 줄 반환
}
10.3 람다에서 반환: 고차 함수에서 흐름 제어
람다에서 return을 사용할 때 어떤 일이 일어나는지 알아보는 챕터
10.3.1 람다 안의 return 문: 람다를 둘러싼 함수에서 반환
비로컬 return (Non-local return) ,forEach도 일반 루프와 똑같이 동작
- 비로컬 return: 람다를 넘어서서 바깥 함수까지 종료
- 로컬 return: 람다만 종료하고 바깥 함수는 계속 실행
//일반 루프
fun lookForAlice(people: List<Person>) {
for (person in people) {
if (person.name == "Alice") {
println("Found!")
return // lookForAlice 함수를 종료
}
}
println("Alice is not found") // 출력 X
}
//forEach
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name == "Alice") {
println("Found!")
return // lookForAlice 함수를 종료
// return이 forEach만 끝내는 건지, 아니면 findPerson 함수 전체를 끝내는 건지?
}
}
println("Alice is not found") // 출력 X
}
//람다 안의 return은 람다를 감싸고 있는 함수를 종료
자바 람다의 제약 사항
// 자바 람다는 "값을 반환하는 식"이지 "코드 블록"이 아님
List<String> names = people.stream()
.map(person -> person.getName()) // 값 반환은 OK
.collect(toList());
// 하지만 제어 흐름(return/break/continue)은 불가능
people.forEach(person -> {
if (condition) {
return; // 안됨
break; // 안됨
continue; // 안됨
}
});
public void findPerson() {
for (Person person : people) {
if (person.getName().equals("Alice")) {
System.out.println("찾음");
return; // 이건 됨
}
}
System.out.println("못 찾음");
}
10.3.2 람다로부터 반환: 레이블을 사용한 return
로컬 return (레이블 사용)
fun lookForAlice(people: List<Person>) {
people.forEach label@{
if (it.name != "Alice") return@label // 람다만 종료
print("Found Alice!")
}
}
// 또는 함수 이름을 레이블로 사용
fun lookForAlice(people: List<Person>) {
people.forEach {
if (it.name != "Alice") return@forEach
print("Found Alice!")
}
}
10.3.3 익명 함수: 기본적으로 로컬 return
익명 함수 = 이름이 없는 함수
// 일반 함수 (이름 있음)
fun add(a: Int, b: Int): Int {
return a + b
}
// 익명 함수 (이름 없음)
fun(a: Int, b: Int): Int {
return a + b
}
람다 vs 익명함수
val numbers = listOf(1, 2, 3, 4, 5)
// 1. 람다
numbers.filter { it > 3 }
// 2. 익명 함수
numbers.filter(fun(x) = x > 3)
// 3. 일반 함수
fun isGreaterThan3(x: Int) = x > 3
numbers.filter(::isGreaterThan3)
// [4,5]
fun processNumbers() {
numbers.forEach(fun(it) {
if (it == 3) {
println("3 건너뜀!")
return // 이 익명 함수만 종료
}
println("숫자: $it")
})
println("처리 완료") // 항상 실행됨
}
//복잡한 조건들... 레이블 많이 필요
fun process() {
items.forEach label1@{ item ->
if (item.type == "A") {
return@label1
}
item.subItems.forEach label2@{ subItem ->
if (subItem.isInvalid()) {
return@label2
}
// 처리...
}
}
}
//익명 함수로 깔끔하게!
kotlinfun process() {
items.forEach(fun(item) {
if (item.type == "A") {
return // 간단
}
item.subItems.forEach(fun(subItem) {
if (subItem.isInvalid()) {
return // 레이블 필요 없음
}
// 처리...
})
})
}
결론
전체 종료: return (비로컬 return) , 여기서 끝
건너뛰기: return@label 또는 익명 함수 , 이번만 패스
복잡한 로직: 익명 함수가 깔끔 , (익명함수 : 안전한 return 구역)
'language > Kotilin In Action' 카테고리의 다른 글
| 6주차 (13장) (2) | 2025.09.01 |
|---|---|
| 5주차(11~12장) (1) | 2025.09.01 |
| 3주차 (6~8장) (1) | 2025.09.01 |
| 2주차 (4~5장) (1) | 2025.09.01 |
| 1주차 (1~3장) (0) | 2025.09.01 |