반응형
Notice
Recent Posts
Recent Comments
Link
«   2024/09   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30
Tags
more
Archives
Today
Total
관리 메뉴

안드로이드 개발자 노트

[이펙티브 코틀린] Item37. 데이터 집합 표현에 data 한정자를 사용하라 본문

Kotlin/이펙티브 코틀린

[이펙티브 코틀린] Item37. 데이터 집합 표현에 data 한정자를 사용하라

어리둥절범고래 2023. 12. 31. 12:11
반응형

때로는 데이터를 한꺼번에 전달해야 할 때가 있는데, 일반적으로 이러한 상황에서는 data 클래스를 사용합니다.

data class Player(
    val id: Int,
    val name: String,
    val points: Int
)

val player = Player(0, "Gecko", 9999)

data 한정자를 붙이면, 몇 가지 함수가 자동으로 생성됩니다.

 

  • toString: 클래스 이름과 기본 생성자 형태로 모든 프로퍼티와 값을 출력
  • equals: 기본 생성자의 프로퍼티가 같은지 확인
player == Player(0, "Gecko", 9999) // true
player == Player(0, "Ross", 9999) // false
  • hashCode: equals와 같은 결과를 낸다.
  • copy: immutable 데이터 클래스를 만들 때 편리하다. copy는 기본 생성자 프로퍼티가 같은 새로운 객체를 복제한다. 새로운 객체의 값은 이름 있는 아규먼트를 활용해서 변경 가능하다. 또한 copy는 객체를 얕은 복사한다.
val newObj = player.copy(name = "Thor")
print(newObj) // Player(id=0, name=Thor, points=9999)

// 'Player' 데이터 클래스의 'copy'는 이런 형태로 만들어진다.
fun copy(
    id: Int = this.id,
    name: String = this.name,
    points: Int = this.points
) = Player(id, name, points)
  • componentN (component1, component2 ...): 위치를 기반으로 객체를 해제할 수 있게 한다.
val id: Int = player.component1() // 0
val name: String = player.component2() // "Gecko"
val pts: Int = player.component3() // 9999

// Map.Entry 등의 원하는 형태로도 객체를 해제할 수 있다.
val trip = mapOf(
    "China" to "Tianjin",
    "Russia" to "Petersburg",
    "India" to "Rishikesh"
)
for ((country, city) in trip) {
    println("we loved $city in $country")
}

// 위치 순서를 혼동하면 안되니 주의하자.
data class FullName(
    val firstName: String,
    val secondName: String,
    val lastName: String
)
val elon = FullName("Elon", "Reeve", "Musk")
val (name, surname) = elon
print("It is $name $surname") // It is Elon Reeve

// 값을 하나만 갖는 데이터 클래스를 해제할 경우, 특히 람다 표현식과 함께 활용할 때 혼란을 줄 수 있다.
data class User(val name: String)
fun main() {
    val user = User("John")
    user.let { a -> print(a) } // User(user=John)
    user.let { (a) -> print(a) } // John
}

 

 

튜플 대신 데이터 클래스 사용하기

 

코틀린의 튜플은 Serializable을 기반으로 만들어지며, toString을 사용할 수 있는 제네릭 데이터 클래스입니다.

public data class Pair<out A, out B> (
    public val first: A,
    public val second: B
) : Serializable {

    public override fun toString(): String =
        "($first, $second)"
}

public data class Triple<out A, out B, out C>(
    public val first: A,
    public val second: B,
    public val third: C
) : Serializable {

    public override fun toString(): String =
        "($first, $second, $third)"
}

Pair와 Triple은 코틀린에 남아 있는 마지막 튜플입니다.

튜플은 과거에 (Int, String, String, Long)처럼 괄호와 타입 지정을 통해 원하는 형태의 튜플을 정의할 수 있었습니다.

튜플은 데이터 클래스와 같은 역할을 하지만, 튜플만 보고는 어떤 타입을 나타내는지 예측할 수 없어 점차 없어져 지역적인 목적으로 인해 Pair와 Triple만 남았습니다.

// 값에 간단하게 이름을 붙일 때
val (description, color) = when {
    degrees < 5 -> "cold" to Color.BLUE
    degrees < 23 -> "mild" to Color.YELLOW
    else -> "hot" to Color.RED
}

// 미리 알 수 없는 aggregate(집합)을 표현할 때
val (odd, even) = numbers.partition { it % 2 == 1 }
val map = mapOf(1 to "San Francisco", 2 to "Amsterdam")

 

 

데이터 클래스를 활용하면 함수를 더 명확하게 만들어 줍니다.

 

  • 함수의 리턴 타입이 더 명확해진다.
  • 리턴 타입이 더 짧아지며, 전달하기 쉬워진다.
  • 사용자가 데이터 클래스에 적혀 있는 것과 다른 이름을 활용해 변수를 해제하면, 경고가 출력된다.
  • 좁은 스코프를 갖고 싶다면, private을 붙여 주면 된다.

예를 들어, 전체 이름(fullname)을 이름(name)과 성(surname)으로 분할하는 코드가 있을 때, 이름과 성을 Pair<String, String>으로 나타냈다고 해보겠습니다.

fun String.parseName(): Pair<String, String>? {
    /* */
}

// 사용
val fullName = "Marcin Mosckala"
val (fisrtName, lastName) = fullName.parseName() ?: return

Pair<String, String>이 전체 이름을 나타낸다는 것을 인지하기 어려우며, 성(lastName)과 이름(firstName) 중에 어떤 것이 앞에 있을지 예측하기 어렵습니다.

data class FullName(
    val firstName: String,
    val lastName: String
)

fun String.parseName(): FullName? {
    /* */
}

// 사용
val fullName = "Marcin Mosckala"
val (fisrtName, lastName) = fullName.parseName() ?: return

데이터 클래스를 활용하면 이를 해결할 수 있습니다.

 

 


정리

 

  • 데이터 클래스를 활용하면, 튜플을 활용할 때보다 더 많은 장점이 있다.
  • 코틀린에서 클래스는 큰 비용없이 사용할 수 있는 좋은 도구이다.
  • 클래스를 활용하는 데 두려움을 갖지 말고, 적극 활용하라.
반응형