ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [kotlin in action] 연산자 오버로딩과 기타 관례
    프로그래밍 언어/kotlin 2021. 8. 13. 19:37
    728x90
    반응형

    상호작용하는 클래스가 둘 이상이면, 다른 클래스에게 API를 제공하는 클래스가 적어도 하나 이상 있기 마련..! 

    7장 이후로는, 자신만의 API를 코틀린답게 만드는 방법을 배운다.

     

    어떤 언어 기능이, 정해진 사용자 작성 함수와 연결되는 경우 (타입에 의존)

    ex) for ... in 루프에 java.lang.Iterable을 구현한 객체를 사용

    try문에 java.lang.AutoCloseable 구현한 객체 사용

    코틀린에서는 어떤 언어 기능이 어떤 타입(클래스)과 연관되기보다는, 특정 함수 이름과 연관된다. (관례에 의존)

    *관례 : 어떤 언어 기능과, 미리 정해진 이름의 함수를 연결해주는 기법

     

    연산자 오버로딩

    산술 연산자 오버로딩

    • 이항 산술 연산 오버로딩

    연산자를 오버로딩하는 함수 앞에는 꼭 operator가 붙어야 한다. 

    멤버 함수로 정의

    data class Point(val x: Int, val y: Int) {
        operator fun plus(other: Point) : Point {
            return Point(x + other.x, y + other.y)
        }
    }
    println(p1 + p2) // +로 계산하면 plus 함수가 호출된다

    확장 함수로 정의

    operator fun Point.plus(other: Pointer): Point {
        return Point(x + other.x, y + other.y)
    }

    외부 함수의 클래스에 대한 연산자를 정의할 때는 관례를 따르는 이름의 확장함수로 구현하는게 일반적 패턴이다.

    함수 이름
    a * b times
    a / b div
    a % b mod
    a + b plus
    a - b miunus

     

    • 복합 대입 연산자 오버로딩

    +=, -=

    변경 가능한 컬렉션에 대해서 plusAssign 을 정의한다. 

    operator fun <T> MutableCollection<T>.plusAssign(element: T) {
        this.add(element)
    }

    plus 와 plusAssgin을 동시에 정의하지 말자.

    변경 불가능한 클래스 -> plus 정의

    변경 가능한 클래스 -> plusAssign 정의

     

    +, - 는 항상 새로운 컬렉션을 반환

    +=, -= 연산자는 항상 변경 가능한 컬렉션에 작용해 메모리에 있는 객체 상태를 변화

    +=, -= 연산자는 읽기 전용 컬렉션에서 변경을 적용한 복사본을 반환

     

    • 단항 연산자 오버로딩
    operator fun Point.unaryMinus(): Point {
        return Point(-x, -y)
    }
    함수 이름
    +a unaryPlus
    -a unaryMinus
    !a not
    ++a, a++ inc
    --a, a-- dec

    inc나 dec 함수를 정의해 오버로딩 하는 경우 컴파일러는 전위와 후의 증가/감소 연산자와 같은 의미를 제공한다.

    operator fun BigDecimal.inc() = this + BigDecimal.ONE

     

    비교 연산자 오버로딩

    • 동등성 연산자: equals

    널이 될 수 있는 값에도 호출 가능하다.

    class Point(val x: Int, val y: Int) {
        override fun equals(obj: Any?): Boolean {
            if (obj === this) return true // 두 피연산자가 같은 객체를 가리키고 있는지 검사 (최적화)
            if (obj !is Point) return false // 파라미터 타입 검사. Point 타입인가?
            return obj.x == x && obj.y == y
        }
    }

    equals함수에는 override가 붙어있다. Any에 정의된 메소드이므로, override가 필요하다. Any의 equals에는 operator가 붙어있기 때문에 이 메소드를 오버라이드 하면 자동으로 상위 클래스의 operator 지정이 적용된다. 또한 equals를 확장함수로 정의할 수 없다.

     

    • 순서 연산자: 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) // 여러 비교 함수
        }
    }

    compareValuesBy를 통해서 두 객체와 여러 비교함수를 인자로 받는다. 비교 함수는 람다, 프로퍼티, 메소드 참조일 수 있다.

    필드를 직접 비교하면 코드는 좀 복잡해 지지만 비교 속도는 빨라진다.

     

    컬렉션과 범위에 대해 쓸 수 있는 관례

    인덱스 연산자 : 인덱스를 사용해 원소를 설정하거나 가져오고 싶을 때 사용

    in 연산자  : 원소가 컬렉션이나 범위에 속하는지 검사하거나 컬렉션에 있는 원소를 이터레이션할 때 사용

     

    인덱스로 원소에 접근: get과 set

    mutableMap[key] = newValue

    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")
        }
    }
    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")
        }
    }

     

    in 관례

    객체가 컬렉션에 들어있는지 검사. contains 함수랑 대응한다.

    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 until lowerRight.x &&
               p.y in upperLeft.y until lowerRight.y
    }
    
    fun main(args: Array<String>) {
        val rect = Rectangle(Point(10, 20), Point(50, 50))
        println(Point(20, 30) in rect) // true
        println(Point(5, 5) in rect) // false
    }

     

    rangeTo 관례

    .. 연산자는 rangeTo 함수를 간략하게 표현하는 방법이다. rangeTo함수는 범위를 반환하며, 아무 클래스에나 정의할 수 있다. 

    val now = LocaleDate.now()
    val vacation = now..now.plusDays(10)
    println(now.plusWeeks(1) in vacation) // true

    범위 연산자는 우선순위가 낮아서 범위의 메소드를 호출하려면 범위를 괄호로 둘러싸야 한다.

     

    for 루프를 통한 iterator 관례

    iterator 메소드를 확장 함수로 정의할 수 있다.

    operator fun CharSequence.iterator(): CharIterator // 문자열을 iteration 할 수 있다

    클래스 안에 직접 iterator 메소드를 구현할 수도 있다.

    operator fun ClosedRange<LocalDate>.iterator(): Iterator<LocalDate> =
            object : Iterator<LocalDate> {
                var current = start
    
                override fun hasNext() =
                    current <= endInclusive
    
                override fun next() = current.apply {
                    current = plusDays(1)
                }
            }
    
    fun main(args: Array<String>) {
        val newYear = LocalDate.ofYearDay(2017, 1)
        val daysOff = newYear.minusDays(1)..newYear
        for (dayOff in daysOff) { println(dayOff) }
    }

     

    구조분해 선언과 component 함수

    구조분해 : 복합적인 값을 분리해서 여러 다른 변수를 한꺼번에 초기화할 수 있다.

    구조분해선언은 내부에서 다시 관례를 사용한다. 구조 분해 선언의 각 변수를 초기화하기 위해 componentN이라는 함수를 호출한다. 

    표준 라이브러리에서는 맨 앞의 다섯 원소에 대한 componentN을 제공한다.

     

    구조 분해 선언과 루프

    fun printEntries(map: Map<String, String>) {
        for ((key, value) in map) {
            println("$key -> $value")
        }
    }
    
    fun main(args: Array<String>) {
        val map = mapOf("Oracle" to "Java", "JetBrains" to "Kotlin")
        printEntries(map)
    }

    객체를 이터레이션 하는 관례

    -> 맵에 대한 확장함수로 iterator가 들어있다. 따라서 맵을 직접 이터레이션할 수 있다.

    public inline operator fun <K, V> Map<out K, V>.iterator(): Iterator<Map.Entry<K, V>> = entries.iterator()

    구조 분해 선언

    -> Map.Entry에 대한 확장 함수로 component1과 component2를 제공한다. (참고)

     

    프로퍼티 접근자 로직 재활용: 위임 프로퍼티

    뒷받침하는 필드에 단순히 저장하는 것보다 더 복잡한 방식으로 작동하는 프로퍼티를 쉽게 구현할 수 있다.

    프로퍼티의 접근자 로직을 매번 구현할 필요 없이, 위임을 사용해 자신의 값을 필드가 아니라 데이터베이스 테이블이나 브라우저 세션, 맵 등에 저장할 수 있다. 

     

    위임: 객체가 직접 작업을 수행하지 않고 다른 도우미 객체가 그 작업을 처리하게 맡기는 디자인패턴이다. 작업을 처리하는 도우미 객체를 위임 객체라고 부른다.

     

    위임 프로퍼티

    p 프로퍼티는 접근자 로직을 다른 객체에 위임한다. by 키워드는 프로퍼티와 위임 객체를 연결한다.

    class Foo {
        var p: Type by Delegate()
    }

    Delegate 클래스의 인스턴스를 위임 객체로 사용한다.

     

     

     

    위임 프로퍼티 사용: 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(args: Array<String>) {
        val p = Person("Alice")
        p.emails
        p.emails
    }

     

    위임 프로퍼티는 데이터를 저장할 때 쓰이는 뒷받침하는 프로퍼티와 값이 오직 한 번만 초기화됨을 보장하는 게터 로직을 함께 캡슐화해준다.

    class Person(val name: String) {
        val emails by lazy { loadEmails(this) }
    }

    lazy 함수는 시그니처의 getValue 메소드가 들어있는 객체를 반환한다. 이 객체를 처음 실행하면 get 메소드가 람다식을 실행하고 결과를 기억한다. 두번째 호출부터는 기억한 결과를 반환한다.

     

    위임 프로퍼티 구현

    open class PropertyChangeAware { // PropertyChangeSupport 사용하기 위한 도우미 클래스
        protected val changeSupport = PropertyChangeSupport(this) // 프로퍼티의 리스너 목록관리
    
        fun addPropertyChangeListener(listener: PropertyChangeListener) {
            changeSupport.addPropertyChangeListener(listener)
        }
    
        fun removePropertyChangeListener(listener: PropertyChangeListener) {
            changeSupport.removePropertyChangeListener(listener)
        }
    }
    
    class Person(
            val name: String, age: Int, salary: Int
    ) : PropertyChangeAware() { // 도우미클래스를 확장해 changeSupport 에 접근
        //프로퍼티의 변경 통지 직접 구현
        var age: Int = age
            set(newValue) {
                val oldValue = field
                field = newValue
                changeSupport.firePropertyChange(
                        "age", oldValue, newValue)
            }
    
        var salary: Int = salary
            set(newValue) {
                val oldValue = field
                field = newValue
                changeSupport.firePropertyChange(
                        "salary", oldValue, newValue)
            }
    }
    
    fun main(args: Array<String>) {
        val p = Person("Dmitry", 34, 2000)
        p.addPropertyChangeListener( // 프로퍼티 변경 이벤트 발생 시, PropertyChangeSupport 클래스의 인스턴스에 처리 위임
            PropertyChangeListener { event ->
                println("Property ${event.propertyName} changed " +
                        "from ${event.oldValue} to ${event.newValue}")
            }
        )
        p.age = 35
        p.salary = 2100
    }

    세터코드쪽 중복이 많이 보인다. 프로퍼티 값을 저장하고, 필요에 따라 통지를 보내주는 클래스를 추출해보자.

    프로퍼티 값을 저장하고 그 값이 바뀌면 자동으로 변경 통지를 전달해주는 클래스를 만들었다.

    // ...
    
    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 Person(
        val name: String, age: Int, salary: Int
    ) : PropertyChangeAware() {
    
        val _age = ObservableProperty("age", age, changeSupport)
        var age: Int
            get() = _age.getValue()
            set(value) { _age.setValue(value) }
    
        val _salary = ObservableProperty("salary", salary, changeSupport)
        var salary: Int
            get() = _salary.getValue()
            set(value) { _salary.setValue(value) }
    }
    
    // ...

     

    각각의 프로퍼티마다 ObservableProperty를 만들고 게터와 세터에서 ObservableProperty에 작업을 위임하는 준비 코드가 상당부분 필요하다.

    코틀린의 위임 프로퍼티 기능을 사용하여 이런 준비코드를 없애보자

    class ObservableProperty(
        var propValue: Int, val changeSupport: PropertyChangeSupport
    ) {
        operator fun getValue(p: Person, prop: KProperty<*>): Int = propValue
    
        operator fun setValue(p: Person, prop: KProperty<*>, newValue: Int) {
            val oldValue = propValue
            propValue = newValue
            changeSupport.firePropertyChange(prop.name, oldValue, newValue)
        }
    }
    
    class Person(
        val name: String, age: Int, salary: Int
    ) : PropertyChangeAware() {
    
        var age: Int by ObservableProperty(age, changeSupport)
        var salary: Int by ObservableProperty(salary, changeSupport)
    }

    코틀린은 위임 객체를 감춰진 프로퍼티에 저장하고, 주 객체의 프로퍼티를 읽거나 쓸때마다 위임 객체의 getValue, setValue를 호출해준다.

    Delegates.observable 함수를 사용하면 프로퍼티의 변경을 관찰할 수 있는 관찰자를 쉽게 추가할 수 있다.

    class Person(
        val name: String, age: Int, salary: 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)
        var salary: Int by Delegates.observable(salary, observer)
    }

     

    위임 프로퍼티 컴파일 규칙

    class C {
        var prop: Type by MyDelegate()
    }
    val c = C()
    class C {
        private val <delegate> = MyDelegate()
        var prop: Type
        get() = <delegate>.getValue(this, <property>)
        set(value: Type) = <delegate>.setValue(this, <property>, value)
    }

    컴파일러는 모든 접근자 안에 getValue와 setValue 호출 코드를 생성한다.

    -> 프로퍼티 값이 저장될 장소를 바꿀 수 있다.

    -> 프로퍼티를 읽거나 쓸 때 벌어질 일을 변경할 수 있다.

     

    프로퍼티 값을 맵에 저장

    class Person {
        // 추가 정보
        private val _attributes = hashMapOf<String, String>()
    
        fun setAttribute(attrName: String, value: String) {
            _attributes[attrName] = value
        }
    
        // 필수 정보
        // val name: String
        //  get() = _attributes["name"]!! // 수동으로 맵에서 꺼내기 
        val name: String by _attributes
    }
    
    fun main(args: Array<String>) {
        val p = Person()
        val data = mapOf("name" to "Dmitry", "company" to "JetBrains")
        for ((attrName, value) in data)
           p.setAttribute(attrName, value)
        println(p.name)
    }

    자신의 프로퍼티를 동적으로 정의할 수 있는 객체를 만들 때 위임 프로퍼티를 활용하는 경우가 있다. -> 확장 가능한 객체

    by 키워드 뒤에 직접 맵을 넣으면, getValue 가 호출되면서 프로퍼티의 이름을 키로 하는 값을 가져온다.

     

    프레임워크에서 위임 프로퍼티 활용

    객체 프로퍼티를 저장하거나 변경하는 방법을 바꿀 수 있으면 프레임워크를 개발할 때 유용하다. 

    ORM 프레임워크인 Exposed 를 예제로 살펴보자.

    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
    }

    Column 클래스 안에 getValue와 setValue메소드를 정의한다.

    operator fun <T> Column<T>.getValue(o: Entity, desc: KProperty<*>): T {
        // 데이터베이스 값 가져오기 
    }
    
    operator fun <T> Column<T>.setValue(o: Entity, desc: KProperty<*>, value: T) {
        // 데이터베이스 값 변경하기 
    }

    ( 풀코드 참고 )

     

    728x90

    댓글

Designed by Tistory.