성장기록지
코틀린 Data Class 자세히 알아보기 본문
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://devocean.sk.com/blog/techBoardDetail.do?ID=165658&boardType=techBlog
'코틀린' 카테고리의 다른 글
[Kotlin] 구조 분해와 Component함수, 분해 가능 개수 (0) | 2025.01.06 |
---|---|
코틀린 sealed class와 실제 활용 예시 (0) | 2025.01.04 |
코틀린 추상 클래스와 인터페이스의 차이 (1) | 2024.12.23 |