본문 바로가기

코틀린

[코틀린] Data 클래스

안드로이드 개발하는 kancho입니다.

이번 포스팅에서는 코틀린의 Data 클래스에 대해 알아보고자 합니다.

 

'코틀린 완벽 가이드' 책을 참고하였습니다.

 

 

 

Data 클래스

주로 데이터를 저장하기 위한 목적으로 사용되며

equals 메서드(동등성 비교), toString 메서드 등의 구현을 자동으로 생성해준다.

 

구조 분해 선언 (destructuring declaration)을 활용할 수도 있다.

이는 클래스의 프로퍼티를 간단한 한 가지 언어 구성 요소를 사용해 여러 변수에 나눠 넣을 수 있다.

 

아래 User 클래스를 보면

user1, user2, user3 인스턴스들의 동등성 비교를 하고 있다.

 

user1과 user2는 메모리에서 다른 위치(다른 객체)를 가리키고 있기에 동등하지 않다.

user1과 user3는 같은 위치를 가리키고 있어 동등하다고 판별된다.

 

만약 custom 한 동등성 비교가 필요하다면, equals 메서드나 hashCode() 메서드의 구현을 통해 비교한다.

 

class User(val firstName: String, val lastName: String)

fun main() {
    val user1 = User("kancho", "park")
    val user2 = User("kancho", "park")
    val user3 = user1

    println(user1 == user2) // false
    println(user1 == user3) // true
}

 

 

 

이번에는 User를 데이터 클래스로 바꿔보자.

 

이전과는 다르게 

user1, user2, user3 가 모두 동등하다고 판별되어 true가 결과값으로 나온다.

 

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

fun main() {
    val user1 = User("kancho", "park")
    val user2 = User("kancho", "park")
    val user3 = user1

    println(user1 == user2) // true
    println(user1 == user3) // true
}

 

 

클래스 앞에 data만 붙였는데 무엇이 다른 것일까?

앞에서 데이터 클래스는 equals, toString 등의 메소드를 자동으로 생성한다고 하였다.

 

 

디컴파일을 통해 User 클래스를 자바 코드로 살펴보면

생성자로 인해 정의된 firstName, lastName 프로퍼티의 값을 비교하는 메서드는 가지고 있지 않다.

 

public final class User {
   @NotNull
   private final String firstName;

   @NotNull
   private final String lastName;

   @NotNull
   public final String getFirstName() {
      return this.firstName;
   }

   @NotNull
   public final String getLastName() {
      return this.lastName;
   }
   
   ...
}

 

 

다음으로 User 데이터 클래스의 자바 코드를 살펴보면

copy(), toString(), hashCode(), equals() 메서드를 자동으로 생성하고 프로퍼티의 값을 서로 비교하고 있는 것을 알 수 있다.

 

단, 주 생성자의 프로퍼티만 equals, hashCode, toString 메서드 구현에 쓰이고 다른 프로퍼티들은 이런 함수에 영향을 미치지 못한다.

 

public final class User {
  
   /* ... */

   @NotNull
   public final String component1() {
      return this.firstName;
   }

   @NotNull
   public final String component2() {
      return this.lastName;
   }

   /*
   모든 데이터 클래스는 암시적으로 copy 함수를 제공한다.
   현재 instance를 복사하면서 프로퍼티를 변경할 수 있다.
   */
   @NotNull
   public final User copy(@NotNull String firstName, @NotNull String lastName) { ... }

   public static User copy$default(
       User var0, String var1, String var2, int var3, Object var4
   ) { ... }

   // 클래스의 instance를 문자열로 변환해준다.
   @NotNull
   public String toString() {
      return "User(firstName=" + this.firstName + ", lastName=" + this.lastName + ")";
   }

   // 주생성자에 선언된 프로퍼티(firstName, lastName)의 해시 코드에 의존해 계산한 해시코드 반환
   public int hashCode() {
      String var10000 = this.firstName;
      int var1 = (var10000 != null ? var10000.hashCode() : 0) * 31;
      String var10001 = this.lastName;
      return var1 + (var10001 != null ? var10001.hashCode() : 0);
   }

   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof User) {
            User var2 = (User)var1;
            if (Intrinsics.areEqual(this.firstName, var2.firstName) && Intrinsics.areEqual(this.lastName, var2.lastName)) {
               return true;
            }
         }
         return false;
      } else {
         return true;
      }
   }
}

 

 

equals 함수

프로퍼티 값의 비교는 equals() 메서드를 통해서 비교한다.

 

프로퍼티의 타입에 따라 깊은 동등성 비교가 이뤄지는지 알 수 있다.

 

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

data class Mailbox(val address: String, val user: User)

fun main() {
    val box1 = Mailbox("Unknown", User("kancho", "park"))
    val box2 = Mailbox("Unknown", User("kancho", "park"))
    println(box1 == box2)   // true
}

 

 

데이터 클래스인 User의 자바 코드를 보면

프로퍼티를 바탕으로 동등성을 구현한다.

 

public boolean equals(@Nullable Object var1) {
   if (this != var1) {
      if (var1 instanceof User) {
         User var2 = (User)var1;
         if (Intrinsics.areEqual(this.firstName, var2.firstName) &&
             Intrinsics.areEqual(this.lastName, var2.lastName)) {
                return true;
             }
          }
      return false;
   } else {
      return true;
   }
}

 

 

만약 user가 데이터 클래스가 아니라면

box1, box2의 User 인스턴스의 정체성이 다르기 때문에 false를 출력한다.

 

class User(
    val firstName: String,
    val lastName: String
)

data class Mailbox(val address: String, val user: User)

fun main() {
    val box1 = Mailbox("Unknown", User("kancho", "park"))
    val box2 = Mailbox("Unknown", User("kancho", "park"))
    println(box1 == box2)   // false
}

 

 

toString 함수

클래스 instance를 문자열로 변환한다.

 

fun main() {
    val user = User("kancho", "park")
    println(user)   // User(firstName=kancho, lastName=park)
}

 

 

copy 함수

모든 데이터 클래스는 암시적으로 copy 함수를 제공한다.

현재 instance를 복사하면서 프로퍼티를 변경할 수 있다.

 

보통 안전성을 위해 불변 데이터를 사용하는 것을 권장하는데,

copy 함수처럼 instance를 복사할 수 있는 것은 불변 데이터 구조를 더 쉽게 사용할 수 있도록 도와준다.

 

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

fun User.show() = println("$firstName $lastName")

fun main() {
    val user = User("kancho", "park")

    // default 값으로 원본 user 객체의 프로퍼티가 들어가 있다.
    user.copy().show()  // "kancho park"
    
    // copy 함수는 복사한 것이므로 기존 user 인스턴스에는 변화가 없다.
    user.show() // "kancho park"
    
    // 가독성을 위해 이름 붙은 인자 구문을 사용.
    user.copy(firstName = "Kotlin").show()  // "Kotlin park"
    user.copy(firstName = "Kotlin", lastName = "Java").show()   // "Kotlin Java"
}

 

 

코틀린 표준 라이브러리에는 2가지 범용 데이터 클래스가 있다.

 

대부분의 경우 범용 데이터 클래스보다 custom한 클래스를 사용하는 편이 더 낫다.

custom 클래스는 프로퍼티에 의미가 있는 이름을 부여할 수 있기 때문이다.

 

Pair

두 개의 값(한 쌍)을 저장할 수 있는 데이터 클래스

 

인스턴스의 first, second를 통해 값에 접근할 수 있다.

 

fun main() {
    val pair = Pair(1, "one")
    println(pair.first + 1) // 2
    println("${pair.second}!")   // one!
    println(pair)   // (1, one)
}

 

 

Triple

세 개의 값(triplet)을 저장할 수 있는 데이터 클래스

 

인스턴스의 first, second, third를 통해 값에 접근할 수 있다.

 

fun main() {
    val triple = Triple("one", 1, false)
    println("${triple.first}!") // one!
    println(triple.second - 1)  // 0
    println(!triple.third)  // true
    println(triple) // (one, 1, false)
}

 

 

구조 분해 선언

User 클래스와 미성년자인 user를 출력하는 코드이다.

여기서는 firstName, lastName, age를 모두 각각의 프로퍼티로 정의하였다.

 

data class User(
    val firstName: String,
    val lastName: String,
    val age: Int
)

fun main() {
    val user = User("kancho", "park", 30)

    val firstName = user.firstName
    val lastName = user.lastName
    val age = user.age

    if (age < 18) {
        println("$firstName $lastName is under-age")
    }
}

 

위 코드를 아래와 같이 data 클래스의 구조 분해 선언을 통해 좀 더 간결하게 표현할 수 있다.

 

변수 이름을 괄호로 감싸 여러 변수를 하나의 이름으로 정의하게 해주는 지역 변수 선언 구문이다.

지역 변수에서만 사용할 수 있다.

 

클래스 본문이나 파일 최상위에서는 사용할 수 없다.

 

val (firstName, lastName, age) = user

 

각 변수에 매핑되는 프로퍼티는 data 클래스 인스턴스의 프로퍼티에 해당한다.

매핑은 데이터 클래스의 생성자에 있는 각 프로퍼티의 위치에 따라 결정된다.

 

fun main() {
    /*
    firstName = "kancho"
    lastName = "park"
    */
    val (firstName, lastName, age) = User("kancho", "park", 30)
    println("$firstName $lastName")
    
    /*
    lastName = "kancho"
    firstName = "park"
    */
    val (lastName, firstName, age) = User("kancho", "park", 30)
    println("$firstName $lastName")
}

 

 

구조 분해 선언을 사용할 때 아래와 같이 사용할 수 있다.

 

fun main() {
    // 구조 분해 선언 전체는 타입이 없지만 각 변수에 타입을 표기할 수 있다.
    val (firstName, lastName: String, age1) = User("kancho", "park", 30)
    
    // 데이터 클래스의 생성자 프로퍼티 수보다 적은 수의 변수를 정의할 수 있다. 
    val (fName, lName) = User("kancho", "park", 30)
    println("$fName $lName")    // "kancho park"
    
    // 시작이나 중간에서 프로퍼티를 생략하려면 언더바(_) 로 대신할 수 있다. 
    val (_, _, age2) = User("kancho", "park", 30)
    println("$age2") // 30
    
    /*
    var 변수를 사용하면 변경할 수 있는 변수를 정의할 수 있다.
    하지만 val, var 를 혼용할 수는 없다.
    */
    var (name, familyName) = User("kancho", "park", 30)
    name = name.lowercase()
    familyName = familyName.lowercase()
    println("$name $familyName")    // kancho park
}

 

 

반복문에서도 구조 분해를 사용할 수 있다.

 

fun main() {
    val pairs = arrayOf(1 to "one", 2 to "two", 3 to "three")

    pairs.forEach { (number, name) ->
        println("$number : $name")
    }
}

 

 

람다 파라미터에 대해서도 구조 분해를 사용할 수 있다.

 

fun combine(
    user1: User,
    user2: User,
    folder: ((String, User) -> String)
): String =
    folder.invoke(folder("", user1), user2)

fun main() {
    val user1 = User("kancho", "park", 30)
    val user2 = User("kotlin", "java", 30)

    /*
    파라미터를 괄호로 둘러싸야 한다.
    
    result : "park java"
    */
    println(combine(user1, user2) { text, (_, lastName) ->
        "$text $lastName"
    })
}

 

 

'코틀린' 카테고리의 다른 글

[코틀린] 상속(Inheritance)  (0) 2022.11.06
[코틀린] Inline 클래스  (0) 2022.10.23
[코틀린] Enum 클래스  (0) 2022.10.21
[코틀린] 람다와 고차함수  (0) 2022.10.17
[코틀린] Scope function (영역 함수)  (0) 2022.10.10