Java의 Object
메서드인 Clone()
메서드는 인스턴스 객체의 복제를 위한 메서드로, 해당 인스턴스를 복제하여 새로운 인스턴스를 생성해 그 참조 값을 반환합니다.
Clone()
메서드는 기본적으로 Protected 접근 권한을 보유하고 있으며 상속을 통해 public
접근 제어자로 재정의하여 어디에서나 복제가 가능하도록 설정해 사용합니다.
Clone()
메소드를 사용하기 위해서는 오버라이딩을 진행해야하고 데이터의 보호 목적으로 Cloneable
인터페이슬 구현한 클래스만 사용이 가능합니다.
import java.lang.Cloneable;
//Clone을 사용하기위한 Cloneable 구현
class TestClone implements Cloneable {
private String name;
private int age;
public TestClone (String name, int age){
this.name = name;
this.age = age;
}
//접근제어자 변경을 위한 clone 오버라이딩
@override
public Object clone() throws CloneNotSupportedException { //checked exception이라 반드시 예외처리해야함.
return super.clone(); //기본적으로 부모의 clone을 불러와 사용
}
}
public class Main {
public static void main(String [] args){
try{
TestClone tc = new TestClone("a", 1)
TestClone tc_copy = (TestClone) tc.clone();
}catch(Exception e) {
//...
}
}
}
자바의 객체와 같은 참조 변수는 직접 값을 변경하는 것이 아닌 Heap영역에 데이터를 저장하고 그에 해당하는 주소의 값을 저장하는 방식으로 구성되어 동작됩니다.
자바의 원시타입이아닌 참조타입의 변수를 복제해 사용한다면 "값의 복제"가 아닌 "참조주소"의 복제로 결국에는 같은 Heap에 저장된 데이터를 바라보게 됩니다.
이러한 복사를 "얕은 복사(Shallow Copy)"라고 하며 얕은 복사에서는 원본을 변경하면 복제본에도 영향을 미치고, 복제본을 변경하면 원본에도 영향을 미치게됩니다.
깊은 복사(Deep Copy)는 원본 데이터가 참조하고 있는 힙의 데이터까지 복제하는 것을 말하며, 깊은 복사에서는 원본과 복사본이 서로 다른 객체를 참조하기 때문에 서로가 서로에 영향이 없습니다.
// 객체 복사 메소드를 사용하기 위해서는 Cloneable 인터페이스를 구현해서 clone을 재정의 해야함
class User implements Cloneable {
private String name;
public void setName(String name) {
this.name = name;
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Main {
public static void main(String[] args) {
try {
// 얕은 복사(shallow copy)
User user = new User();
user.setName("Edward");
User copy = user;
System.out.println(user.hashCode()); // 622488023
System.out.println(copy.hashCode()); // 622488023
System.out.println(user.equals(copy));
// true - 둘이 동인할 힙데이터를 바라보고 있기 때문에
// 깊은 복사(deep copy)
User user2 = new User();
user2.setName("Edward");
User copy2 = (User) user2.clone();
System.out.println(user2.hashCode()); // 1933863327
System.out.println(copy2.hashCode()); // 112810359
System.out.println(user2.equals(copy2));
// false - 둘은 복사되어 생김새만 같지 다른 힙데이터를 바라보고 있기 때문에
} catch(CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
깊은 복사는 참조 주소까지 모두 복제하기때문에 완벽하게 복제가 됐다고 생각할 수 있습니다.
여기서 한번더 생각해볼 수 있는 것은 "객체 배열을 복제할때는 어떻게 진행 될지?"에 대한 문제가 나올 수 있을 거라고 생각합니다.
객체 배열은 그 자체로도 참조타입이고, 내부의 요소들도 모두 참조타입으로 구성되어 있어서 그냥 깊은 복사를 진행하게 되면 "배열 자체를 깊은 복사", "배열의 요소는 얕은 복사가 진행됩니다."
public class DeepCloneTest {
String name;
int age;
public DeepCloneTest(String name, int age) {
this.name = name;
this.age = age;
}
}
import java.util.Arrays;
public class DeepCloneTestMain {
public static void main(String[] args) {
DeepCloneTest[] arr = {
new DeepCloneTest("a", 1),
new DeepCloneTest("b", 2),
new DeepCloneTest("c", 3)
};
//배열 주소 확인
System.out.println("기준 배열 : " + Arrays.toString(arr));
//배열 복제
DeepCloneTest[] copy = arr.clone();
//복제 주소 확인
System.out.println("복제 배열 : " + Arrays.toString(copy));
//요소 확인
System.out.println("기준배열 요소 : " + arr[0].name);
System.out.println("복제배열 요소 : " + copy[0].name);
//요소 변경 시 사이드 이펙트
arr[0].name = "change";
System.out.println("기준배열 요소 변경 : " + arr[0].name);
System.out.println("복제배열 요소 변경 : " + copy[0].name);
}
}
기준 배열 :
[Test.ex1.DeepCloneTest@6acbcfc0, Test.ex1.DeepCloneTest@5f184fc6, Test.ex1.DeepCloneTest@3feba861]
복제 배열 :
[Test.ex1.DeepCloneTest@6acbcfc0, Test.ex1.DeepCloneTest@5f184fc6, Test.ex1.DeepCloneTest@3feba861]
기준배열 요소 : a
복제배열 요소 : a
기준배열 요소 변경 : change
복제배열 요소 변경 : change
위 예제 결과를 보면 배열 자체는 복사가 되었지만, 배열의 내용물 객체는 얕은 복사가 되어버려 "기준"배열의 요소 변경시 복제 배열 요소에 영향을 미치는 것을 확인 할 수 있습니다.
이와 같은 현상을 방지하기 위해서는 Object.clone()
의 재정의와 각각 요소에 대한 참조값을 루프를 돌며 복제를 진행해야합니다.
데이터를 Clone하는 것의 핵심은 원시타입이 아닌 참조타입의 복제를 진행할때는 오버라이딩이 진행해야
원하는 깊은 복사가 된다는 점이다.
public class DeepCloneTest implements Cloneable {
String name;
int age;
public DeepCloneTest(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected DeepCloneTest clone() throws CloneNotSupportedException {
return (DeepCloneTest) super.clone();
}
}
import java.util.Arrays;
public class DeepCloneTestMain2 {
public static void main(String[] args) throws CloneNotSupportedException {
DeepCloneTest[] arr = {
new DeepCloneTest("a", 1),
new DeepCloneTest("b", 2),
new DeepCloneTest("c", 3)
};
//배열 주소 확인
System.out.println("기준 배열 : " + Arrays.toString(arr));
DeepCloneTest[] copyData = new DeepCloneTest[arr.length];
for(int i = 0; i < arr.length; i++){
copyData[i] = arr[i].clone();
}
System.out.println("복제 배열 : " + Arrays.toString(copyData));
//요소확인
System.out.println("기준요소 : " + arr[0].name);
System.out.println("복제요소 : " + copyData[0].name);
//요소 변경 후 확인
arr[0].name = "test";
System.out.println("기준요소 : " + arr[0].name);
System.out.println("복제요소 : " + copyData[0].name);
}
}
기준 배열 : [Test.ex1.DeepCloneTest@6acbcfc0, Test.ex1.DeepCloneTest@5f184fc6, Test.ex1.DeepCloneTest@3feba861]
복제 배열 : [Test.ex1.DeepCloneTest@723279cf, Test.ex1.DeepCloneTest@10f87f48, Test.ex1.DeepCloneTest@b4c966a]
기준요소 : a
복제요소 : a
기준요소 : test
복제요소 : a
clone()
메서드의 오버라이딩 진행 후에는 배열자체가 바라보고 있는 주소와 각각 요소가 바라보고 있는 주소가 다른 것을 확인 할 수 있습니다.
여기서 하나 다른 점은 기존에 단일 참조타입 객체를 깊은 복사를 진행하기 위해 오버라이딩을 했을 때는
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
으로 반환 타입이 Object
로 설정해서 사용을 했습니다.
객체 배열을 복제하기 위해서는 Object
타입으로 반환하는 것이 아닌 DeepCloneTest
으로 반환 받는 것을 확인 할 수 있습니다.
이는 JDK 1.5부터 지원하는 "공변 반환 타입(convariant return type)"으로 오버라이딩을 진행할때 부모 메서드의 반환타입을 자식클래스의 반환타입으로 변경을 허용한다는 것입니다.
이를 통해 자식 타입으로 바로 반환을 받을 수 있어 불필요한 캐스팅이 줄어든다는 장점이 있습니다.
Clone()
은 기본적으로 얕은 복사를 수행super.clone()
은 새로운 객체를 생성하지만, 참조타입 필드는 그대로 복사되므로 깊은 복사가 필요한 경우에는 오버라이딩이 필요하다.