kotiln 객체 복사 방식 (얕은복사/copy()/깊은복사)
객체를 선언하고 복사하는 과정에서 의도하지 않게 데이터가 변경되어서 오류가 발생할 수 있습니다. kotiln의 복사 방식을 살펴보고 실수하지 않게 잘 기억해둡시다.
얕은 복사
흔히 우리가 사용하는게 얕은 복사입니다.
주로 = 을 이용하며
객체의 주소값을 복사하기 때문에
복사본을 교체하면 원본도 함께 변경되게 됩니다.
data class Data(
var data: String,
val dataset: MutableList<String> = mutableListOf()
)
fun main() {
val data = Data("basic Data")
val dataShallowCopy = data
dataShallowCopy.data = "changed Data"
println("basic data = $data")
println("dataShallowCopy data = $dataShallowCopy")
}
basic data = Data(data=changed Data, dataset=[added data])
dataShallowCopy data = Data(data=changed Data, dataset=[added data])
data2는 data와 같은 Data의 주소값을 가르키고 있기 때문에
data2의 값을 변경하게되면 data의 값도 변경되게 됩니다.
그럼 data class에서 지원하는 copy() 메소드는 어떻게 작동할까요?
copy 메소드는 언뜻보면 깊은 복사처럼 작동하는 것 같습니다.
copy 메소드는 새로운 객체를 만들어서 주소값을 저장합니다.
하지만 기본형 타입은 그대로 값이 복사되지만
dataSet 처럼 참조형 타입이 존재한다면 해당 값이아닌 주소값을 복사하므로
완전한 깊은 복사는 지원하지 않습니다.
따라서 dataset에 데이터를 추가할 경우 모든 복사본의 dataset 값에 데이터가 추가됩니다.
data class Data(
var data: String,
val dataset: MutableList<String> = mutableListOf()
)
fun main() {
val data = Data("basic Data")
val dataShallowCopy = data
val dayaCopy = data.copy()
dataShallowCopy.data = "changed Data"
dayaCopy.data = "copy() data"
dayaCopy.dataset.add("added data")
println("basic data = $data")
println("dataShallowCopy data = $dataShallowCopy")
println("dayaCopy data = $dayaCopy")
}
basic data = Data(data=changed Data, dataset=[added data])
dataShallowCopy data = Data(data=changed Data, dataset=[added data])
dayaCopy data = Data(data=copy() data, dataset=[added data])
dataCopy의 data는 새로운 객체의 값이므로 변경하더라도 원본값에 영향을 미치지 않습니다.
하지만 dataset은 같은 주소값을 가르키고 있기 떄문에 data를 추가하면 원본 dataset에도 영향을 미치게 됩니다.
원본과 복사본을 완전히 독립적인 객체로 구분하기 위해선 깊은 복사를 사용해야합니다.
깊은 복사
깊은 복사를 위해서는 수동으로 객체를 복사하거나 별도의 유틸리티를 작성해야 합니다.
깊은 복사는 다양한 방식으로 구현할 수 있는데 저는 간단히 dataset도 새로운 객체를 만들어서 저장하는 방식으로 구현했습니다.
data class Data(
var data: String,
val dataset: MutableList<String> = mutableListOf()
) {
fun deepCopy(): Data {
return Data(
data = this.data,
dataset = this.dataset.toMutableList()
)
}
}
fun main() {
val data = Data("basic Data")
val dataShallowCopy = data
val dayaCopy = data.copy()
val dataDeepCopy = data.deepCopy()
dataShallowCopy.data = "changed Data"
dayaCopy.data = "copy() data"
dayaCopy.dataset.add("added data")
dataDeepCopy.data = "deep copy data"
dataDeepCopy.dataset.add("deep copy added data")
println("basic data = $data")
println("dataShallowCopy data = $dataShallowCopy")
println("dayaCopy data = $dayaCopy")
println("DeepCopy data = $dataDeepCopy")
}
basic data = Data(data=changed Data, dataset=[added data])
dataShallowCopy data = Data(data=changed Data, dataset=[added data])
dayaCopy data = Data(data=copy() data, dataset=[added data])
DeepCopy data = Data(data=deep copy data, dataset=[deep copy added data])
결론
객체와 같은 값을 유지하고 싶다면 그냥 =을 사용하고
새로운 객체를 생성해서 복사하고 싶고 객체의 필드들이 전부 기본형며 data class 라면 copy()를 사용
새로운 객체를 생성하고 복사하고 싶은데 dataclass가 아니거나 참조형이 존재하면 직접함수를 만든다던지 라이브러리를 사용해야합니다.
Cf)
Spring의 Repository interface에서 흔히 사용하는 save()는 새로운 인스턴스를 반환할까요?
아닙니다. save는 인자로 넘어온 인스턴스의 주소값을 그대로 반환합니다.