[JAVA] 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)에 대하여
얕은 복사(Shallow Copy)란?
public class Human {
String name;
int age;
public Human(){
}
public Human(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;
}
}
위와 같은 Human 클래스가 있다고 하자. 이 클래스를 다음과 같이 복사한 뒤 멤버 값을 변경해 보자.
public static void main(String[] args) {
Human human = new Human("Jackson", 31);
Human copyHuman = human;
copyHuman.setName("Bob");
System.out.println(human.getName());
System.out.println(copyHuman.getName());
}
출력값:
copyHuman = human과 같이 객체를 복사한 뒤 값을 변경시킨 결과는 위와 같다.
개발자의 의도는 복사한 copyHuman의 값만 변경하는 거였을지라도, 원본인 human의 값도 변경되어 버린다.
이는 copyHuman = human과 같은 대입이 실제 값이 아닌 주소를 대입하는 것이기 때문이다.
실제로 두 값의 주소를 아래와 같이 출력해 보면 같은 주소임을 알 수 있다.
public static void main(String[] args) {
Human human = new Human("Jackson", 31);
Human copyHuman = human;
copyHuman.setName("Bob");
System.out.println(human);
System.out.println(copyHuman);
}
이처럼 얕은 복사(Shallow Copy)는 실제 값이 아닌, 값을 가리키는 "주소"를 복사해 같은 객체를 가리키는 것을 뜻한다.
그림으로 쉽게 나타내면 다음과 같다.
Human 인스턴스는 Heap 영역에 생성되고 이를 가리키는 human 변수가 Stack 영역에 생성된다.
이때 copyHuman가 human을 얕은 복사 하면 같은 주소값의 인스턴스를 가리키게 된다.
따라서 copyHuman에서 setName() 메서드를 호출해 값을 변경하면, human의 name 값도 변경되는 것이다.
이러한 상황을 방지할 수 있는 복사 방식이 바로 깊은 복사이다.
깊은 복사(Deep Copy)란?
깊은 복사는 주소값이 아닌 실제 값을 복사하는 것을 뜻한다.
이러한 깊은 복사는 주로 Cloneable 인터페이스 구현, 복사 생성자, 복사 팩토리 이용 등을 이용할 수 있다.
1) Cloneable 인터페이스 구현
Cloneable 인터페이스의 clone() 메서드를 구현하여 깊은 복사를 진행하는 방식이다.
이펙티브 자바의 저자 조슈아 블로크는 해당 저서에서 clone() 메서드의 재정의를 주의해서 진행하라고 했다.
더 자세한 내용을 알고 싶다면 다음 블로그에 잘 정리되어 있으니 참고하자
결론은 배열을 제외하고는 생성자와 팩토리를 이용하는 방식이 더 낫다는 것이다.
2) 생성자, 팩터리 이용
public class Human {
String name;
int age;
public Human(){
}
/*복사 생성자*/
public Human(Human original){
this.name = original.getName();
this.age = original.getAge();
}
/*복사 팩터리*/
public static Human copy(Human original){
Human copyObject = new Human();
copyObject.name = original.getName();
copyObject.age = original.getAge();
return copyObject;
}
public Human(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;
}
}
위와 같이 복사 생성자와 복사 팩터리 메서드를 클래스에 추가하자.
public static void main(String[] args) {
Human human = new Human("Jackson", 31);
Human copyHumanUsingConstructor = new Human(human);
Human copyHumanUsingFactory = Human.copy(human);
copyHumanUsingConstructor.setName("Bob");
copyHumanUsingFactory.setName("IU");
System.out.println(human.getName());
System.out.println(copyHumanUsingConstructor.getName());
System.out.println(copyHumanUsingFactory.getName());
}
이후 위와 같은 코드를 실행하면 결과는 어떨까?
원본인 human을 복사 생성자를 이용해 깊은 복사한 copyHumanUsingConstructor의 값은 "Bob"으로,
복사 팩터리 메서드를 이용해 깊은 복사한 copyHumanUsingFactory의 값은 "IU"로 잘 바뀌었으며,
원본인 human의 값은 그대로 "Jackson"인 것을 확인할 수 있다.
public static void main(String[] args) {
Human human = new Human("Jackson", 31);
Human copyHumanUsingConstructor = new Human(human);
Human copyHumanUsingFactory = Human.copy(human);
copyHumanUsingConstructor.setName("Bob");
copyHumanUsingFactory.setName("IU");
System.out.println(human);
System.out.println(copyHumanUsingConstructor);
System.out.println(copyHumanUsingFactory);
}
실제로 세 값의 주소를 출력해 보면 다음과 같다.
세 값의 주소가 모두 다른 것을 확인할 수 있다.
이를 그림으로 나타내면 다음과 같다.
깊은 복사는 Heap 영역의 같은 인스턴스를 가리키지 않는다. Heap 영역에 새로운 인스턴스를 생성하고 해당 인스턴스에 값을 복사하는 방식으로 동작한다.
따라서 복제된 값을 변경해도 원본이 바뀌는 부작용이 발생하지 않는다.