본문 바로가기
프로그래밍/kotlin

[kotlin] 코틀린 차곡차곡 - 9. 연산자 오버로딩 (Operator Overloading)

by 사바라다 2021. 7. 4.

안녕하세요. 오늘은 코틀린의 연산자 오버로딩에 대해서 알아보도록 하겠습니다.

연산자 오버로딩

Java, javascript 만 사용하여 코딩을 하셨던 분들은 연산자 오버로딩이라는 개념이 생소하실 수 있습니다. 만약 C++, C#, python 등의 언어를 사용해 본적이 있으시다면 해당 언어에서도 연산자 오버로딩을 제공하기 때문에 알고 있으실 수 도 있습니다.

연산자 오버로딩이란 위키에서 객체 지향 프로그래밍에서 다형성의 특별한 경우로 다른 연산자들이 함수 연사자를 통해서 구현을 할 때를 말한다. 라고 정의되어 있습니다. 이 말인 즉슨 +, - 등 과 같은 연산자가 어떤 값과 함께 사용하느냐에 따라서 다르게 동작할 수 있도록 그것을 커스터마이징 할 수 있다는 의미입니다.

java 에서도 연산자 오버로딩은 아니지만 같은 연산자에서 다르게 동작하는 예제가 있습니다. 아래코드를 보시겠습니다. String+ 연산자를 이용하면 내부에서 StringBuilder를 이용하여 String을 이어줍니다. 그리고 int를 +를 이용하면 내부에서 값을 더하기만 합나디.

String a = "sabarada is";
String b = "karol";
String result = a + " " + b // sabarada is karol

int c = 1
int d = 2
int result_2 = c + d // 3

코틀린에서의 연산자 오버로딩

코틀린은 이 뿐만 아니라 연산자 오버로딩이 가능하기 때문에 아래와 같은 방식이 가능합니다. 아래 코드 처럼 Price 라는 커스텀 객체를 만들고 이를 + 이어줄 수 있습니다. 연산자 오버로딩을 구현하기 위해서는 operator 라는 키워드를 이용합니다. 그리고 코틀린에 이미 정해져 있는 함수를 새롭게 정의하면 됩니다.

data class Price(val value: Int) {
    operator fun plus(b: Price): Price {
        return Price(value + b.value)
    }
}
val a: Price = Price(10)
val b: Price = Price(50)
val result: Price = a + b // 결과 Price(60)

예시

이미 예약되어있는 함수 이름을 이용하여 연산자 오버로딩이 가능합니다. 아래에서 그 리스트를 예제와 함께 보도록 합시다. 오늘은 아래 코드를 예제로 하도록 하겠습니다.

data class Price(val value: Int)

단항 연산자(Unary operations)

1개의 값을 올리고 내리는 연산자(operator)의 대표적인 예로는 ++ 또는 --가 있습니다. 일반적인 ++은 값을 1 올리는 연산자이며 --는 1 내리는 연산자입니다. ++inc 함수이며 --dec 함수로 오버라이딩하여 커스텀이 가능합니다. 아래 코드는 연산자 오버로딩을 사용해서 커스텀 한 후 사용한 코드입니다.

data class Price(val value: Int) {

    operator fun inc(): Price {
        return Price(value + 100)
    }

    operator fun dec(): Price {
        return Price(value - 100)
    }
}
var a = Price(500)
println("value = ${a++}") // 결과 : Price(value=500)
println("value = ${a}")   // 결과 : Price(value=600)
println("value = ${++a}") // 결과 : Price(value=700)

println("value = ${a--}") // 결과 : Price(value=700)
println("value = ${a}")   // 결과 : Price(value=600)
println("value = ${--a}") // 결과 : Price(value=500)

사칙 연산

더하기, 빼기, 곱하기, 나누기에 더해서 나머지, 그리고 +=, -= 연산자를 한번 보도록 하겠습니다. 여기서 참고하실 점은 +를 오버라이딩한다고 하여 += 도 동일한 연산을 하는 것은 아니라는 것입니다. 사칙연산에 대응되는 연산자 함수는 아래와 같습니다. 연산자 오버라이딩을 위해서는 아래의 표처럼 대응되는 각 함수를 오버라이딩하여 사용하시면 됩니다.

표현 함수
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.rem(b)
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.remAssign(b)

그렇다면 몇가지 예시를 보도록 하겠습니다. 대표적인 예로 +, % 그리고 +=를 한번 코드로 나타내보도록 하겠습니다. 아래 코드는 + 연산자를 나타내는 plus 함수를 연산자 오버로딩한 후 사요안 것입니다. Price 끼리 + 하여 새로운 Price가 생성되는 것을 확인할 수 있습니다.

data class Price(val value: Int) {
    operator fun plus(b: Price): Price {
        return Price(value + b.value)
    }
}

val a: Price = Price(10)
val b: Price = Price(50)
val result: Price = a + b // result : Price(60)

나머지 연산 또한 마찬가지입니다. rem 이라는 함수를 재정의하여 새로운 형태의 나머지 연산을 만들어내었습니다.

data class Price(val value: Int) {
    operator fun rem(price: Price): Price {
        return Price(value + price.value)
    }
}

val a = Price(500)
val b = Price(499)
println("rem = ${a % b}") // 결과 : rem = Price(1)

+= 연산은 이후에 나오는 값을 기존의 값에 더하는 연산입니다. 이 연산은 plusAssign 함수를 오버로딩하여 커스텀 할 수 있습니다.

data class Price(var value: Int) {
    operator fun plusAssign(value: Int) {
        this.value += value
    }
}

val a = Price(500)
val b = Price(499)
a += b
println("a = $a") // 결과 : a = Price(999)

get / set

get과 set 또한 중요한 연산자 오버로딩이 가능합니다. 연산자 오버로딩을 통해서 내부의 값을 조금 더 가독성 좋고 짧게 호출하거나 세팅하는 것이 가능합니다. 아래코드는 내부의 값을 가져오는 코드입니다. 아래처럼 get을 operator로 정의하면 [] 연선자롤 통해 가져올 수 있게됩니다.

data class Price(var value: Int) {
    operator fun get(num: Int): Int {
        return value
    }
}

val a = Price(500)        
println("a_price = ${a[100/* dummy 숫자 */]}") // 결과 : a = 500

set 함수를 정의하면 []연산자를 통해 값을 새로 대입할 수 있습니다. 내부에 만약 collection이 있다면 좀 더 간단하게 사용할 수 있음을 알 수 있습니다.

data class Price(var value: Int) {

    operator fun set(num: Int, value: Int) {
        this.value = value
    }
}

val a = Price(500)
a[100] = 10
println("a_price = ${a[100/* dummy 숫자 */]}") // 결과 : a = 10

생성자

마지막으로 알아볼 연산자 오버로딩은 생성자입니다. invoke 함수는 () 연산자를 나타냅니다. 즉 아래의 코드는 Price()를 호출하는 것이 operator fun invoke(): Price 연산자를 호출하는 결과를 가져올 수 있는 것입니다. 이를 이용하여 factory method를 추가적인 메서드 없이 사용할 수 있는 것처럼 구현할 수 있습니다.

data class Price private constructor(val value: Int) {

    companion object {
        operator fun invoke(): Price {
            return Price(100)
        }

        operator fun invoke(value: Int): Price {
            return Price(value)
        }
    }
}

val a = Price(500)
println("a = $a") // 결과 : a = Price(500)

val b = Price()
println("b = $b") // 결과 : b = Price(100)

마무리

오늘은 이렇게 코틀린에서의 연산자 오버로딩에 대해서 알아보는 시간을 가져보았습니다.

알아본 연산자 이외에도 범위를 나타내는 .., in 등이 있습니다.

이 부분에 대해서는 커스터 마이징을 해보려고 했으나 자료가 부족하여 진행하지 못했습니다.

다음에 기회가 되면 다시한번 도전하여 별도의 포스팅으로 찾아뵙고자 합니다.

감사합니다.

참조

https://ko.wikipedia.org/wiki/연산자_오버로딩

https://kotlinlang.org/docs/operator-overloading.html

댓글