[Kotlin in Action] 19. 코틀린의 toString(), equals(), hashCode(), copy() 메서드 구현과 자동생성 data 변경자, 클래스 위임 by

반응형
728x90
반응형

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

자바와 마찬가지로 코틀린 클래스도 toString, equals, hashCode 등을 오버라이드 할 수 있다. 코틀린은 이런 메서드 구현을 자동으로 생성해줄 수 있다. 

 

자동으로 생성해주기전, 메서드를 직접 구현해보자.

 

 

toString() 구현

자바처럼 코틀린의 모든 클래스도 인스턴스의 문자열 표현을 얻을 방법을 제공한다.

class CustomClient(val name: String, val postalCode: Int) {
    /* toString */
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"
}

 

 

equals() 구현

코틀린에서 == 연산자가 내부적으로 equals를 호출해서 객체를 비교한다.

따라서 클래스가 equals를 오버라이드하면 ==를 통해 안전하게 그 클래스의 인스턴스를 비교할 수 있다. 참조 비교를 위해서는 === 연산자를 사용할 수 있다. 연산자는 자바에서 객치의 참조를 비교할때 사용하는 ==와 동일하다.

자바에서는 ==를 원시타입과 참조 타입을 비교할때 사용한다.
원시 타입의 경우
- 두 피연산자의 값이 같은가? (동등성)

참조 타입의 경우
- 피연산자의 주소가 같은가? (참조 비교)
class CustomClient(val name: String, val postalCode: Int) {
    /*
    Any : 코틀린의 모든 클래스의 최상위 클래스다. Any? 는 null이 될수 있는 타입이므로 other 는 null이 될 수 있다.
     */
    override fun equals(other: Any?) : Boolean {
        // other 이 Client 인지 검사 (is 는 자바의 instanceOf와 같다.)
        if (other == null || other !is CustomClient) return false
        // 두 객체의 프로퍼티 값이 서로 같은지 검사
        return name == other.name && postalCode == other.postalCode
    }
}

 

 

hashCode() 구현

자바에서는 equals를 오버라이드할때 반드시 hashCode 로 함께 오버라이드해야한다.

class CustomClient(val name: String, val postalCode: Int) {
    /* hashCode */
    override fun hashCode() : Int = name.hashCode() * 31 + postalCode
}

 

위 구현 메서드 호출
fun main() {
    /* toString */
    val client1 = CustomClient("김서해", 1234)
    println(client1) // Client(name=김서해, postalCode=1234)

    /* equals */
    val client2 = Client("김서해2", 12345)
    val client2_2 = Client("김서해2", 12345)
    println(client2 == client2_2) // false

    // override
    val client3 = CustomClient("김서해3", 12345)
    val client3_2 = CustomClient("김서해3", 12345)
    println(client3 == client3_2) // true
}

 

 

data 변경자

위에서 직접 구현해본 toString(), equals(), hashCode()는 모두 코틀린에서 자동으로 생성해준다.
data 라는 변경자를 클래스 앞에 붙이면 필요한 메서드를 컴파일러가 직접 생성해준다.
data 변경자가 붙은 클래스를 데이터 클래스라고 부른다.

 

  • 인스턴스간 비교를 위한 euqals
  • HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
  • 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어주는 toString
data class AutoClient(val name: String, val postalCode: Int)

equals와 hashCode는 주 생성자에 나열된 모든 프로퍼티를 고려해 만들어진다. 생성된 equals 메서드는 모든 프로퍼티 값의 동등성을 확인한다. hashCode 메서드는 모든 프로퍼티의 해시 값을 바탕으로 계산한 해시 값을 반환한다.

 

이때 주 생성자 밖에 정의된 프로퍼티는 equals, hashCode를 계산할때 고려의 대상이 아니므로 유의하자.

 

 

copy() 메서드

데이터 클래스의 프로퍼티가 꼭 val일 필요는 없다. 원한다면 var 프로퍼티를 써도 된다.
하지만 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장한다. HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우엔 불변성이 필수적이다.


데이터 클래스 객체를 키로 하는 값을 컨테이너에 담은 다음에 키로 쓰인 데이터 객체의 프로퍼티를 변경하면, 컨테이너 상태가 잘못될 수 있다. 다중스레드 프로그램의 경우 스레드 동기화를 해야할 필요가 줄어든다.

데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있게 코틀린 컴파일러는 한가지 편의 메서드를 제공한다. 그 메서드는 객체를 복사(copy)하면서 일부 프로퍼티를 바꿀 수 있게 해주는 copy 메서드다.
객체를 메모리상에서 직접 바꾸는 대신 복사본을 만드는 편이 더 낫다.

복사본은 원본과 다른 생명주기를 가지며, 복사를 하면서 일부 프로퍼티 값을 바꾸거나 복사본을 제거해도 원본을 참조하는 다른 부분에 전혀 영향을 끼치지 않는다.

 

자동생성 - data 클래스

data class CopyClient(val name: String, val postalCode: Int)

 

직접 구현

class CopyCustomClient(val name: String, val postalCode: Int) {
    /* equals */
    override fun equals(other: Any?) : Boolean {
        // other 이 Client 인지 검사 (is 는 자바의 instanceOf와 같다.)
        if (other == null || other !is CopyCustomClient) return false
        // 두 객체의 프로퍼티 값이 서로 같은지 검사
        return name == other.name && postalCode == other.postalCode
    }

    /* toString */
    override fun toString() = "Client(name=$name, postalCode=$postalCode)"

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

    /* copy */
    fun copy(name: String = this.name, postalCode: Int = this.postalCode) = CopyCustomClient(name, postalCode)
}

 

호출
fun main() {
    val client2 = CopyCustomClient("김서해", 12345)
    println(client2) // Client(name=김서해, postalCode=12345)
    println(client2.copy(postalCode = 12345)) // Client(name=김서해, postalCode=12345)

    val client = CopyClient("김서해", 12345)
    println(client.copy(postalCode = 12345)) // Client1(name=김서해, postalCode=12345)
}

 

 

 

클래스 위임 by 키워드

대규모 객체지향 시스템을 설계할때 시스템을 취약하게 만드는 문제는 보통 구현 상속에 의해 발생한다.
하위 클래스가 상위 클래스의 메서드 중 일부를 오버라이드하면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존하게 된다. 시스템이 변함에 따라 상위 클래스의 구현이 바뀌거나 상위 클래스에 새로운 메서드가 추가된다. 이 과정에서 하위 클래스가 상위 클래스에 대해 갖고있던 가정이 깨져서 코드가 정상적으로 작동하지 못하는 경우가 생길 수 있다.

코틀린을 사용하면서 이런 문제를 인식하고 기본적으로 클래스를 final로 취급하기로 결정했다. 모든 클래스를 기본적으로 final로 취급하면 상속을 염두에 두고 open 변경자로 열어둔 클래스만 확장할 수 있다.
열린 상위 클래스의 소스코드를 변경할때는 open 변경자를 보고 해당 클래스를 다른 클래스가 상속하리라 예상할 수 있으므로, 변경시 하위 클래스를 깨지 않기 위해 좀더 조심할 수 있다.

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

상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 만들되, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고, 기존 클래스를 데코레이터 내부에 필드로 유지하는 것이다.

새로 정의해야하는 기능은 데코레이터의 메서드에 새로 정의하고, 기존 기능이 그대로 필요한 부분은 데코레이터의 메서드가 기존 클래스의 메서드에게 요청을 전달(forwarding) 한다.
이런 위임은 코틀린이 제공해준다. 인터페이스를 구현할때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다.

 

Before

class CustomDelegatingCollection<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)
}

 

After

// by 키워드 사용 (위 예제 재작성)
class DelegatingCollection<T> (
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList{}

 

 

by 키워드 예제

class CountingSet<T> (
    val innerSet: MutableCollection<T> = HashSet<T> ()
) : MutableCollection<T> by innerSet { // MutableCollection의 구현을 innerSet 에게 위임한다.
    var objectsAdded = 0

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

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

1) MutableCollection의 구현을 innerSet 에게 위임한다.

class CountingSet<T> (
    val innerSet: MutableCollection<T> = HashSet<T> ()
) : MutableCollection<T> by innerSet

 

2) add(), addAll()은 오버라이드한다.

메서드 중 일부의 동작을 변경하고 싶은 경우 메서드를 오버라이드하면 컴파일러가 생성한 메서드 대신 오버라이드한 메서드가 쓰인다.
기존 클래스의 메서드를 위임하는 기본 구현으로 충분한 메서드는 따로 오버라이드할 필요가 없다.

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

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

 

호출
fun main() {
    val cset = CountingSet<Int>()
    cset.addAll(listOf(1, 1, 2))
    // 3 objects were added, 2 remain
    println("${cset.objectsAdded} objects were added, ${cset.size} remain")
}

 

반응형

Designed by JB FACTORY