본문 바로가기
Kotlin

[Effective Kotlin] 12 - 연산자 오버로드를 할 때는 의미에 맞게 사용하라

by 매운돌 2023. 4. 15.

연산자 오버로딩은 굉장히 강력한 기능이지만, 위험할 수 있습니다.

fun Int.factorial(): Int = (1..this).product()

fun Iterable<Int>.product(): Int = 
		fold(1) { acc, i -> acc * i }
        
print(10 * 6.factorial()) // 7200 = 10 * 6!

위 처럼 Int확장 함수로 정의되어 있으므로 편하게 사용할 수 있습니다.

operator fun Int.not() = factorial()

print(10 * !6) // 7200

연산자 오버로딩을 활용하여 위처럼 표현할 수 있습니다.

왜냐하면 이 함수의 이름은 not()이기 때문에 논리 연산에 사용되어야 합니다. 이렇게 작성할 경우 굉장히 혼란스럽고, 오해의 소지가 있습니다.

 

Kotlin에서 각 연산자의 의미는 항상 같게 유지됩니다.

(이는 중요한 설계 결정입니다.)

 

분명하지 않은 경우

하지만 관례를 충족하는지 아닌지 확실하지 않을 때가 문제입니다.

 

예를 들어 함수를 세 배 한다는 것 다음과 같이 2가지 의미로 사용될 수 있습니다.

첫 번째: 함수를 세 번 반복하는 새로운 함수를 만들어 낸다고 생각할 수 있습니다.

operator fun.Int.times(operation: () -> Unit): ()->Unit = { repeat(this) { operation() }}

val tripleHello = 3 * { print("Hello") }
tripleHello() // 출력: HelloHelloHello

 

두 번째: 함수를 세 번 호출하는 함수

앞의 코드는 함수를 생성하고, 아래의 코드는 함수를 호출한다는 것에서 차이가 발생합니다.

operator fun.Int.times(operation: () -> Unit) { 
	repeat(this) { operation() }
}

3 * { print("Hello") } // 출력: HelloHelloHello

 

의미가 명확하지 않다면, infix를 활용한 확장 함수를 사용하는 것이 좋습니다.

(일반적인 이항 연산자 형태처럼 사용할 수 있습니다.)

infix fun.Int.timesRepeated(operation: () -> Unit) = { repeat(this) { operation() }}

val tripleHello = 3 timesRepeated { print("Hello") }
tripleHello() // 출력: HelloHelloHello

클래스 또는 다른 대상 내부에 있지 않는 top-level함수를 사용하는 것도 좋습니다.

 

규칙을 무시해도 되는 경우

지금까지 설명한 연산자 오버로딩 규칙을 무시해도 되는 중요한 경우가 있습니다. 바로 도메인 특화 언어(DSL)를 설계할 때입니다.

예를 들어 HTML, DSL을 생각해 보면

body {
	div {
    	+"Some text"
    }
}

문자열 앞에 String.unarayPlus가 사용된 것을 볼 수 있습니다.

 

정리

연산자 오버로딩은 그 이름의 의미에 맞게 사용해야 합니다. 의미가 명확하지 않다, 연산자 오버로딩을 사용하지 않는 것이 좋습니다. 대신 이름이 있는 일반함수를 사용하기 바랍니다. 꼭 연산자 같은 형태를 사용하고 싶다면, infix 확장 함수 또는 top-level함수를 활용하면 됩니다.