오늘도 더 나은 코드를 작성하였습니까?

클래스 객체 인터페이스(컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임) 본문

Kotlin in Action/코틀린 기초

클래스 객체 인터페이스(컴파일러가 생성한 메소드: 데이터 클래스와 클래스 위임)

hik14 2020. 8. 14. 18:31

자바에서는 필수적인  eqauls, hashCode, toString 등의 메서드를 클래스 생성 시 기계적으로 구현을 해둬야 한다.

하지만 코틀린 컴파일러는 이런 메서드를 보이지 않는 곳에서 생성해준다. 따라서 필수 메서드로 인한 잡음 없는 깔끔한 소스코드를 유지하는 것이다 가능하다

 

모든 클래스가 정의해야 하는 메서드

 

- 자바와 마찬가지로 코틀린 역시 eqauls, hashCode, toString 등을 오버라이드 할 수 있다. 

class Client(val name: String, val postCode: Int)

 

문자열 표현 : toString()

 

- 객체(Instance)의 정보를 표현하는 메서드

- 기본적으로 주소 값을 보여주지만 오버라이드로 유용한 표현으로 바꿔주는 게 좋다.

class Client(val name: String, val postCode: Int){

    override fun toString(): String =
            "Client(name= $name, postCode= $postCode)"
}

 

객체의 동등성: equals()

 

- 서로 다른 두 객체가 내부에 동일한 데이터를 포함하는 경우 그 둘을 동등한 객체로 간주할 수 있어야 한다.

 

* 자바에서 == 두 객체의 참조의 동일성(주소 값)을 검사하지만 코틀린에서는 == 연산은 equals를 호 출하식으로 컴파일이 된다.

class Client(val name: String, val postCode: Int){

    override fun equals(other: Any?): Boolean {
        if(other == null || other !is Client)
            return false
        return name == other.name && postCode == other.postCode
    }

    override fun toString(): String =
            "Client(name= $name, postCode= $postCode)"
}

Any는 자바의 Object클래스에 대응하는 최상위 클래스이다. 

? 는 값이 null 일수 있음을 알려준다.

is는 타입 검사를 한다는 점에서 자바의 instanceOf와 동일하다.

하지만 이후 자동으로 불변타입이라면, 타입변환(스마트캐스팅)이 된다. 

 

 

해시 컨테이너: hashCode()

 

* JVM 언어에서는 hashCode가 지켜야 하는 "equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 된다. 는 제약이 있다.

 

hash 기반으로 이루어진 자료구조 Hashset, HashMap, HsahTable 등은 내부 데이터를 비교 및 탐색할 때 비용을 줄이기 위해 먼저 객체의 hashnCode를 비교하고 이 코드가 동일한 경우에만 실제 값을 비교한다. 즉 equals가 true라는 사실은 판단에 영향을 주지 못한다.

class Client(val name: String, val postCode: Int){

    override fun equals(other: Any?): Boolean {
        if(other == null || other !is Client)
            return false
        return name == other.name && postCode == other.postCode
    }

    override fun hashCode(): Int = name.hashCode()*31 + postCode

    override fun toString(): String =
            "Client(name= $name, postCode= $postCode)"
}

 

데이터 클래스 : 모든 클래스가 정의해야 하는 메서드 자동생성

- 위와 같은 데이터를 저장하는 역할을 수행하는 클래스들이 오버라이드 해야 하는 메서드를 코틀린은 data 변경자를 통해 컴파일러가 만들어준다.

data class Client(val name: String, val postCode: Int)

equals와 hashCode는 주생 성자에 나열된 모든 프로퍼티를 고려해서 만들어진다.

equals는 모든 프로퍼티의 동등성을 확인

hashCode 모든 프로퍼티의 해시 값을 바탕으로 계산해서 해시값을 반환한다.

 

* 주 생성자 밖에 정의된 프로퍼티는 반영되지 않음이 중요하다. 

 

 

데이터 클래스와 불변성: copy()

 

프로퍼티가 꼭 val 필요는 없지만 데이터 클래스의 경우 모든 프로퍼티를 val(읽기 전용)으로 만들어서 사용할 것을 권장한다. 불변 객체를 사용 시 프로그램에 훨씬 쉽게 추론 가능하며 다중 스레드 프로그램일수록 더욱 중요하다.

 

데이터 클래스 객체를 불변 객체로 더욱 쉽게 활용할 수 있게 코틀린 컴파일러는 한 가지 편의 메서드를 제공한다.

 

copy - 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해주는 메서드

 

객체를 메모리상에서 직접 바꾸는 것보단 복사본을 만들어 일부 프로퍼티 값을 변경하거나 복사본을 제거해도 프로그램에서 원본을 참조하는 다른 부분에 전혀 영향을 끼치지 않는다.

 

class Client(val name: String, val postCode: Int){

    override fun equals(other: Any?): Boolean {
        if(other == null || other !is Client)
            return false
        return name == other.name && postCode == other.postCode
    }

    override fun hashCode(): Int = name.hashCode()*31 + postCode

    override fun toString(): String =
            "Client(name= $name, postCode= $postCode)"

    fun copy(name: String = this.name, postCode: Int= this.postCode) = Client(name, postCode)
}

data 클래스는 알아서 copy 메서드를 컴파일러가 자동으로 만들어준다. 

 

클래스 위임: by 키워드

대규모 객체지향 시스템을 설계할 때 시스템을 취약하게 하는 것은 보통 구현, 상속에 의해 발생한다.

하위 클래스는 상위 클래스의 세부 구현 사항에 의존하는데 상위 클래스의 변경으로 하위 클래스의 상위 클래스에 대한 가정이 깨진다. 

 

하지만 상속을 허용하지 않는 클래스에 대해 새로운 동작을 추가할 때가 있는데 일반적인 방법이 데코레이터 패턴이다.

 

데코레이터 패턴의 핵심은

기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부의 필드로써 유지하는 것이다.

 

하지만 이러한 방법은 준비 코드가 상당히 많이 필요하다.

class DelegatingCollection<T>: Collection<T>{
    private val innerList = arrayListOf<T>()

    override val size: Int
        get() = innerList.size

    override fun isEmpty(): Boolean = innerList.isEmpty()
    override fun contains(element: T): Boolean = innerList.contains(element)
    override fun iterator(): Iterator<T> = innerList.iterator()
    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)
    
}

 

코틀린은 인터페이스를 구현할 때 by 키워드를 사용해서 그 인터페이스에 대한 구현을 다른 객체에 위임 중을 명시한다.

class DelegatingCollection<T>(private val innerList: Collection<T> = ArrayList<T>())
    : Collection<T> by innerList{}

일부 메서드의 동작을 바꾸고 싶다면 오버라이드 하면 된다.

 

원소 추가 횟수를 기록하는 컬렉션을 만들어보자 

 

class CountingSet<T>(val innerSet: MutableCollection<T> =HashSet<T>()): MutableCollection<T> by innerSet{
    var objectAdded = 0

    override fun add(element: T): Boolean {
        objectAdded++
        return innerSet.add(element)
    }

    override fun addAll(elements: Collection<T>): Boolean {
        objectAdded += elements.size
        return innerSet.addAll(elements)
    }
}

fun main() {
    val countingSet = CountingSet<Int>()
    countingSet.addAll(listOf(1,1,2))
    println("${countingSet.objectAdded} objects were added ${countingSet.size} remain")
}

- add와 addAll은 오버 라이딩을 하였고 나머지 메서드는 내부 컨테이너인 innerSet: MutableCollection <T>에 위임하였다.

- CountingSet은 MutableCollection의 구현 방식에 의존관계가 생기지 않는다는 점이 중요하다.

- 내부 클래스인 MutableCollection이 문서화된 API를 변경하지 않는 한 계속 잘 작동한다.