[Kotlin] Data class
Data class는 Kotlin 언어로 바꾼 뒤 정말 많이 사용하는 클래스이다.
이러한 Data class가 일반 클래스와 어떤 것이 다르기 때문에 자주 사용되는 걸까에 대해서 알아볼까 한다.
우선 가장 큰 차이점은 Data class 자동으로 메서드를 만들어준다는 점이다.
자동으로 메서드를 만들어주는 게 왜 그렇게 큰 차이점일까에 대해서 궁금할 수도 있지만,
자동으로 만들어주는 메서드가 정~말정말 유용하기 때문에 Data class는 Kotlin에서 아주 사랑받는 녀석이다.
Data class에서 자동으로 만들어주는 메서드는 다음과 같다.
- toString()
- equals()
- hashCode()
- copy()
위 메서드를 살펴보기 전에 Data class의 특징에 대해서 먼저 알아볼까 한다.
Data class의 특징
기본생성자에 반드시 1개 이상의 파라미터가 있어야 한다.
오류 내용과 같이 data class 선언 시 생성자의 반드시 하나 이상의 파라미터가 있어야 한다.
생성자의 파라미터가 val 또는 var로 선언해야 한다.
오류 내용과 같이 생성자를 추가했지만, val과 var로 명시해줘야 한다는 오류내용을 확인할 수 있다.
다른 클래스를 상속받을 수 없다.
사진과 같이 SportCar가 Car 상속받는 구조로 만들고 싶었는데, 상속받을 수 없다는 오류를 확인할 수 있다.
이렇게 만들고 싶다면 다음과 같이 할 수 있다.
부모 클래스를 sealed class로 두어 car를 상속받는 SportsCar를 구현할 수 있다.
abstract, open, sealed, inner 등의 키워드를 붙일 수 없다.
koltin class의 default는 final이다.
final class는 상속이 불가능하기 때문에 open, abstract 등의 키워드를 붙일 수 없다.
자동으로 생성된 메서드들도 오버라이딩해서 구현할 수 있다.
data class에서 생성되는 메서들 중 가장 많이 사용되는 메서드가 toString()이라고 생각한다.
나는 주로 디버깅의 용도로 로그캣이나 프린트를 할 때 사용한다.
데이터 클래스의 제공해 주는 toString()의 호출결과이다.
생성자의 이름과 값을 확인할 수 있는 함수이다.
위 코드와 같이 데이터 클래스의 toString()을 오버라이딩 할 수 있다.
기존의 toString()이 아닌 오버라이딩된 toString()이 사용되는 것을 확인할 수 있다.
이렇게 Data class에서 자동으로 생성된 메서드들을 오버라이딩해서 사용할 수 있는 것도 Data class의 특징이다.
이제는 Data class에서 자동으로 생성해 주는 메서드들을 일반 클래스와 비교하고, Data class가 왜 많이 사용되는지 확인해 보겠다.
자동으로 생성되는 메서드
toString()
class People(val name: String, val age: Int, val salary: Int, val address: String)
People을 toString()하면 어떻게 될까?
다음과 같은 실행결과를 확인할 수 있다.
Data class에서 사용되는 toString()과 결과와 다른 People의 메모리주소가 출력된다.
즉 일반 클래스에서는 toString()을 항상 오버라이딩해서 구현해야 한다.
class People(val name: String, val age: Int, val salary: Int, val address: String){
override fun toString(): String {
return "name:${name}, age:${age}, salary:${salary}, address:${address}"
}
}
얼마나 번거로운 작업인가..
모든 클래스마다 toString()을 오버라이딩해서 구현해줘야 하지만,
Data class는 그럴 필요가 없다.
Copy()
이미 선언한 클래스 변수를 특정값만 바꿔서 사용하는 용도가 Copy()이다.
하지만 일반 클래스에서는 Copy()가 구현되어 있지 않다.
직접 구현해야 한다.
class People(val name: String, val age: Int, val salary: Int, val address: String){
override fun toString(): String {
return "name:${name}, age:${age}, salary:${salary}, address:${address}"
}
fun copy(newName: String = this.name, newAge: Int = this.age, newSalary: Int = this.salary, newAddress: String = this.address): People {
return People(newName, newAge, newSalary, newAddress)
}
}
fun main(){
val people = People("Jaehan",26,1000000000,"대한민국")
val newPeople = people.copy(newName="IU",newAge=30)
println(newPeople)
}
다음과 같이 copy의 코드를 작성하는 번거로움이 있다.
하지만 Data class라면 copy를 자동으로 생성해 주기 때문에 copy의 코드를 작성할 필요가 없다.
data class People(val name: String, val age: Int, val salary: Int, val address: String)
fun main(){
val people = People("Jaehan",26,1000000000,"대한민국")
val newPeople = people.copy(name = "IU", age =30)
println(newPeople)
}
이러한 편리함 때문에 Data class를 사용하지 않을 이유가 없다.
hashCode()
hashCode는 클래스의 고유한 값을 확인할 수 있는 메서드이다.
이러한 불일치를 해결하기 위해서 자바는 hashCode를 오버라이딩해서 사용한다.
class People(val name: String, val age: Int, val salary: Int, val address: String){
override fun toString(): String {
return "name:${name}, age:${age}, salary:${salary}, address:${address}"
}
fun copy(newName: String = this.name, newAge: Int = this.age, newSalary: Int = this.salary, newAddress: String = this.address): People {
return People(newName, newAge, newSalary, newAddress)
}
}
fun main(){
val people = People("Jaehan",26,1000000000,"대한민국")
val newPeople = People("Jaehan",26,1000000000,"대한민국")
println(people.hashCode()==newPeople.hashCode())
}
자바를 사용하면서 불편한 점은 분명히 해당 클래스는 같은 파라미터의 값을 가지고 있지만 hashCode는 다른 것이다.
두 객체는 논리적으로 동일하지만.. hashCode는 다른..? 왜!!!
people과 newPeople은 같은 파라미터를 가지고 있다.
하지만 Data class는 자동으로 생성된 hashCode를 통해서 이러한 불일치를 해결해 준다.
data class People(val name: String, val age: Int, val salary: Int, val address: String)
fun main(){
val people = People("Jaehan",26,1000000000,"대한민국")
val newPeople = People("Jaehan",26,1000000000,"대한민국")
println(people.hashCode()==newPeople.hashCode())
}
두 객체가 논리적으로 동일하기에, hashCode는 같은 값을 가진다.
equals()
두 객체를 비교하는 연산인 equals()이다.
class People(val name: String, val age: Int, val salary: Int, val address: String){
override fun toString(): String {
return "name:${name}, age:${age}, salary:${salary}, address:${address}"
}
fun copy(newName: String = this.name, newAge: Int = this.age, newSalary: Int = this.salary, newAddress: String = this.address): People {
return People(newName, newAge, newSalary, newAddress)
}
}
fun main(){
val people = People("Jaehan",26,1000000000,"대한민국")
val newPeople = People("Jaehan",26,1000000000,"대한민국")
println(people.equals(newPeople))
}
people과 newPeople는 동일한 데이터를 포함하고 있지만 equals를 통한 동등성비교에서 false를 반환한다.
그렇기 때문에 java에서 equals는 오버라이딩해서 프로퍼티가 동일한지 작성해줘야 한다.
override fun equals(other: Any?): Boolean {
if (other == null || other !is People)
return false
return name == other.name &&
age == other.age &&
salary == other.salary &&
address == other.address
}
이렇게 equals()를 오버라이딩한 후 비교를 하면 객체가 동일하다는 결과를 얻을 수 있다.
하지만 이러한 코드 작성 역시 양이 많고, 불편한 것이 사실이다.
data class People(val name: String, val age: Int, val salary: Int, val address: String)
fun main() {
val people = People("Jaehan", 26, 1000000000, "대한민국")
val newPeople = People("Jaehan", 26, 1000000000, "대한민국")
println(people == newPeople)
}
equals()를 오버라이딩 할 필요 없이 data class의 eqauls는 객체의 생성자가 같은지 비교하기 때문에 true를 반환한다.
느낀점
class의 불변성을 유지하면서 변경하기 때문에 Data class의 copy를 정말 많이 사용한다.
class의 프로퍼티를 var보다는 val로 불변성을 유지하고, 변경을 해야 한다면 copy를 사용하는 것이 맞는 방향성이라고 생각한다.
그 외에도 toString을 자동으로 제공해 준다는 장점이 정말 크다고 생각한다.
자동으로 생성되는 메서드들이 주는 편리함 덕분에 Data class를 많이 사용한다고 생각한다.