Kotlin - Kotlin Data Class

Park Suyong·2023년 6월 10일
0

Kotlin

목록 보기
6/7

1. Kotlin Data Class 란 ?

안드로이드 앱을 개발하다 보면 사용자에게 보여지는 뷰의 상태, API Response, Clean Architecture 기반 각 레이어의 데이터 모델 등 데이터 홀더가 필요한 경우가 굉장히 잦다. 이러한 경우에 사용하는 클래스가 data class 이다.

data class 는 데이터 홀더로써의 역할을 충실하게 수행하며, 아래 5가지 메서드를 기본적으로 제공함으로써 보일러 플레이트 코드를 줄일 수 있다.

equals()
  • 하나의 data class 에 대해 생성된 여러 개의 객체를 ===== 연산자를 사용하여 비교하는 것이 가능하다.*
  • == 연산자는 비교할 두 객체가 동일한 값을 갖고 있는지 검사한다. 반면 === 연산자의 경우, 메모리 주소값이 같은 지를 검사한다. 즉, == 는 동등성 비교로서 활용된다.
toString()
  • data class 가 없다면, toString 메서드를 오버라이드하여 필요한 정보를 출력해낼 수 있도록 직접 작성했어야 했다. 하지만 toString 메서드를 data class 에서 제공해 주면서, data class 의 생성자를 출력해 준다.
hashCode()
  • 객체의 해시 코드값을 반환하는데, 두 객체를 equals() 메서드를 통해 비교하였을 때 동일한 경우, hashCode() 메서드도 동일한 값을 반환해야 한다.
  • 다만, data class 의 생성자 밖에서 선언한 변수는 equals() 메서드 혹은 hashCode() 메서드에서 포함하지 않는다. 따라서 포함시켜 주기 위해서는 메서드를 오버라이드 하여 구성해 주어야 한다.
copy()
  • copy 메서드는 객체를 복사하여 새로운 객체로 생성해 준다. copy 메서드에 아무런 인자도 넘기지 않으면 기존 객체를 복사한 똑같은 데이터를 가진 객체를 생성하며, 인자를 추가하는 경우 해당 인자를 오버라이드한 새로운 데이터 클래스 객체를 생성해 준다.

  • data class Product(
      val id: Long,
      val name: String,
      val price: Long
    )
    
    val product = Product(30, "상품 이름", 3000)
    val newProduct = product.copy(id = 31)
componentN()
  • componentN() 메서드에서 N 은 데이터 클래스의 N번 째 프로퍼티를 반환한다. 이로써 구조를 분해하며 각 프로퍼티를 따로 초기화할 수 있다. 이를 구조 분해라 한다.

data class 의 생성자

생성자 값은 val 로 선언하는 것이 좋을까, 아니면 var 로 선언하는 것이 좋을까?

결론은 상관 없다. 다만, val 을 통해 값이 immutable 하게 유지되는 것을 권장한다. 그렇다면 Android 에서 뷰의 상태가 변경되는 경우 (가령, isSelected / isOpen 등) 해당 상태는 어떻게 보관하는 것이 좋을까?

copy 메서드를 통해 data class 객체를 새롭게 생성하고 값을 새로 주입해 주는 것이 좋다. 다음과 같은 이유가 있다.

  • HashMap 등 컨테이너에 데이터를 담는 경우, 불변성은 필수적이다.
  • 불변 객체가 훨씬 더 추론하기 쉽다.
  • 멀티스레드 프로그램의 경우, 객체의 값을 변경시켰을 때 스레드 간의 동기화가 필요하다.
  • 객체를 메모리에서 변경시키는 것보다 copy 시켜 객체 자체를 변경시키는 것이 낫다.

kotlin 에서는 = 연산자를 통한 값 주입은 얕은 복사이다. 아래의 예시를 보자.

data class User(
  val id: Long,
  val name: String,
  val job: String,
  val friendNames: MutableList<String>
)

fun main() {
  val user1 = User(0L, "박수용", "개발자", mutableListOf("A", "B")) 
  val user2 = user1
  user1.friendNames.add("C")
  print("user1=$user1\n")
  print("user2=$user2")
}

// 실행 결과는 아래와 같다.
// user1=User(id=0, name=박수용, job=개발자, friendNames=[A, B, C])
// user2=User(id=0, name=박수용, job=개발자, friendNames=[A, B, C])

분명 코드 상으로 user1 의 값을 변경시켰음에도 불구하고 user2 도 변경되었다. 얕은 복사란, 객체의 메모리 주소값을 복사하는 것이기 때문에 복사하여 주입한 객체가 변경되면 이전의 객체도 변경되게 된다.

그렇다면 data class 의 copy() 메서드는 어떨까?

data class User(
  val id: Long,
  val name: String,
  val job: String,
  val friendNames: MutableList<String>,
  var isLogin: Boolean = true
)

fun main() {
  val user1 = User(0L, "박수용", "개발자", mutableListOf("A", "B")) 
  val user2 = user1.copy()
  user1.friendNames.add("C")
  user1.isLogin = false
  print("user1=$user1\n")
  print("user2=$user2")
}

// 실행 결과는 아래와 같다.
// user1=User(id=0, name=박수용, job=개발자, friendNames=[A, B, C], isLogin=false)
// user2=User(id=0, name=박수용, job=개발자, friendNames=[A, B, C], isLogin=true)

동일하다. copy() 메서드 또한 얕은 복사이다. (원시 타입의 경우에는, 깊은 복사가 가능하지만 그 외는 안된다..)

그렇다면 깊은 복사가 되려면 어떻게 해야 할까?

copy 메서드를 data class 에서 구현하여 사용하는 방법이 있다. 참고로, copy 메서드는 Any 타입의 메서드가 아니기 때문에 override 하여 사용하는 것이 아닌 직접 구현하여 사용하면 된다.

data class User(
  val id: Long,
  val name: String,
  val job: String,
  val friendNames: MutableList<String>,
  var isLogin: Boolean = true
) {
    fun copy(): User {
        return User(
            id = id,
            name = name,
            job = job,
            friendNames = friendNames.toMutableList(),
            isLogin = isLogin
        )
    }
}

fun main() {
  val user1 = User(0L, "박수용", "개발자", mutableListOf("A", "B")) 
  val user2 = user1.copy()
  user1.friendNames.add("C")
  user1.isLogin = false
  print("user1=$user1\n")
  print("user2=$user2")
}

// 실행 결과는 아래와 같다.
// user1=User(id=0, name=박수용, job=개발자, friendNames=[A, B, C], isLogin=false)
// user2=User(id=0, name=박수용, job=개발자, friendNames=[A, B], isLogin=true)

References

profile
Android Developer

0개의 댓글