Kotlin Data Class 분석

김병수·2024년 3월 1일
0
post-thumbnail

이번 포스팅에서는 Kotlin data class의 특징에 대해 알아볼 예정입니다.
자주 사용하고 있었지만, 특징을 제대로 분석해 본 적은 없었네요😅

data class 특징

toString

class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)

fun main() {
    val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    println(gameData)
    /*
    Output
    GameData@4e0e2f2a
    */
}

class를 사용한다면 toString을 직접 구현해야 합니다.
toString을 구현하지 않으면 위와 같이 className@hashCode 형태로 출력됩니다.

data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)

fun main() {
    val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    println(gameData)
    
    /*
    Output
    GameData(gameId=1, year=2024, month=3, day=1)
    */
}

data class는 모든 프로퍼티 값을 보여주는 형태로 toString을 지원해줍니다.

equals

equals는 동일한 클래스로부터 생성된 객체에 대하여, 프로퍼티 값을 바탕으로 서로 다른 객체를 같은 객체라고 판단할지 여부를 결정하는 함수입니다.
class의 경우, equals를 따로 정의하지 않은 경우에 두 객체의 메모리 주소가 같은지를 비교합니다. (Java Object 클래스의 equals와 동일)

class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)

fun main() {
    val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    println(gameData)
    println(gameData2)
    println(gameData.equals(gameData2)) // gameData == gameData2 와 동일하게 작동함
    /*
    Output
    GameData@4e0e2f2a
    GameData@73d16e93
    false
    */
}

따라서 직접 equals 함수를 구현해야 프로퍼티를 기준으로 서로 다른 객체를 동일한 객체로 판단할 수 있습니다.

class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int) {
    override fun equals(other: Any?): Boolean {
        return if (other == null || other !is GameData) {
            false
        } else {
            gameId == other.gameId && year == other.year && month == other.month && day == other.day
        }
    }
}

fun main() {
    val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    println(gameData)
    println(gameData2)
    println(gameData.equals(gameData2)) // gameData == gameData2 와 동일하게 작동함
    /*
    Output
    GameData@4e0e2f2a
    GameData@73d16e93
    true
    */
}

하지만 data class는 equals를 지원하기 때문에, 별도의 구현 없이도 프로퍼티가 같은 객체를 서로 같은 객체로 판단할 수 있습니다.

data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)

fun main() {
    val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    println(gameData)
    println(gameData2)
    println(gameData == gameData2)
    /*
    Output
    GameData(gameId=1, year=2024, month=3, day=1)
	GameData(gameId=1, year=2024, month=3, day=1)
	true
    */
}

hashCode

hashCode는 객체의 메모리 주소를 기반으로 생성되는 값을 의미합니다.
따라서 동일한 객체에 대해서는 항상 동일한 해시 코드가 생성되지만, 서로 다른 객체는 서로 다른 해시 코드가 생성됩니다.
이러한 특징 때문에 hashCode는 아래와 같은 상황에서 주로 사용됩니다.

  1. 해시 기반 컬렉션
    HashMap, HashSet 과 같은 자료 구조에서 객체를 저장 및 검색할 때 사용
  2. 객체 동등성 비교
    객체의 내용이 동일한지를 비교할 때 사용
    해시 코드 값이 동일하면 서로 동일한 객체라고 판단할 수 있음
  3. 객체 식별
    해시 코드만으로는 서로 동일한 객체인지 완전히 비교할 수는 없음 (해시 코드가 같더라도 서로 다른 메모리 주소에 적재된 객체라면 서로 다른 객체이기 때문)
    하지만 일반적으로 해시 코드는 객체를 고유하기 식별하는데 사용됨

따라서 hashCode 함수를 적절하게 오버라이딩해서 사용해야 하며,
data class는 이 hashCode 함수를 지원합니다.

data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)

fun main() {
    val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    val gameData2 = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    println(gameData.hashCode())
    println(gameData2.hashCode())
    /*
    Output
    1974949
    1974949
    */
}

copy

data class는 임의의 객체에 대하여, 생성자에 정의된 프로퍼티 값을 변경하여 복사하는 기능을 지원합니다.

data class GameData(val gameId: Long, val year: Int, val month: Int, val day: Int)

fun main() {
    val gameData = GameData(gameId = 1, year = 2024, month = 3, day = 1)
    val gameData2 = gameData.copy(gameId=2)
    println(gameData)
    println(gameData2)
    /*
    Output
    GameData(gameId=1, year=2024, month=3, day=1)
    GameData(gameId=2, year=2024, month=3, day=1)
    */
}

하지만 이는 얕은 복사이기 때문에, 프로퍼티로 객체를 가지고 있는 경우에는 깊은 복사를 따로 구현해야 합니다.

실제로 아래에서 gameDatacopygameData2playerList를 변경했음에도 불구하고, gameDataplayerList도 동일하게 변경 됨을 확인할 수 있습니다.

data class PlayerData(val name: String, val number: Int)

data class GameData(
        val gameId: Long,
        val year: Int,
        val month: Int,
        val day: Int,
        val playerList: ArrayList<PlayerData>
)

fun main() {
    val gameData =
            GameData(
                    gameId = 1,
                    year = 2024,
                    month = 3,
                    day = 1,
                    playerList = arrayListOf(PlayerData("기상호", 6), PlayerData("성준수", 31))
            )
    val gameData2 = gameData.copy(gameId = 2)
    println(gameData)
    println(gameData2)

    gameData2.playerList.clear()
    println(gameData)
    println(gameData2)
    
    /*
    Output
    GameData(gameId=1, year=2024, month=3, day=1, playerList=[PlayerData(name=기상호, number=6), PlayerData(name=성준수, number=31)])
    GameData(gameId=2, year=2024, month=3, day=1, playerList=[PlayerData(name=기상호, number=6), PlayerData(name=성준수, number=31)])
    GameData(gameId=1, year=2024, month=3, day=1, playerList=[])
    GameData(gameId=2, year=2024, month=3, day=1, playerList=[])
    */
}

Destructuring declarations

Kotlin 공식 문서에 따르면, data class는 Destructuring declarations를 지원한다고 합니다.
내부적으로는 componentN 함수가 생성되어, 생성자에 포함된 프로퍼티 순서대로 component1, component2 ... 와 같이 사용할 수 있습니다.

data class PlayerData(val name: String, val number: Int)

data class GameData(
        val gameId: Long,
        val year: Int,
        val month: Int,
        val day: Int,
        val playerList: ArrayList<PlayerData>
)

fun main() {
    val gameData =
            GameData(
                    gameId = 1,
                    year = 2024,
                    month = 3,
                    day = 1,
                    playerList = arrayListOf(PlayerData("기상호", 6), PlayerData("성준수", 31))
            )

    val (gameId, year, month, day) = gameData
    val (_, _, _, _, playerList) = gameData
    println("gameId=$gameId, year=$year, month=$month, day=$day, playerList=$playerList")
    /*
    Output
    gameId=1, year=2024, month=3, day=1, playerList=[PlayerData(name=기상호, number=6), PlayerData(name=성준수, number=31)]
    */
}
profile
주니어 개발자

0개의 댓글