성장기록지

코틀린 Data Class 자세히 알아보기 본문

코틀린

코틀린 Data Class 자세히 알아보기

pengcon 2024. 12. 24. 13:04

Data Class란?

오직 데이터를 실어담을 수 있는 객체를 만들고 싶을 때 사용한다.

일반 클래스와 다르게, 다양한 메소드를 자동으로 생성해주는 클래스이다.

구성되는 메소드들은 아래와 같다.

  • toString()
  • hashCode()
  • copy()
  • equals()
  • toString()
  • componentsN()

또한 아래와 같은 특징들을 가지고 있다. 

  • 기본 생성자에 1개 이상의 파라미터가 있어야 함
  • 기본 생성자의 파라미터가 val 또는 var 로 선언해야 함
  • 다른 클래스를 상속받을 수 없음 ( sealed 클래스는 상속받을 수 있으며, 인터페이스는 구현할 수 있다.)
  • abstract, open, sealed, inner 등 키워드를 붙일 수 없음
  • 자동으로 생성한 메소드를 오버라이딩할 경우, 오버라이드 된 메소드 사용

아래에선 하나씩 어떻게 구현되는지 알아보자.


toString() 메소드

일반 class를 print한다면 아래와 같이 Any class의 toString()인 클래스이름 + 메모리 주소 를 반환하게 될 것이다. 

class Penguin(
    val name: String,
    val age: Int
)

fun main() {
    val penguin = Penguin("king", 1)
    print(penguin)
}

 

하지만 data를 담은 객체는 내부의 data 정보들을 보고싶을 경우가 많을것이다.

data class는 그런 니즈들을 충족시켜주기 위해 toString()이 구현되어있다.

 

data class Penguin(
    val name: String,
    val age: Int
)

fun main() {
    val penguin = Penguin("king", 1)
    print(penguin)
}

data만 붙여주었을 뿐인데 아래와 같이 프로퍼티의 값들이 출력되는 마법같은 일이 벌어진다. 


copy() 메소드

copy() 메소드 역시 사용할 수 있다. 특정 필드값만 바꿔서 복사하기에 간편하다.

fun main() {
    val penguin = Penguin("king", 1)
    val anotherPenguin = penguin.copy(name = "emperor")
    print(anotherPenguin)
}


hashCode() 메소드

자바에선 다음과 같은 규칙이 있다.

  • Class에 equals를 정의했다면, 반드시 hashCode도 재정의해야 한다.
  • 2개 객체의 equals가 동일하다면 반드시 hashCode도 동일한 값이 리턴되어야 한다.
  • 이런 조건을 지키지 않을 경우 HashMap, HashSet 등의 컬렉션 사용 시 문제가 생길 수 있다.

요컨데 2개의 객체의 파라미터가 같을경우에는, hashCode도 같은 값이 리턴되어야 한다는 것이다.

data class는 이를 완벽하게 구현하고 있다. 다음 예시를 살펴보자.

fun main() {
    val penguin = Penguin("king", 1)
    val penguin2 = Penguin("king", 1)
    println(penguin.hashCode())
    print(penguin2.hashCode())
}

파라미터가 같은 값을 출력하였을 때는 다음과 같이 같은 해시코드 값이 나온다.

hashCode()는 어떻게 구현되어있을까?

코틀린 코드를 디컴파일하여 확인해보았다.

public int hashCode() {
   int result = this.name.hashCode();
   result = result * 31 + Integer.hashCode(this.age);
   return result;
}

왜 31이라는 숫자를 사용할까? 그 이유는 다음과 같이 작성되어있다.

해석해보자면,

  • 31은 홀수이면서 소수(odd prime)다.
  • 소수를 선택하는 이유는 충돌(hash collision)을 줄이기 위해서다.
  • 충돌이란 서로 다른 두 객체가 같은 해시코드를 갖는 경우를 말합니다.
  • 홀수여야 하는 이유는 짝수를 곱했을 때, 이진수(bit) 연산에서 패턴이 단순화될 가능성이 있기 때문이다. 예를 들어, 곱셈 결과가 2로 나누어떨어지면(짝수) 해시값의 다양성이 줄어들 수 있다.
  • 만약 31이 짝수였다면, 곱셈 연산 중 오버플로우(overflow)가 발생했을 때 정보 손실이 일어날 가능성이 크다. 짝수 곱셈은 이진수 계산에서 왼쪽으로 비트 이동(shifting)과 동일하기 때문이다.

이러한 이유로 31이라는 숫자를 사용한다고 한다.

해시 및 해시충돌에 대해선 다음 글에서 자세히 다루겠다.


equals() 메소드

hashcode()에서 언급된 두 객체를 비교하는 연산도 살펴보자.

println(peopleA == peopleB)

// true (두 객체의 프로퍼티가 완전히 같음)

'==' 연산자 하나로, 두 객체가 동일한 값을 담고있는지 쉽게 검사할 수 있다.

또한, === 연산도 가능하다. 메모리 상 다른 객체이므로 false 를 출력한다.

println(peopleA === peopleB)

// false (두 객체의 메모리 주소가 다름)

 Java vs Kotlin 동등성 비교

  • 갖고 있는 값이 동일한지 검사
    • Java : equals()
    • Kotlin : ==
  • 메모리상 같은 객체인지 검사
    • Java : ==
    • Kotlin : ===

 

equals()는 어떻게 구현되어 있을까?

내가 만든 Penguin 객체로 한번 디컴파일 결과를 확인해보았다.

public boolean equals(@Nullable Object other) {
   if (this == other) {
      return true;
   } else if (!(other instanceof Penguin)) {
      return false;
   } else {
      Penguin var2 = (Penguin)other;
      if (!Intrinsics.areEqual(this.name, var2.name)) {
         return false;
      } else {
         return this.age == var2.age;
      }
   }
}
  • 우선 두 객체의 참조가 같으면 true를 반환한다.
  • Penguin 클래스의 인스턴스가 아니라면 false이다.
  • 이외에는 각 프로퍼티가 모두 같은지 value equality를 확인하여 true나 false를 반환한다.

componentN() 메소드

Data Class 는 기본적으로 componentN() 메소드가 생성이 되기 때문에,

각 프로퍼티에 번호가 붙어 구조 분해(Destructuring Declarations)가 가능한 형태가 된다.

fun main() {
    val penguin = Penguin("king", 1)
    val (name,age) = penguin
    println("$name,$age")
}

역시나 내부적으로 어떻게 동작하는지 알아보겠다. 

Penguin class에는 프로퍼티가 두개이다. 

name 다음에 age를 구성하였기 때문에

name은 compoonent1, age는 compoonenet2로 대응되게 되었다.

따라서 val (name, age) = penguin으로만 해줘도, 내부적으로 구조분해를 하여 할당해준다!

 


참고 링크 

https://velog.io/@haero_kim/Kotlin-%EA%B0%90%EB%8F%99-%EC%8B%A4%ED%99%94-Data-Class-%EC%95%8C%EC%95%84%EB%B3%B4%EA%B8%B0

https://devocean.sk.com/blog/techBoardDetail.do?ID=165658&boardType=techBlog

https://thdev.tech/kotlin/2020/09/15/kotlin_effective_02/