Developing Myself Everyday
article thumbnail

 

사진: Unsplash Kelly Sikkema

 

 

이번 게시글에서는 얕은 복사와 깊은 복사에 대해 이해해보고, 데이터 클래스의 copy는 어떤 복사를 하고 있는지 알아보고자 합니다.


 

 

 

1. 얕은 복사와 깊은 복사 이해하기


"복사"한다는 것은 프로그래밍 관점에서 데이터를 새로운 메모리 공간에 재배치하여 원본 데이터와 독립적인 데이터 구조를 생성하는 작업을 의미합니다.

 

 

1.1. 얕은 복사(Shallow Copy)

얕은 복사는 객체의 참조 주소만 복사하여 새로운 객체를 생성하는 방식입니다.

 

만약 최상위 타입의 객체가 존재한다면, 최상위 객체만 새로운 메모리 공간에 복사하고 그 내부의 속성들은 원본 객체와 같은 참조를 공유하게 됩니다.

<kotlin />
fun main() { val original = mutableListOf("Bob", "Charlie") val shallowCopy = original shallowCopy.add("Diana") // 원본의 friends도 영향을 받음. println(original) // [Bob, Charlie, Diana] println(original === shallowCopy) // true: 원본과 동일한 참조 주소를 가짐 }

 

 

얕은 복사는 메모리를 적게 사용하고, 복사 과정이 간단하기 때문에 성능이 좋습니다. 하지만, 원본 객체와 같은 주소를 참조하기 때문에 원본 객체에 사이드이팩트가 발생할 수도 있습니다.

 

 

1.2. 깊은 복사(Deep Copy)

깊은 복사는 객체의 모든 필드를 복사하여 새로운 객체를 생성하는 방식입니다.

 

깊은 복사가 되기 위해서는 객체 뿐만이 아니라 내부에 포함돤 모든 참조 타입의 객체들까지 새로운 메모리 공간에 복사해야 합니다.

<code />
fun main() { val original = mutableListOf("Bob", "Charlie") val shallowCopy = original.toList() original.add("Diana") println(original) // [Bob, Charlie, Diana] println(shallowCopy) // [Bob, Charlie] println(original === shallowCopy) // true: 원본과 동일한 참조를 가짐 }

 

 

 

2. 참조형 타입에서의 얕은 복사와 깊은 복사


 

<kotlin />
fun main() { val originalList2 = mutableListOf("Alice", "Bob") val copiedList2 = originalList2.toList() originalList2[0] = "new" println(originalList2 == copiedList2) // false println(originalList2 === copiedList2) // false println(copiedList2[0]) // "Alice" }

 

 

 

<code />
fun main() { val originalList = listOf(Person("Alice"), Person("Bob")) val copiedList = originalList.toList() // 얕은 복사 originalList[0].name = "new" println(originalList == copiedList) // true println(originalList === copiedList) // false println(copiedList[0].name) // "new" println() val originalList2 = mutableListOf("Alice", "Bob") val copiedList2 = originalList2.toList() originalList2[0] = "new" println(originalList2 == copiedList2) // false println(originalList2 === copiedList2) // false println(copiedList2[0]) // "Alice" }

 

 

3. 데이터 클래스의 copy()


데이터 클래스의 copy는 컴파일 과정에서 자동으로 생성되는 메서드입니다. 

 

아래의 데이터 클래스를 예시로 들어보겠습니다.

<kotlin />
data class Person(val name: String, val friends: MutableList<String>)

 

 

위의 데이터 클래스는 컴파일러에 의해 아래와 같이 java로 변환됩니다.

<java />
public final class Person { @NotNull private final String name; @NotNull private final List friends; public static final int $stable = 8; public Person(@NotNull String name, @NotNull List friends) { Intrinsics.checkNotNullParameter(name, "name"); Intrinsics.checkNotNullParameter(friends, "friends"); super(); this.name = name; this.friends = friends; } @NotNull public final String getName() { return this.name; } @NotNull public final List getFriends() { return this.friends; } @NotNull public final String component1() { return this.name; } @NotNull public final List component2() { return this.friends; } @NotNull public final Person copy(@NotNull String name, @NotNull List friends) { Intrinsics.checkNotNullParameter(name, "name"); Intrinsics.checkNotNullParameter(friends, "friends"); return new Person(name, friends); } // $FF: synthetic method public static Person copy$default(Person var0, String var1, List var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = var0.name; } if ((var3 & 2) != 0) { var2 = var0.friends; } return var0.copy(var1, var2); } @NotNull public String toString() { return "Person(name=" + this.name + ", friends=" + this.friends + ')'; } public int hashCode() { int result = this.name.hashCode(); result = result * 31 + this.friends.hashCode(); return result; } public boolean equals(@Nullable Object other) { if (this == other) { return true; } else if (!(other instanceof Person)) { return false; } else { Person var2 = (Person)other; if (!Intrinsics.areEqual(this.name, var2.name)) { return false; } else { return Intrinsics.areEqual(this.friends, var2.friends); } } } }

 

 

위의 코드 중 copy를 보겠습니다.

 

자바에는 Default 파라미터의 개념이 존재하지 않기 때문에 `copy$default` 메서드를 통해 새로운 값이 할당되지 않은 경우 Origin 객체(var0)을 대신해서 넣어줍니다.

 

그 다음 copy를 호출해 객체를 `new`를 통해 새롭게 생성하고 나머지 속성들을 새로운 객체에 그대로 넣어줍니다.

<java />
@NotNull public final Person copy(@NotNull String name, @NotNull List friends) { Intrinsics.checkNotNullParameter(name, "name"); Intrinsics.checkNotNullParameter(friends, "friends"); return new Person(name, friends); } public static Person copy$default(Person var0, String var1, List var2, int var3, Object var4) { if ((var3 & 1) != 0) { var1 = var0.name; } if ((var3 & 2) != 0) { var2 = var0.friends; } return var0.copy(var1, var2); }

 

 

이러한 작동 방식은 단순하게 새로운 객체를 만들고 내부의 속성들은 단순하게 넣어주고 있기 때문에 기본적으로 얕은 복사의 방식입니다.

<kotlin />
data class Person(val name: String, val friends: MutableList<String>) fun main() { val original = Person("Alice", mutableListOf("Bob", "Charlie")) val shallowCopy = original.copy() shallowCopy.friends.add("Diana") // 원본의 friends도 영향을 받음. println(original.friends) // [Bob, Charlie, Diana] println(original === shallowCopy) // false: 최상위 객체의 주소는 새로운 메모리 주소를 가짐 println(original.friends === shallowCopy.friends) // true: 내부 속성은 원본과 동일한 참조를 가짐 }

 

 

무조건 copy를 하면 얕은 복사가 된다는 것은 아닙니다. 만약 copy를 통해 내부 속성들을 모두 새롭게 할당한다면 이는 깊은 복사가 될 것입니다. 

<kotlin />
fun main() { val original = Person("Alice", mutableListOf("Bob", "Charlie")) val shallowCopy = original.copy( name = "new name", friends = original.friends.toMutableList() ) shallowCopy.friends.add("Diana") // 원본의 friends도 영향을 받음. println(original.friends) // [Bob, Charlie] println(original === shallowCopy) // false: 최상위 객체의 주소는 새로운 메모리 주소를 가짐 println(original.name === shallowCopy.name) // false: 내부 속성은 새로운 메모리 주소를 가짐 println(original.friends === shallowCopy.friends) // false: 내부 속성은 새로운 메모리 주소를 가짐 }

 

 

 

4. 결론


데이터 클래스의 copy는 본질적으로 얕은 복사를 구현합니다.

 

이는 데이터 클래스의 내부의 모든 속성을 새로 정의하지 않고 새로운 객체를 생성할 수 있는 매우 좋은 방법입니다.

 

다만 List와 같은 참조형 타입을 사용할 때에는 생각하지도 못한 사이트 이펙트가 발생할 수 있으니 이를 매우 유의하면서 사용해야 합니다.

 

 

 

5. Reference

 

깊은 복사와 얕은 복사의 차이점 이해하기

이 글에서는 깊은 복사와 얕은 복사의 차이점과 각각의 장단점에 대해 설명합니다. 객체를 복사하는 두 가지 방법을 이해하고, 상황에 맞게 선택하여 사용하는 방법을 다룹니다.

f-lab.kr

 

profile

Developing Myself Everyday

@배준형

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!