KotlinInAction/클래스, 객체, 인터페이스

컴파일러가 생성한 메서드: 데이터 클래스와 클래스 위임

webmaster 2022. 7. 31. 13:30
728x90

코틀린 컴파일러는 equals, hashCode, toString과 같은 기계적인 메서드를 생성하는 작업을 보이지 않는 곳에서 해준다.

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

자바와 마찬가지로 코틀린 클래스도 toString, equals, hashCode 등을 오버라이드 할 수 있다.

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

문자열 표현 : toString()

class Client(val name: String, val postalCode:Int){
    override fun toString(): String = "Client(name=$name, postalCode=$postalCode)"
}
fun main(args: Array<String>) {
    val client = Client("오현석", 4122)
    println(client)
}
  • 코틀린의 모든 클래스도 인스턴스의 문자열 표현을 얻을 방법을 제공한다.
  • 이런 문자열 표현으로부터 기본 문자열 표현보다 더 많은 정보를 얻을 수 있다.

객체의 동등성 : equals()

fun main(args: Array<String>) {
    val client1 = Client("오현석", 4122)
    val client2 = Client("오현석", 4122)
    println(client1 == client2) //== 연산자는 참조 동일성을 검사하지 않고 객체의 동등성을 검사한다
}
  • 서로 다른 두 객체가 내부에 동일한 데이터를 포함하는 경우 그 둘을 동일한 객체로 간주해야 할 때가 있다
  • 위 예제에서는 동등성을 검사하기 때문에 오버라이드 할 필요가 있다
class Client(val name: String, val postalCode:Int){
    override fun toString(): String = "Client(name=$name, postalCode=$postalCode)"
    override fun equals(other: Any?): Boolean { //Any는 Object에 대응하는 클래스로 코틀린의 모든 클래스의 최상위 클래스다
        if(other == null || other !is Client){
            return false
        }
        return name == other.name && postalCode == other.postalCode
    }
}
  • 코틀린의 is는 자바의 instanceof 와 같다. -> !is의 결과는 is 연산자의 결과를 부정한 값이다.
  • 코틀린에서는 override 변경자가 필수이므로 실수로 파라미터 타입을 잘 못쓸 수없다.
    • 그래서 equals를 오버라이드하고 나면 프로퍼티의 값이 모두 같은 두 고객 객체는 동등하리라 예측이 가능하다.
  • 단, client 클래스로 더 복잡한 작업을 수행하다 보면 제대로 작동하지 않을 수가 있는데 이는 hashcode 정의를 빠뜨려서 이다

해시 컨테이너 : hashCode()

자바에서 equals를 오버라이드 할 때 hashCode도 함께 오버라이드 해야 하는 이유?

fun main(args: Array<String>) {
    val processed = hashSetOf(Client("오현석", 4122))
    println(processed.contains(Client("오현석", 4122)))
}
  • equals() 메서드를 구현하고 프로퍼티가 동일하기 때문에 true가 나올 거라 예상하지만 false가 나온다
  • client가 hashCode 메서드를 정의하지 않았기 때문인데 JVM에서는 equals()가 true를 반환하는 두 객체는 반드시 같은 hashCode()를 반환해야 한다 라는 제약이 있기 때문이다.
  • processed 집합은 HashSet인데, HashSet 같은 경우 해시 코드를 비교하고 해시 코드가 같은 경우에만 equals함수를 통해 실제 값을 비교한다.
    • 그렇기 때문에 equals()를 검사하기 전 해시 코드가 달라 false를 반환하는 것이다
class Client(val name: String, val postalCode:Int){
    override fun toString(): String = "Client(name=$name, postalCode=$postalCode)"
    override fun equals(other: Any?): Boolean { //Any는 Object에 대응하는 클래스로 코틀린의 모든 클래스의 최상위 클래스다
        if(other == null || other !is Client){
            return false
        }
        return name == other.name && postalCode == other.postalCode
    }

    override fun hashCode(): Int =
        name.hashCode() * 31 + postalCode
    
}
  • hashCode를 오버라이드 하면 해당 문제는 해결된다

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

어떤 클래스가 데이터 저장하는 역할만을 수행한다면 toString, hashCode, equals 메서드를 반드시 오버라이드 해야 한다.

코틀린에서는 이런 메서드를 IDE를 통해 생성할 필요도 없이 data라는 변경자를 클래스 앞에 붙이면 필요한 메서드를 자동으로 만들어 준다.

data 변경자가 붙은 클래스를 데이터 클래스라고 한다

data class Client(val name: String, val postalCode: Int)
  • 자바에서 요구하는 모든 메서드를 포함한다
    • 인스턴스 간 비교를 위한 equals
    • HashMap과 같은 해시 기반 컨테이너에서 키로 사용할 수 있는 hashCode
    • 클래스의 각 필드를 선언 순서대로 표시하는 문자열 표현을 만들어 주는 toString
  • equals, hashCode는 주생성자에 나열된 모든 프로퍼티를 고려해 만들어진다.
    • 주생성자 밖에 정의된 프로퍼티는 equals, hashCode를 계산할 때 고려의 대상이 아니다.

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

데이터 클래스의 프로퍼티가 꼭 val일 필요가 없다 그러나 데이터 클래스의 모든 프로퍼티를 읽기 전용으로 만들어서 데이터 클래스를 불변 클래스로 만들라고 권장한다 

HashMap 등의 컨테이너에 데이터 클래스 객체를 담는 경우에는 불변성이 더욱 필수인데, 데이터 클래스 객체를 키로 하는 값을 컨테이너에 담은 다음에 키로 쓰인 데이터 객체의 프로퍼티를 변경하면 컨테이너 상태가 잘못될 수도 있기 때문이다. 게다가 불변 객체를 사용하면 프로그램에 대해 훨씬 쉽게 추론이 가능하고 다중 스레드 프로그램에서는 스레드 동기화 문제가 발생할 문제를 야기하지 않기 때문이다.

fun main(args: Array<String>) {
    
    val lee = Client("이계영", 4122)
    println(lee.copy(postalCode = 4000))
}
  • 데이터 클래스 인스턴스를 불변 객체로 더 쉽게 활용할 수 있게 코틀린 컴파일러는 copy라는 메서드를 제공한다
  • 객체를 복사하면서 일부 프로퍼티를 바꿀 수 있게 해 준다.
    • 복사본은 원본과 다른 생명주기를 가지며, 복사를 하면서 일부 프로퍼티 값을 바꾸거나 복사본을 제거해도 원본 프로퍼티에는 아무런 영향을 끼치지 않는다

By 키워드 사용(클래스 위임)

대규모 객체지향 시스템을 설계할 때 시스템을 취약하게 만드는 대부분을 문제는 구현 상속에 의해 발생한다 -> 하위 클래스가 상위 클래스의 메서드를 오버라이드 하게 되면 하위 클래스는 상위 클래스의 세부 구현 사항에 의존하게 되고, 상위 클래스에 구조가 바뀌거나 코드가 수정이 되면 하위 클래스에 영향을 받게 된다.

코틀린에서는 이런 설계를 문제점을 알기 때문에 final로 취급하기로 결정하였고, open변경자로 열어준 클래스만 확장이 가능하도록 하였다.

class DelegatingCollection<T>: Collection<T>{
    private val innerList = arrayListOf<T>()
    override val size: Int
        get() = innerList.size

    override fun contains(element: T): Boolean = innerList.contains(element)

    override fun containsAll(elements: Collection<T>): Boolean = innerList.containsAll(elements)

    override fun isEmpty(): Boolean = innerList.isEmpty()

    override fun iterator(): Iterator<T> = innerList.iterator()
}

하지만 종종 상속을 허용하지 않는 클래스에 새로운 동작을 추가해야 할 때인데 일반적으로 데코레이터 패턴이 있다 (상속을 허용하지 않는 클래스 대신 사용할 수 있는 새로운 클래스를 만들고, 기존 클래스와 같은 인터페이스를 데코레이터가 제공하게 만들고 기존 클래스를 데코레이터 내부 필드로 유지하며, 새로 정의하는 기능은 데코레이터의 메서드에 새로 정의하고, 기존 기능을 그대로 필요한 부분은 데코레이터의 메서드가 기존 클래스의 메서드에게 전달하는 패턴). 이런 패턴은 준비 코드가 상당히 많이 필요한 단점이 있는데 코틀린은 언어가 제공하는 일급 시민 기능으로 지원한다는 장점이 있다.

class DelegatingCollection<T> (
    innerList: Collection<T> = ArrayList<T>()
) : Collection<T> by innerList{
    
}
  • 인터페이스를 구현할 때 by 키워드를 통해 그 인터페이스에 대한 구현을 다른 객체에 위임 중이라는 사실을 명시할 수 있다.
  • 클래스 안에 있던 모든 메서드 정의가 사라졌다
    • 컴파일러가 그런 전달 메서드를 자동으로 생성하며 자동으로 생성한 코드의 구현은 DelegatingColletion에 있던 구현과 비슷하다
  • 메서드 중 일부의 동작을 변경하고 싶은 경우 메서드를 오버라이드 하면 컴파일러가 생성한 메서드 대신 오버라이드 한 메서드가 쓰인다.
    • 기존 클래스의 메서드에 위임하는 기본 구현으로 충분한 메서드는 따로 오버라이드 할 필요 없다
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)
    }
}
  • add와 addAll을 오버라이드 해서 카운터를 증가시키고 MulableCollection 인터페이스의 나머지 메서드는 내부 컨테이너에게 위임한다
  • CountingSet에 MutableCollection의 구현 방식에 대한 의존관계가 생기지 않는 것이 중요하다.
    • 클라이언트 코드가 CountingSet의 코드를 호출할 때 발생하는 일은 CountingSet안에서 마음대로 제어할 수 있지만, CountingSet 코드는 위임 대상 내부 클래스인 MutableCollection에 문서화된 API를 활용한다
    • 그러므로 내부 클래스 MutableCollection이 문서화된 API를 변경하지 않는 한 CountingSet 코드가 잘 동작할 것을 확신한다.

 

728x90