Java Deep Copy 및 얕은 복사
안녕하세요! 이번에 정리할 내용은 Java로 되어 있습니다. 깊은 복사그리고 얕은 복사 안돼.
깊은 복사와 얕은 복사의 개념은 꽤 일반적이었습니다.
그런데 오늘 알고리즘 문제를 풀면서 의심의 여지없이(?) 다음과 같이 컬렉션 List를 얕은 복사하는 코드를 작성했는데, 그 결과 참조되는 두 리스트의 값이 모두 바뀌어 출력이 다르게 나왔다. 내가 예상했던 것에서, 그래서 나는 약간 혼란스러웠다. .
List<String> list = new ArrayList<>();
...
List<String> temp = list; // shallow copy
디버깅을 통해 문제점을 파악할 수 있었는데 기본이지만 정리하고 넘어가도록 하겠습니다
딥 카피‘실제 값’을 새 메모리 공간에 복사하는 것을 의미하며,
얕은 카피‘주소 값’을 복사하는 것을 의미합니다.
얕은 복사주소 값을 복사하는 경우, 참조하는 실제 값은 동일합니다.
예제와 설명으로 확인해 봅시다.
얕은 카피
public class CopyObject {
private String name;
private int age;
public CopyObject() {
}
public CopyObject(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CopyObjectTest {
@Test
void shallowCopy() {
CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original; // 얕은 복사
copy.setName("JuBal");
System.out.println(original.getName());
System.out.println(copy.getName());
}
}

위의 코드에서 복사 set 메서드를 통해 개체 이름을 변경했습니다.
실제 결과 원래의 개체 및 복사 모든 개체의 값이 변경되었습니다.
CopyObject 사본 = 원본 ‘의 코드에서 개체의 얕은 복사본을 통해주소 값‘ 왜냐하면
참조되는 실제 값은 동일합니다. 복사한 개체가 변경되면 원본 개체도 변경됩니다.안돼.
위 상태의 메모리 구조는 다음과 같습니다.
CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;

원본 인스턴스가 생성되면 스택 영역에 참조 값이 저장되고 힙 영역에 실제 값이 저장됩니다.
그리고 개체가 얕은 복사본을 통해 복사되었으므로 원본 인스턴스에서 복사본 인스턴스를 참조합니다.
마찬가지로 힙 영역의 기준값을 보고 있는 상태입니다.
이후 set 메소드를 통해 값을 변경하면 같은 주소를 참조하기 때문에 아래와 같이 됩니다.

따라서 코드에서는 복사 객체의 이름만 변경하였고,
같은 주소 참조하고 있기 때문에 원래 개체에도 영향을 미칩니다.
따라서 다음과 같이 개체를 인쇄하는 경우: 같은 주소출력됩니다.
CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original;
System.out.println(original);
System.out.println(copy);

딥 카피
딥 카피를 구현하는 방법에는 여러 가지가 있습니다.
- Cloneable 인터페이스 구현
- 복사 생성자
- 카피팩토리 등등….
◎ Cloneable 인터페이스 구현

Cloneable 인터페이스는 위와 같이 빈 쉘 인터페이스이지만,
주석을 보면 Object 클래스의 clone() 메서드를 구현해야 한다고 설명되어 있습니다.
public class CopyObject implements Cloneable {
private String name;
private int age;
public CopyObject() {
}
public CopyObject(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected CopyObject clone() throws CloneNotSupportedException {
return (CopyObject) super.clone();
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@Test
void shallowCopy() throws CloneNotSupportedException {
CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copy = original.clone();
copy.setName("JuBal");
System.out.println(original.getName());
System.out.println(copy.getName());
}

얕은 복사와 달리 깊은 복사를 통해 테스트할 때 원본 인스턴스의 값이 변경되지 않습니다.
※ 효과적인 자바 13장에는 복제를 재정의할 때 주의하여 진행이라는 항목이 있습니다.
책의 내용을 간단히 요약하면 다음과 같다.
Cloneable 인터페이스는 클래스를 복제할 수 있도록 지정하기 위한 믹스인 인터페이스이지만 안타깝게도 의도한 목적을 달성하지 못했습니다. 여기서 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 그마저도 보호된다는 점이다. 따라서 Cloneable을 구현하는 것만으로는 외부 개체에서 복제 메서드를 호출할 수 없습니다. 리플렉션을 사용하면 가능하지만 100% 성공하는 것도 아닙니다.
이 인터페이스는 문제점이 많지만 Cloneable 방식이 많이 사용되기 때문에 익숙해지는 것이 좋습니다.
새로운 인터페이스를 만들 때 Cloneable이 가져온 모든 문제를 되돌아보면 Cloneable을 확장해서는 안 되며, 새 클래스도 이를 구현해서는 안 됩니다. 최종 클래스라면 Cloneable을 구현하는 것은 위험하지 않지만 성능 최적화 관점에서 검토한 후 문제가 없는 경우에만 드물게 허용해야 합니다.
기본 원칙은 ‘복제 기능은 생성자 및 공장입니다.‘를 사용하는 것이 가장 좋습니다.
단계, 준비Mann은 복제 방법이 가장 깨끗하기 때문에 이 규칙에 대한 합리적인 예외입니다.
◎ 복사 생성자, 복사 팩토리
public class CopyObject {
private String name;
private int age;
public CopyObject() {
}
/* 복사 생성자 */
public CopyObject(CopyObject original) {
this.name = original.name;
this.age = original.age;
}
/* 복사 팩터리 */
public static CopyObject copy(CopyObject original) {
CopyObject copy = new CopyObject();
copy.name = original.name;
copy.age = original.age;
return copy;
}
public CopyObject(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
@Test
void shallowCopy() {
CopyObject original = new CopyObject("JuHyun", 20);
CopyObject copyConstructor = new CopyObject(original);
CopyObject copyFactory = CopyObject.copy(original);
copyConstructor.setName("JuBal");
copyFactory.setName("BalJu");
System.out.println(original.getName());
System.out.println(copyConstructor.getName());
System.out.println(copyFactory.getName());
}

복사 생성자와 복사 팩토리를 통해 객체를 복사하는 과정도 딥 카피임을 알 수 있습니다.
딥 카피의 그림 표현은 다음과 같습니다.

얕은 복사와 달리 실제 값을 복사하기 위해 힙 영역에 새로운 메모리 공간이 생성됩니다.
Collections 또는 Maps의 경우 복사 팩토리인 copy() 메서드를 이미 구현하고 있습니다.
/**
* Copies all of the elements from one list into another. After the
* operation, the index of each copied element in the destination list
* will be identical to its index in the source list. The destination
* list's size must be greater than or equal to the source list's size.
* If it is greater, the remaining elements in the destination list are
* unaffected. <p>
*
* This method runs in linear time.
*
* @param <T> the class of the objects in the lists
* @param dest The destination list.
* @param src The source list.
* @throws IndexOutOfBoundsException if the destination list is too small
* to contain the entire source List.
* @throws UnsupportedOperationException if the destination list's
* list-iterator does not support the {@code set} operation.
*/
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
int srcSize = src.size();
if (srcSize > dest.size())
throw new IndexOutOfBoundsException("Source does not fit in dest");
if (srcSize < COPY_THRESHOLD ||
(src instanceof RandomAccess && dest instanceof RandomAccess)) {
for (int i=0; i<srcSize; i++)
dest.set(i, src.get(i));
} else {
ListIterator<? super T> di=dest.listIterator();
ListIterator<? extends T> si=src.listIterator();
for (int i=0; i<srcSize; i++) {
di.next();
di.set(si.next());
}
}
}