Kotlin - Data Class에 대해 (Class와의 차이점, 샘플 코드, 생성 메서드 직접 구현해보기)

반응형

간단히 알아보는 Data Class란?

데이터 클래스는 코틀린 언어에서 제공하는 클래스 유형으로,

주로 데이터를 보유하고 전달하는 것을 목적으로 만들어진 클래스이다.

전반적으로 데이터 클래스는 데이터 모델 및 DTO를 만들고 관리하기 위해 사용된다.

 

코틀린에서 data class를 선언하여 기본 생성자를 초기화 할 때,

기본 생성자 매개변수를 기반으로 하는 여러 유용한 메서드들을 컴파일러가 자동으로 생성한다.

자동으로 생성되는 메서드를 이용하여 프로그래머는 보다 편하고 쉽게 코드를 구성할 수 있도록 도와준다.

 

자동으로 생성되는 메서드는 다음과 같다.

  • equals() : 객체의 내용 비교
  • hashCode() : 해시 기반 자료구조에서 사용
  • toString() : 객체를 문자열로 표현
  • copy() : 객체의 일부 속성만 변경한 새 객체 생성
  • componentN() : 구조 분해 할당

Q. 일반 Class와의 차이는?

  • 일반 classdata class와 달리 위의 기능들이 자동으로 생성되지 않기 때문에 직접 구현해야 한다.

 

Data Class는 왜 쓰는걸까?

코드의 간결성 제공 및 반복적인 코드 감소

데이터 객체를 정의하고 조작해야 하는 상황에서

데이터가 정확하게 일치하는지 등의 확인 작업을 하다보면 코드가 다소 복잡해질 수 있다.

이럴 때, 데이터 클래스의 생성된 메서드들을 이용하면

명시적으로 작성할 필요 없이 데이터 모델을 간결하게 정의하는 방법을 제공한다.

이는 코드를 더 읽기 쉽고 이해하기 쉽게 코드를 만들어주기 때문에 유지보수성에서도 매우 유용하다.

 

안정적인 데이터 제공

모든 기본 생성자 매개 변수는 val 또는 var로 선언된다.

데이터 클래스는 불변성(Immutability)을 제공하도록 설계되었다.

그래서 기본적으로 data class의 속성은 val(읽기 전용)으로 선언된다.

불변성을 장려하는 클래스는 더 예측 가능하고 안정적인 코드를 만들어 오류 발생률을 줄일 수 있다.

 

Q. var로 선언하면 불변성이 제공되는 것이 아니지 않을까?

물론 명시적으로 속성을 var로 선언할 수는 있으나,

데이터 클래스에서 생성된 매서드가 생성자 매개변수의 초기 값을 기반으로 하기 때문에 

가변성을 허용하게 되며 이는 곧 다른 인스턴스에 영향을 미칠 수 있으며

데이터 클래스를 기반으로 하는 해시 테이블, 데이터 구조에서 오류를 초래할 수 있다.

이는 곧 데이터 무결성 문제로 이어질 수가 있어서 유의해야 한다.

 

예시로 알아보는 데이터 클래스 사용법

데이터 클래스 선언

클래스 앞에 data 키워드를 선언한다.

기본 생성자로 생성될 매개 변수를 정의한다.

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

 

데이터 추가

생성자에 값을 넣어준다.

다른 프로그래밍 언어의 생성자 생성 방식과 동일하다.

fun main() {
	// User 데이터 클래스에 값 추가
    val user1 = User("Eunbyeol", 35)
    val user2 = User("tester", 1)
}
  • 위와 같이 코드를 작성하게 되면 각각 user1, user2에 대한 유용한 메서드가 자동으로 생성된다.

 

자동 생성 메서드 활용

  • 주석을 보고 어떻게 작동하는지 이해해보자.
fun main() {
	// User 데이터 클래스에 값 추가
    val user1 = User("Eunbyeol", 35)
    val user2 = User("Eunbyeol", 35)
    
    // equals() 메서드 자동 생성
    println(user1 == user2) // true

    // hashCode() 메서드 자동 생성
    println(user1.hashCode() == user2.hashCode()) // true

    // toString() 메서드 자동 생성
    println(user1) // User(name=Eunbyeol, age=35)

    // copy() 메서드 자동 생성
    val user3 = user1.copy(age = 999)
    println(user3) // User(name=Eunbyeol, age=999)

    // componentN() 메서드 자동 생성
    val (name, age) = user1
    println("Name: $name, Age: $age") // Name: Eunbyeol, Age: 35
}

 

 

일반 클래스로 위와 같은 메서드를 사용하면 어떤 결과가 나올까?

데이터 클래스는 기본 생성자의 매개 변수를 이용하여 자동으로 유용한 메서드를 제공한다고 하였다.

그렇다면 일반 클래스는 어떻길래 데이터 클래스를 사용하는게 편하다고 하는 것일까?

직접 결과를 확인해보자.

 

일반 클래스 선언

class RegularUser(
	val name: String,
    val age: Int
) { }

 

메서드 작동 확인

  • 데이터 클래스에서 확인했던 메서드의 결과값과 전혀 다른 값이 출력되는 것을 확인할 수 있다.
fun main() {
    // User 일반 클래스에 값 추가
    val user4 = RegularUser("Eunbyeol", 35)
    val user5 = RegularUser("Eunbyeol", 35)

    // equals() 메서드
    println(user4 == user5) // false

    // hashCode() 메서드
    println(user4.hashCode() == user5.hashCode()) // false

    // toString() 메서드
    println(user4) // org.example.RegularUser@79fc0f2f
}

 

자동 생성 메서드를 직접 구현을 해야 한다면?

일반 클래스 equals(), hashCode(), toString() 메서드 직접 구현 코드

  • 생성자의 매개변수를 확인하여 오버라이딩한다.
class RegularUser(val name: String, val age: Int) {

    // equals() 메서드 직접 구현
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is RegularUser) return false
        return name == other.name && age == other.age
    }

    // hashCode() 메서드 직접 구현
    override fun hashCode(): Int {
        return name.hashCode() * 31 + age
    }

    // toString() 메서드 직접 구현
    override fun toString(): String {
        return "RegularUser(name=$name, age=$age)"
    }
}

 

구현 확인

  • 데이터 클래스에서 확인했던 값과 동일한 것을 확인할 수 있다.
fun main() {
    // User 일반 클래스에 값 추가
    val user4 = RegularUser("Eunbyeol", 35)
    val user5 = RegularUser("Eunbyeol", 35)

    // equals() 메서드
    println(user4 == user5) // true

    // hashCode() 메서드
    println(user4.hashCode() == user5.hashCode()) // true

    // toString() 메서드
    println(user4) // RegularUser(name=Eunbyeol, age=35)
}

 

추가 정보. 해시 코드를 구할 때 31을 사용하는 이유는?

간단히 요약하면 31이라는 숫자는 소수(Prime Number)로,

해시 충돌(다른 객체가 같은 해시 값을 가지는 현상)의 가능성을 줄이고

컴파일러가 최적화를 수행할 때 곱셈 연산으로 최적화 될 수 있다.

(비트 시프트와 뺄셈으로 변환할 수 있어 성능 이점이 있다.)

문자열과 같은 일반적인 데이터에 대해 좋은 분포를 제공한다.

실제 Java의 String. Integer 등 많은 핵심 클래스들도 31이라는 숫자를 사용한다.

반응형