[HeadFirst] Kotlin 데이터 클래스

timothy jeong·2021년 10월 26일
0

코틀린

목록 보기
9/20

Java 에서 단순히 데이터를 옮기는 용도로 사용하기 위한 객체 dto 라는 개념이 있었다. Java 에서는 여러 에노테이션을 통해 dto 를 구성해야 했다면 kotlin 은 data class 를 준비해뒀다.

==(equivalence 검사) & .equals()

equality 검증 할때 == 를 이용해 왔다. 그런데 내부적으로는 == 를 호출할때마다 equals() 함수를 호출하고 있다.

기본적으로 equals() 메서드는 두 변수가 동일한 레퍼런스 값을 가지고 있는지 체크한다. 일반 primitive type 에 대해서도 레퍼런스 값을 검사하는 걸까? 변수에 문자, 숫자, boolean 값을 넣는 것은 모두 메모리에 존재하는 리터럴 문자를 넣는 것이고, primitive type 이 같은 값을 갖는 다는 것은 같은 리터럴 값을 참조하고 있다고 생각할 수 있다. 그러므로 위 질문에 대한 대답은 '그렇다' 이다.

모든 객체는 equals() 메서드를 가지고 있는데, 이는 해당 메서드가 Any 클래스에 정의 되어 있기 때문이다. 모든 클래스들이 Any 라는 rootSuperClass 를 갖고 있기 때문에 클래스들이 동일한 행위(toString, equals, hashCode)를 갖고, 모든 클래스를 대상으로 다형성을 이용할 수 있다.

=== 연산자(identity 검사)
Java 에서도 제공되었던 기능이다. idenity 검증을 위해 사용되는 연산자이다. kotlin 에서는 == 연산자의 행위가 equals() 정의에 따라 달라지므로, 언제나 일관된 결과를 반환하는 동등연산자가 필요했다.
=== 는 언제나 두 변수가 참조하고 있는 참조값이 동일한지 검사한다.

Any

Any 클래스가 만든 Common Protocol 을 살펴보자.

equlas(any: Any) : Boolean

hashCode : Int
해당 객체의 hashCode 값을 반환한다. hashTable, hashMap 등의 자료구조를 만들때 사용된다.

toString() : String
해당 객체를 대변할 수 있는 String 메시지를 반환한다. 기본적으로 해당 객체의 이름과 그와 관련된 숫자를 반환한다.

이상의 함수들은 하위 객체에서 override 될 수 있다.

data class & equals()

데이터 클래스의 역할의 관점에서 equals() 메서드를 보자, 완전히 동일한 정보를 가지고 있는 두개의 data class Recipe 이 있다. 하지만 둘은 서로 다른 인스턴스이기 때문에 일반적인 equals() 를 쓰면 둘은 같지 않다는 결과를 얻게 될 것이다.


data class Recipe(val title: String, val isVegetarian: Boolean) {}

fun main() {
    val recipe1 = Recipe("soup", false)
    val recipe2 = Recipe("soup", false)

    println(recipe1.equals(recipe2)) // false
}

하지만 그렇지 않다. 일반 class 였다면 다르다는 결과를 반환 받았겠지만 data class 는 그 원래 목적에 집중하여, 두 class 의 내용물이 같다면 둘은 동일하다고 판단한다.

data class 는 Any 가 상속해준 모든 메서드 (toString, equals, hashCode) 를 모두 목적에 맞게 override 해버린다.

주의사항
컴파일러는 주 생성자(primary constructor) 에 있는 프로퍼티만을 고려해서 equals, hashcode, toString 을 override 한다. 즉, data class body 부분에 선언된 변수는 equals 판단에서 고려되지 않는다.

data class 의 활용

copy()

data class 를 복제하기 위해서는 copy() 메서드를 쓰는게 좋다. 특정 값만 변경되고, 나머지 값들은 유되는 기능을 제공하기 때문이다.

data class Recipe(val title: String, val isVegetarian: Boolean) {}

fun main() {
    val recipe1 = Recipe("soup", false)
    val recipe2 = recipe1.copy(isVegetarian = true)

    println(recipe2.isVegetarian)
}

componentN 함수과 destructure data object

data class 를 선언하면 컴파일러가 해당 객체의 프로퍼티에 접근하는 색다른 기능을 제공하는 함수를 추가한다. 이러한 함수들을 componentN 함수라고 부른다. N은 프로퍼티의 갯수를 의미한다.

쉽게 말하면 프로퍼티가 선언된 순서로 접근하는 방법이다.

data class Recipe(val title: String, val isVegetarian: Boolean) {}

fun main() {
    val recipe1 = Recipe("soup", false)
    val recipe2 = recipe1.copy(isVegetarian = true)

    val title = recipe2.component1()
    println(title)
}

그런데 이 방법보다는 차라리 recipe2.title 로 접근하는 방법이 더 직관적이지 않은가? 그렇다면 componentN 함수는 왜 필요한 걸까?

componentN 함수가 있음으로, data class 를 프로퍼티 단위로 분해하기가 훨씬 수월해진다. 더 generic 한 내부 함수를 만들 수 있다는 것이다.

data class Recipe(val title: String, val isVegetarian: Boolean) {}

fun main() {
    val recipe1 = Recipe("soup", false)
    val recipe2 = recipe1.copy(isVegetarian = true)

    val title = recipe2.title
    val isVegetarian = recipe2.isVegetarian

    val (title2, isVegetarian2) = recipe2

    println("$title $isVegetarian\n$title2 $isVegetarian2" )
}

data class & val

데이터 class 는 val 로 만드는 것이 추천된다. data variable 의 참조값이 바뀌는 것은 혼란을 초래할 수 있기 때문이다.

data class constructing trick

data class 는 주 생성자에 선언된 프로퍼티들만 자동 override 되는 함수들의 고려대상이 된다. 그렇기 때문에 equality 검사에서 고려되어야 하는 property 들이 많아지면 코드가 복잡해진다.

assign default value

이럴때는 선언된 프로퍼티에 default value 를 할당함으로써 코드의 복잡성을 줄일 수 있다.

이러한 default value 는 function 을 정의하는데도 사용할 수 있다. 만약 deafault value 이외의 function 을 정의하고 싶으면 overload 해버리면 된다.

named argument

주 생성자에 선언된 순서대로 argument 에 값을 넣어주는것도 번거로운 일이다. 이때는 프로퍼티 명 = 값 이런식으로 argument 에 넣어주면 된다.

profile
개발자

0개의 댓글