본문 바로가기

Kotlin

[Kotlin] 3. Data Class, by

#데이터 클래스 (Data Class)

  • 데이터를 저장하는 클래스를 사용할 때 유용한 클래스이다
  • 'data class 클래스명 constructor(val name: String, val grade: Int)' 형태로 사용한다 

 

[Kotlin Data Class의 기능]

코틀린의 Data Class는 toString(), equals(), hashCode() 를 컴파일러가 자동 생성 해준다

  1. toString() 오버라이드 :  클래스의 데이터 값을 문자열로 편하게 보고자
  2. equals() 오버라이드 :  동등 비교를 하고 싶을 경우 
  3. hashCode() 오버라이드 : equals()오버라이드 하면 hashCode도 오버라이드 해야 함

Java나, 코틀린 일반 클래스를 사용한다면, 위의 3가지 함수를 일일이 오버라이딩 해서 사용해야 하는데, 

일단 수동으로 어떻게/왜 오버라이딩 해서 사용하는 지, 동등 비교는 뭐고 동일 비교는 무엇인지 더 확인하고 싶은 경우

더보기를 통해...

더보기
  1. 동등 비교 : 두 객체의 주소값이 다르더라도 내부 데이터 값은 같은지 비교
    • Java에선 equals 
    • Kotlin에선 == (내부적으로 equals 호출)
  2. 동일 비교 : 두 객체의 주소값 비교 
    • Java에선 == 
    • Kotlin에선 === 

Java 최상위 객체인 Object의  equals는 원래 내부적으로 동일 비교로(==) 이루어져 있는데

우리가 지금까지 String 값비교를 equals 로 할 수 있었던 이유는

String 클래스의 equals가 Override 되어 있었기 때문이다(값 비교 결과를  return 하게끔)

 

아래 예제는 JAVA/KOTLIN에서 각각 equals 오버라이드 하지 않고 각각 동등/동일 비교를 진행 한 예제이다

동등, 동일 비교 모두 false가 반환 되는 것을 볼 수 있다

동등 비교에서 value값이 같을 경우 true를 반환하려면, equals를 오버라이드 해야 한다

JAVA

        Student student1 = new Student("Tina", 20);
        Student student2 = new Student("Tina", 20);
        Log.i("test", "[Java] Student1  Student2 동등 비교 = " + student1.equals(student2)); //false
        Log.i("test", "[Java] Student1  Student2 동일 비교 = " + (student1 == student2)); //false

KOTLIN

 

        val student1 = Student("Tina", 20)
        val student2 = Student("Tina", 20)
        println("[Kotlin] Student1  Student2 동등 비교 = " + (student1 == student2)) // false  -> 코틀린에서의 '==' 는 equals를 호출
        println("[Kotlin] Student1  Student2 동일 비교 = " + (student1 === student2)) //false

 

 

Students 내부 데이터 값들이 같은 두 객체가 있고,  둘을 동등한 객체로 취급하고 싶을 때 우리는 equals를 오버라이드 한다 (아래와 같이)

class Student(var name: String, var age: Int) {
   
   override fun equals(other: Any?): Boolean {
        return if (other != null && other is Student) {
            (other.age == this.age) && (other.name == this.name)
        } else {
            false
        }
    }

    override fun hashCode(): Int {
        return name.hashCode() * age * 130
    }
}

*JVM에서는 equals() 메소드로 두 객체 비교시 equal 할 경우, hashCode도 두 객체는 동일한 integer를 반환해야 한다

Collection에서 객체 동등 비교 시, hashCode값을 먼저 비교하기 때문에 데이터 값이 동등하더라도 hashCode를 동일하게 맞추지 않으면 동등 비교가 제대로 이루어지지 않음 (hashCode = 객체를 식별할 수 있는 정수 값)

 

*Any.kt - hashCode() 

If two objects are equal according to the equals() method, then calling the hashCode method on each of the two objects must produce the same integer result.  

 

 

[Kotlin Data Class의 장점]

  1. 대부분의 데이터 클래스가 거의 동일한 형식으로 toString(), equals(), hashCode() 오버라이드를 찍어내듯 쓰고 있는데 이를 자동으로 컴파일러가 생성해주고
  2. equals만 override 하고 hashCode는 생략하는 등 잘못쓰이는 경우를 방지해주고
  3. 객체 copy 기능도 편리하게 사용 가능한(+ 데이터 일부만 변경해서 copy도 가능)...!!!

 

실제로 toString/equals/hashCode를 오버라이드 해준다는 것을 확인 해 보자

  1. toString() : 일반 클래스는 시 인스턴스의 Unique Id를 반환하고, Data 클래스는 데이터 값들을 보기 좋게 문자열로 반환한다
  2. equals() : 일반 클래스는 false를 반환하고 Data 클래스는 true를 반환한다
  3. hashCode() : 일반 클래스는 데이터가 같아도 인스턴스들의 hashCode가 다르지만 Data 클래스는 동일한 hashCode를 반환한다
data class StudentData constructor(val name: String, val grade: Int) //데이터 클래스
class StudentCommon(val name: String, val age: Int) //일반 클래스

fun printStudent() {
    val common01 = StudentCommon("TinaCommon", 1)
    val common02 = StudentCommon("TinaCommon", 1)

    val data01 = StudentData("TinaData", 2)
    val data02 = StudentData("TinaData", 2)

    //1)toString 확인
    println("common01 toString : ${common01.toString()}") //StudentCommon@2d98a335
    println("data01 toString : ${data02.toString()}")//StudentData(name=TinaData, grade=2)
    
    //2)equals 확인
    println("common equals : " + (common01 == common02)) //false 
    println("data equals : " + (data01 == data02)) //true

	//3)hashCode 확인
    println("common01  hashcode : ${common01.hashCode()}") //764977973
    println("common02 hashcode : ${common02.hashCode()}") //381259350
    println("data01 hashcode : ${data01.hashCode()}") //-1506839696
    println("data02 hashcode : ${data02.hashCode()}") //-1506839696
}

 

Copy 예제

        data class Student(val name: String, val age: Int)
        
        val studentTina = Student("Tina", 20)
        val studentBang = studentTina.copy(name = "Bang")
        println(studentTina) //Student(name=Tina, age=20)
        println(studentBang) //Student(name=Bang, age=20)

 

 

# by 키워드

  • 어떤 클래스를 상속하지 않고 확장/수정 하고 싶을 경우 사용하고 싶을 경우가 있는데, 그 방법으로는 보통 기존 클래스를 새로 확장/수정 하려는 클래스 내부에 필드로 가지고 있는 방법을 사용하곤 하는데...(아래와 같이)
interface Student {
    val name: String
    val age: Int

    fun sleep()
    fun eat()
    fun goToSchool()
}

//기존 클래스 - final
class CommonStudent(override val name: String, override val age: Int) : Student {
    override fun sleep() {
        println("[${name}, ${age}] I sleep at 9pm")
    }
    override fun eat() {
        println("[${name}, ${age}] I eat lunch in school cafeteria")
    }
    override fun goToSchool() {
        println("[${name}, ${age}] I go to school everyday")
    }
}


//새로운 클래스 
//CommonStudent를 상속하지 않고 HighSchoolStudent 클래스를 만들어 동작을 추가 변경 하고 싶을 경우
class HighSchoolStudent(val commonStudent: CommonStudent) : Student {   
    override val name = commonStudent.name 
    override val age = commonStudent.age

    //sleep 메소드만 동작을 다르게 구현하고 싶음
    override fun sleep() {
        println("[${commonStudent.name}, ${commonStudent.age}] I sleep at 12pm")
    }
    
    //위임
    override fun eat() {
        commonStudent.eat() 
    }
    
    //위임
    override fun goToSchool() {
        commonStudent.goToSchool() 
    }
}


fun main() {
    val highSchoolStudent = HighSchoolStudent(CommonStudent("Tina", 17))
    highSchoolStudent.sleep()
    highSchoolStudent.eat()
    highSchoolStudent.goToSchool()
    
    ******결과******
    [Tina, 17] I sleep at 12pm
    [Tina, 17] I eat lunch in school cafeteria
    [Tina, 17] I go to school everyday
}

문제점  - CommonStudent에서 수정/추가 하고 싶은 부분은 sleep() 메소드 하나임에도 불구하고 모든 함수/프로퍼티를 override 해야하는 불상사가 발생

 

대안 - 이 때!! 'by 키워드' 사용하면 컴파일러가 기존 클래스에 위임하는 메소드를 만들어 준다

동작 변경하고 싶으면 override로 재정의 해서 사용하면 된다

 

by 키워드를 사용한 HighSchollStudent 클래스

// Student 인터페이스에 대한 구현은 'commonStudent'에 맡기겠다.
// 어차피 Student 인터페이스에 대한 구현은 'commonStudent'에 돼있을 거고,
// HighSchoolStudent 클래스는 대부분의 메소드를 commonStudent 메소드를 그냥 호출 하기만 할 거니까
class HighSchoolStudent(val commonStudent: CommonStudent) : Student by commonStudent {
    //재정의 하고 싶은 메소드만 override
    override fun sleep() {
        println("[${commonStudent.name}, ${commonStudent.age}] I sleep at 12pm")
    }
}

1) 코드량 줄고 깔끔해 지며

2)'HighSchoolStudent' 생성 의도가 명확히 보여진다

 

 

출처 : Kotlin In Action - 에이콘 출판사

(위 도서를 학습하고 개인 학습용으로 정리한 내용입니다)