[Kotlin] 깊은 복사(Deep Copy) 하는 3가지 방법

Doyeon Lim·2021년 9월 11일
6

Kotlin

목록 보기
1/2

> Tech-I-Learned Github 바로가기

너 것도 내꺼, 내 것도 내꺼

🤔복사가 뭐가 중요한데?

가끔 그럴때가 있다.

분명 A 변수/인스턴스의 값을 변경했는데 B까지 바뀐 그런 슬픈 일..
띠용해서 주솟값을 찍어보면 더 띠용한다.
내가 직접 변경한 A와 변경된 B가 같은 주솟값을 가지고 있다.

그것은 바로 얕은 복사와 깊은 복사를 구분하지 않았기 때..문..
내 변수는 소중하니까.. 복사하는 방법에 대해 알아보자..

😮얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy)

얕은 복사는 말 그대로 얕게 복사하고 깊은 복사는 깊게 복사한다.

뭘 깊게 복사한다는건데!!! 라고 묻는다면 대답하는게 인지상정!

값의 변화를 막기 위해
다른 변수에 영향을 주지 않기 위해
사랑과 진실, 어둠을 뿌리고 다니는
소중한 깊은 복사다옹..


얕은 복사(Shallow Copy)

얕은 복사 : 객체를 복사할 때, 객체의 주솟값을 복사하는 방법

class SampleClass(var id: Int) {
	...
}

위와 같은 SampleClass가 존재할 때 얕은 복사는 다음과 같이 진행된다.

val instance1 = SampleClass(1) // 인스턴스 생성

val instance2 = instance1 // 인스턴스1을 2에 얕은 복사 진행

instance1.id = 3 // 얕은 복사를 진행한 객체의 멤버변수를 변경

println(instance2.id) // 3

분명 instance2에 instance1을 복사하고 1의 값만 변경했는데
띠용? 2의 값이 1에 적용한 값으로 변경되었다.

얕은 복사를 통해 주솟값이 복사되었기 때문에 두 변수가 같은 주솟값을 참조하면서 생기는 문제인 것이다.

깊은 복사(Deep Copy)

깊은 복사 : 객체를 복사할 때, 객체의 주솟값이 아닌 전체 값이 복사되는 방법

data class SampleClass(var id: Int) {
	...
}

슬쩍 클래스를 data class로 변경해보면 깊은 복사를 아주 쉽게 진행할 수 있다.

val instance1 = SampleClass(1) // 인스턴스 생성
val instance2 = instance1.copy() // 인스턴스1을 2에 깊은 복사 진행

instance1.id = 3 // 깊은 복사를 진행한 객체의 멤버변수를 변경

println(instance2.id) // 1

data class에서 제공하는 몇가지 기본 오버라이딩 메서드 중 copy() 메서드를 통해서 깊은 복사를 할 수 있다.

얕은 복사와 달리 복사한 객체의 값이 변하지 않는 것을 확인할 수 있다.

그렇다고 우리는 객체를 깊은 복사하기 위해서 무조건 data class로 변경해야할 필요는 없다. 여러 깊은 복사 방법에 대해 알아보도록 하자.

☝첫번째 깊은 복사, Cloneable

깊은 복사를 하는 여러 가지 방법 중 첫번째는 Cloneable이다.

Cloneable 인터페이스를 상속받아서 clone() 메서드를 오버라이딩 하는 것이 이 방법의 핵심이다.

class SampleClass(var id: Int, var list: MutableList<Int>) : Cloneable {
    public override fun clone(): SampleClass {
        return super.clone() as SampleClass
    }
}

너무 쉬운데!!! 라고 생각했다면 그것은 경기도 오산..

primitive type만 멤버변수로 갖고 있다면 매우 쉽게 깊은 복사가 가능하지만 우리가 클래스를 그렇게 호락호락하게 만들지 않는다..

타입을 정의하거나 리스트를 사용하거나 등등.. 여러 기본형이 아닌 멤버변수는 깊은 복사가 되지 않는다는 슬픈 사실

이렇게 클래스에 clone을 오버라이딩해서 작성했다면 아래와 같이 리스트만 얕은 복사가 진행된다.

val instance1 = SampleClass(1, mutableListOf(1, 2))
val instance2 = instance1.clone()

instance1.id = 3
instance1.list.add(3)

println(instance2.id) // 1
println(instance2.list) // [1, 2, 3]

따라서 모든 멤버변수를 깊은복사해주기 위해서는 clone 메서드를 재정의해주어야한다.

class SampleClass(var id: Int, var list: MutableList<Int>) : Cloneable{
    public override fun clone(): SampleClass {
        val sampleClass = super.clone() as SampleClass
        sampleClass.list = mutableListOf<Int>().apply{ addAll(list) }
        return sampleClass
    }
}

아 인터페이스 상속받기 귀찮아~~~ 라고 생각했다면 다음 방법으로 넘어가야한다.

✌두번째 깊은 복사, Copy

두번째 방법은 data class의 copy 메서드를 사용하는 것이다.

위에서 먼저 나왔던 data classtoString(), hashCode(), equals(), copy() 메서드가 이미 구현되어 있다.

그 중에서도 copy() 메서드를 이용해 우리는 깊은 복사를 아주 쉽게 이용할 수 있다.

data class SampleClass(var id: Int) {
	...
}

상속따윈 갖다버리고 바로 copy를 이용해 깊은복사를 할 수 있지만!!

val instance1 = SampleClass(1) // 인스턴스 생성
val instance2 = instance1.copy() // 인스턴스1을 2에 깊은 복사 진행

instance1.id = 3 // 깊은 복사를 진행한 객체의 멤버변수를 변경

println(instance2.id) // 1

clone과 똑같이 이 친구도 기본형이 아닌 변수는 깊은 복사가 안된다..

되는게 없네..

그래서 clone처럼.. copy를 내 입맛대로 재정의해주어야한다.

그런데 copy는 Any의 메서드가 아니기 때문에, 직접 정의하려면 override 함수가 아닌 일반 함수로 정의해야 한다.

data class SampleClass(var id: Int, var list: MutableList<Int>) {
    fun copy() : SampleClass {
        return SampleClass(id, list.toMutableList())
    }
}

이렇게 리스트를 따로 깊은 복사해주어야 완벽한 data class의 깊은 복사가 진행된다.

🤙세번째 깊은 복사, Gson

구글링하다가 stack overflow에서 발견한 Gson을 활용한 방식이 있어서 가져왔다.

Gson은 자바 오브젝트를 JSON으로 변환시켜주거나 반대로 해주는 구글에서 만든 라이브러리다.

이 라이브러리를 사용하기 위해서는 아래의 의존성을 추가해주어야한다.

  • Gradle
dependencies {
  implementation 'com.google.code.gson:gson:2.8.8'
}
  • Maven
<dependency>
  <groupId>com.google.code.gson</groupId>
  <artifactId>gson</artifactId>
  <version>2.8.8</version>
</dependency>

데이터를 복사하기 위해 사용할 메소드는 2가지이다.

  • fromJson : Json 데이터를 지정한 타입으로 변환
  • toJson : 지정한 타입의 데이터를 Json 형식으로 변환

toJson 메서드를 사용해서 data class의 값을 먼저 Json 형식으로 변환하고, fromJson 메서드를 사용해서 해당 데이터를 다시 data class의 형식으로 변환해주면 깊은 복사를 쉽게 할 수 있다.

data class SampleClass(var id: Int, var list: MutableList<Int>){
    fun deepCopy() : SampleClass {
        return Gson().fromJson(Gson().toJson(this), this::class.java)
    }
}

😰복사하기 참 힘드네..

깊은 복사를 할 수 있는 총 3가지 방법에 대해서 알아보았다.
물론 이렇게 안하고 노가다로 새로 인스턴스를 생성해 값을 일일이 넣어주면 되지만 그것도 귀찮으니까..

유익..하다..유익..해..

참고한 자료

뚜뚜시님 Tistory : Cloneable
dEpayse님 Medium : Kotlin-data class
stack overflow : Gson을 사용한 deep copy
Google Github : Gson Repository

profile
🙇‍♀️ Android

1개의 댓글

comment-user-thumbnail
2023년 2월 1일

안녕하세요

"두번째 깊은복사"는 data class 사용했기 때문에 deep copy 되는게 맞지 않을까요?

답글 달기