Android

안드로이드 [Kotlin] - Data Class의 copy()는 얕은 복사다.

🤖 Play with Android 🤖 2022. 12. 10. 22:39
728x90


Data Class란?

데이터 보관이 목적인 클래스로, toString(), equals(), hasCode(), copy()가 기본적으로 오버라이딩 및 추가되어 있어 데이터 출력, 비교, 복사가 편하다.

 

 

 

얕은 복사? 깊은 복사?

  • 얕은 복사란 "주소 값"을 복사하다는 의미이다.
  • 깊은 복사란 "실제 값"을 복사한다는 의미이다.

코틀린에서는 기본적으로 '='을 사용해서 다른 인스턴스를 넣어주면 얕은 복사가 수행된다. 얕은 복사가 수행되게 되면 같은 주소 값을 참조하기 때문에 복사된 객체가 변경되면 원본 객체의 값도 변경된다.

 

data class Car(private val name: String = "", var position: Int = 0) {
    fun move() {
        position++
    }
}

class DeepCopyTest {

    @Test
    fun shallowCopyTest() {
        val car = Car("car")
        val copyCar = car
        copyCar.move()
    }
}

 

car 객체를 생성하고 '='을 통해 copyCar에 car를 넣어준다.

 

copyCar의 position을 변경하였는데 car의 position 역시 변경된 것을 확인할 수 있다.

 

 

 

Data Class의 copy()

그렇다면 본론으로 넘어가서 Data Class의 copy()를 살펴보자.

위와 똑같은 코드에서 '=' 대신 copy() 메서드를 사용해보면

 

data class Car(private val name: String = "", var position: Int = 0) {
    fun move() {
        position++
    }
}

class DeepCopyTest {

    @Test
    fun deepCopyTest() {
        val car = Car("car")
        val copyCar = car.copy()
        copyCar.move()
    }
}

 

car와 copyCar의 주소 값이 다르고 원본 car의 position 역시 변하지 않았다.

 

 

 

🤷‍♂️  의문점

 

그렇다면 Data Class의 copy() 메서드는 깊은 복사라고 할 수 있을까?

 

 

결론부터 말하자면 "Data Class의 copy()는 얕은 복사"이다.

 

아래의 예시를 보자

 

data class Client(val name: String, val pastAddresses: ArrayList<String>)

fun main() {
    val client = Client("Alice", arrayListOf("foo", "bar"))
    println(client) // 복사 이전 원본 출력
    
    // copy 메서드 수행
    val copyClient = client.copy()
    copyClient.pastAddresses.add("Blah")
    copyClient.pastAddresses.remove("foo")
    println(client) // 복사 이후 원본 출력
}

 

위 코드를 실행해보면

 

다음과 같은 결과를 확인할 수 있다.

 

분명 Car의 예시에서는 Data Class의 copy()를 사용하고 나서 원본은 변함이 없었는데 이번 예시에서는 원본의 pastAddresses가 변경되었다. 만약 copy()가 깊은 복사를 수행한다면 원본의 pastAddresses가 그대로 [foo, bar]가 나와야 할 텐데 말이다.

 

여기서 핵심은 "복사하는 요소의 타입이 기본(Primitive) 타입인지 아닌지"이다.

코틀린이 제공하는 기본(Primitive) 데이터 타입 :  Byte, Short, Int, Long, Float, Double, Char, String
 
두 코드를 보자

Data Class 안에 기본(Primitive) 타입만 있는 경우

data class Client(val name: String, var age: Int)

fun main() {
    val client = Client("Alice", 20)
    println(client) // 원본 객체 출력
	
    // copy() 메서드 수행
    val copyClient = client.copy()
    copyClient.age = 22 // 복사 객체 age 변경 
    println(client) // 원본 객체 출력
    println(client.hashCode()) // 원본 객체 해시코드
    println(copyClient.hashCode()) // 복사 객체 해시코드
}

 

  • 우리가 흔히 알던 깊은 복사처럼 copyClient.age로 복사본의 나이를 변경하였을 때 원본은 변경되지 않았다.
  • 또한 원본 객체의 해시 코드와 복사 객체의 해시 코드가 역시 다른 것을 확인할 수 있다.

 

 

Data Class 안에 기본(Primitive) 타입 말고 다른 타입도 존재하는 경우

data class Client(val name: String, val pastAddresses: ArrayList<String>)

fun main() {
    val client = Client("Alice", arrayListOf("foo", "bar"))
    println(client)

    val copyClient = client.copy()
    copyClient.pastAddresses.add("Blah")
    copyClient.pastAddresses.remove("foo")
    println(client)
    println(client.hashCode())
    println(copyClient.hashCode())
}

 

  • 원본 Client의 pastAddresses 값이 변경되었고, 원본의 해시 코드와 복사본의 해시 코드 역시 동일하다.
  • 이렇듯 Data Class 안에 기본(Primitive) 타입 말고 다른 타입도 존재하는 경우, 얕은 복사가 수행된 것을 확인할 수 있다.

 

 

결론

 

 

코틀린의 Data Class의 copy는 기본(Primitive) 타입 이외의 변수는 얕은 복사가 되기 때문에 얕은 복사로 분류한다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Ref 

https://lengrand.fr/kotlin-data-classes-shallow-copies-and-immutability/

https://stackoverflow.com/questions/47359496/kotlin-data-class-copy-method-not-deep-copying-all-members

https://stackoverflow.com/questions/57840759/kotlins-copy-in-data-class-does-shallow-copy

https://seosh817.tistory.com/163