Shallow copy 와 Deep copy

tom·2022년 8월 16일
1

What I Learned

목록 보기
1/2
post-thumbnail

객체 복사 - 위키백과, 우리 모두의 백과사전

객체 지향 프로그래밍에서 객체를 복사할 때의 두 가지 방식인 Shallow Copy(얕은 복사)와 Deep Copy(깊은 복사)에 대해서 공부한 것을 코틀린을 기반으로 정리해보았습니다.

이 두 가지 방식에 대한 차이점을 제대로 숙지하고 있지 않는다면, 프로그래밍 할 때 예기치 못한 복사로 인한 오류로 디버깅하기 어려워질 수 있습니다. (물론 제 이야기 입니다 🥲)

Shallow Copy - 얕은 복사

Shallow Copy 는 얕은 복사를 의미합니다. 얕은 복사란, 주소 공간에서 heap 영역에 저장된 데이터 자체를 복사하는 것이 아닌, stack 영역에 있는 해당 데이터를 가리키는 참조 주소를 복사하는 것을 의미합니다.

아래 코드가 대표적인 shallow copy 예제입니다.

class Person(var name: String, var age: Int)

fun shallowCopy() {
    val person = Person(name = "최우영", age = 26)
    val shallowCopiedPerson = person

    println(person)
    println(shallowCopiedPerson)
}

위 코드의 결과를 보면 person, shallowCopiedPerson 두 객체의 주소가 같은 것을 볼 수 있습니다. 이는 shallow copy 로 인해 참조 주소가 복사되었기 때문입니다.

여튼, 우리는 복사한 객체를 가지고 지지고 볶고 할 수 있을텐데요. 제가 아래와 같은 안일한 생각으로 코딩하다가 실수를 많이 범했습니다….😢

그럼 shallowCopiedPerson 이라는 객체를 새롭게 복사본으로 생성했으니까, 이걸 아무리 수정해도 origin 객체인 person 내부 프로퍼티들의 값은 변하지 않을거야!

fun editShallowCopiedObject() {
	val person = Person(name = "최우영", age = 26)
	val shallowCopiedPerson = person

  shallowCopiedPerson.name = "tom"
  shallowCopiedPerson.age = 27

	println(person)
	println(shallowCopiedPerson)
}

당연히 저의 예상과 희망과는 다르게 값이 전부 변했습니다. 그런데 어떻게 생각해보면 당연한 일이죠, 참조하고 있는 주소를 복사하고 그 주소 내부에 있는 프로퍼티의 값을 변경하였으니 origin 객체의 값 또한 변경되는 것이.

그러면 제가 원했던 대로 작업을 진행하기 위해서는 어떻게 해야할까요?

Deep Copy - 깊은 복사

남은 한 가지 copy 방식인 Deep Copy(깊은 복사)를 하면 되지 않을까요?

Deep Copy 는 깊은 복사를 의미합니다. 깊은 복사란, 얕은 복사와 달리 stack 에 존재하는 참조 주소를 복사하는 것이 아닌 heap 영역에 존재하는 해당 객체의 데이터 자체를 복사하는 것을 의미합니다.

깊은 복사를 하게 되면, 참조 주소를 복사하는 것이 아니기 때문에, 새로운 객체 주소에 origin 객체가 가진 데이터 값 자체를 복사하여 넣어주는 작업을 하게 됩니다.

그런데, 그 전에 한 가지 의문점이 있습니다. 분명히 위의 shallow copy 코드에서는 단순히 대입 연산자를 가지고 copy 를 수행했는데, 그럼 deep copy 는 당연히 대입 연산자로는 안된다는 뜻일텐데…. 먼저 어떤 방법들이 있는지 알아보도록 하겠습니다.

가장 1차원적이고 확실한 방식

가장 1차원적으로 생각해보면 직접 객체를 생성하여 Deep Copy 하는 방식이 있습니다. 아래 코드를 한 번 보겠습니다.

fun deepCopy() {
	val person = Person(name = "최우영", age = 26)
	val deepCopiedPerson = Person()

  deepCopiedPerson.name = person.name
  deepCopiedPerson.age = person.age

	println(person)
	println(deepCopiedPerson)
}

위 코드에서는 새로운 deepCopiedPerson 객체를 기본 생성자를 사용하여 default 값으로 객체를 생성하고, origin 객체의 값을 setter 를 통해 붙여넣었습니다.

물론 객체를 새로 생성하는 과정에 대한 오버헤드가 있을 수 있고, 효율적인 방식인지는 잘 모르겠습니다. 하지만 가장 쉽고, 빠르고, 정확한 방법임은 틀림없어 보입니다.

생성자 또는 복사 팩토리를 사용하는 방식

class Person(var name: String, var age: Int) {
	constructor() : this(name = "", age = 0)

	constructor(person: Person) : this(name = person.name, age = person.age)
}

fun deepCopy() {
	val person = Person(name = "최우영", age = 26)
  val deepCopiedPerson = Person(person)

  println(person)
  println(deepCopiedPerson)
}
fun newPersonInstance(person: Person): Person {
	return Person(person.name, person.age)
}

fun deepCopy() {
	val person = Person(name = "최우영", age = 26)
  val deepCopiedPerson = copyPersonFactory(person)

  println(person)
  println(deepCopiedPerson)
}

위의 코드는 생성자를 통해 origin 객체를 주입하여 새로운 객체를 생성하는 방식과 factory 메소드의 인자로 origin 객체를 넘겨주어 새로운 객체를 return 하도록 하는 방식입니다.

아래 결과에서 볼 수 있듯 완전히 새로운 객체가 생성되어 두 객체의 참조 주소가 다른 것을 확인할 수 있었습니다. (물론 내부 프로퍼티 값도 똑같습니다. 🙂)

코틀린 data class 기본 제공 메서드를 사용하기

Person 클래스를 data class 로 작성하게 되면 코틀린 data class 에서 기본적으로 제공하는 copy 메소드를 사용할 수 있습니다.

이 copy 메소드는 어떻게 보면 위에서 보았던 factory 메소드를 이용하는 것과 얼추 비슷해 보이는데요, 아래 코드로 확인해보겠습니다.

data class Person(var name: String, var age: Int)

fun deepCopy() {
    val person = Person(name = "최우영", age = 26)
    val deepCopiedPerson = person.copy()

    println(person)
    println(deepCopiedPerson)
}

여기까지 deep copy 방식에 대해서 간단하게 알아보았습니다. 그렇다면 아까 전 shallow copy 를 진행했을 때 발생했던 저의 문제점에 대해서 deep copy 가 해결해 줄 수 있는지에 대해서 알아보도록 하겠습니다.

아래 코드는 코틀린의 data class 가 제공해주는 copy 메소드를 통해 deep copy 한 이후 복사된 객체의 내부 프로퍼티 값을 수정하고 origin 객체의 값과 비교해보는 코드입니다.

fun main() {
    val person = Person(name = "최우영", age = 26)
    val deepCopiedPerson = person.copy()

    deepCopiedPerson.name = "tom"
    deepCopiedPerson.age = 27

    println(person)
    println(deepCopiedPerson)
}

결과를 보면 알 수 있듯이 기존의 객체의 값은 변하지 않고 copy 메소드를 통해 deep copy 한 객체의 프로퍼티의 값만 변경된 것을 알 수 있습니다.

이를 통해 제가 shallow copy 를 수행함으로써 실패했던 작업을 이제는 진행할 수 있게 된 것입니다.

조심해야하는 점!

Shallow Copy 는 객체의 참조 주소 자체를 복사하기 때문에, 원본 객체가 만약 할당을 해제해버린다면 복사한 객체는 그 사실을 모르고 해당 주소를 호출하게 되는 현상이 발생합니다.

Shallow Copy 는 이처럼 쉽고 빠르게 복사본을 만들 수 있지만, 개발자가 신경써야할 부분이 아무래도 생깁니다.

Deep Copy 는 해당 주소에 있는 데이터를 꺼내와서 새로운 객체를 생성하는 방식이기 때문에 아무래도 효율이 Shallow Copy 에 비해 떨어집니다.

하지만, 원본 객체가 모두 사용되고 할당이 해제되어도 사본 객체는 아예 다른 참조 주소를 가지는 독립적인 존재이므로 개발자가 그 사실을 인지하지 못해도 된다는 점이 있습니다.

물론 Shallow Copy 든, Deep Copy 든 사용이 완료되었다면 할당 해제 해주는 습관을 가지는 것이 좋습니다. 😀

끝으로

이렇게 Shallow Copy 와 Deep Copy 에 대해서 알아보았습니다. 사실 이름과 동작 방식이 무언가 맞지 않은지라 한 번 공부해놓아도 뒤돌아서면 헷갈리고 까먹기 좋은 주제라고 생각됩니다.

이번에는 아주 간단하게 기본적인 부분들에 대해서만 알아보았는데,

  • 객체 내부의 프로퍼티
  • primitive type
  • Array or List

위와 같이 세분화 하여 각각의 copy 방식을 비교해보아도 재밌을 것 같다는 생각이 들었습니다.

(언젠가 시간이 허락한다면 비교해보고 싶네요. 😃)

참고

Shallow Copy와 Deep Copy

Java - Shallow copy 와 Deep Copy

Java - Shallow Copy(얕은 복사) vs Deep Copy(깊은 복사)

얕은복사(Shallow copy)문제 깊은복사(Deep)로 해결

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

profile
🌱 주니어 안드로이드 개발자 최우영입니다.

0개의 댓글