여러 참조자가 하나의 인스턴스를 공유하게 된다. => 공유하는 하나의 인스턴스 상태를 변경 시 변경으로 인한 영향이 전파된다.
얕은 복사 예시
class Address {
public String addr;
public String phone;
public Address(String addr, String phone) {
this.addr = addr;
this.phone = phone;
}
}
class User {
private String name;
private Address addr;
User(String name, String addr, String phone) {
this.name = name;
this.addr = new Address(addr, phone); // 원본 생성
}
public String getName() {
return name;
}
public Address getAddr() {
return addr;
}
public void copy(User rhs) {
this.name = rhs.name;
this.addr = rhs.addr; // 얕은 복사: 인스턴스 내용이 아닌 참조값 복사
}
}
public class Main() {
public static void main(String[] args) {
User user1 = new User("Hosung", "Hanam", "010-1111-1111");
User user2 = new User("Hoon", "Seoul", "010-2222-2222");
System.out.println(user1.getName());
System.out.println(user1.getAddress().address);
System.out.println(user1.getAddress().phone);
user1.copy(user2);// user1의 addr과 user2의 addr이 같은 Address 인스턴스 참조
user2.getAddress().address = "Busan";
user2.getAddress().phone = "010-3333-3333";
System.out.println(user1.getName());
System.out.println(user1.getAddress().address);
System.out.println(user1.getAddress().phone);
}
}
// 결과
Hosung
Hanam
010-1111-1111
Hoon
Busan
010-3333-3333
user1.copy(user2) 실행 전(앝은 복사 발생 전)의 user1의 addr이 참조하는 인스턴스와 user2의 addr이 참조하는 인스턴스는 다르다.
user1.copy(user2) 실행 후(앝은 복사 발생 후)의 user1의 addr이 참조하는 인스턴스와 user2의 addr이 참조하는 인스턴스는 동일한 인스턴스이다.
그렇기 때문에 user2.getAddress().address = ..., user2.getAddress().phone = ...로 user2의 addr이 참조하는 인스턴스의 내용을 변경하면
user1의 addr을 통해 상태 변경을 하지 않았음에도 user1의 addr을 통해 addr이 참조하는 인스턴스의 상태를 조회하면 인스턴스의 상태가 변경되어 있는 것이다.
이는 예상치 못한 사이드 이펙트 문제가 발생할 수 있음을 의미한다.
깊은 복사: 원본 자체를 새로 할당하고 내용을 복사하는 방식
Shallow Copy와 달리 두 개의 원본 두 개의 참조가 각각 별도로 존재
사이드 이펙트 오류 가능성이 없음
참조를 멤버로 가지며 인스턴스를 동적 할당하는 경우 복사 생성자 구현 필요(clone 메서드)
user1의 addr과 user2의 addr을 얕은 복사가 아닌 깊은 복사 방식으로 변경
public void copy(User rhs) {
this.name = rhs.name;
this.addr.addr = rhs.addr.addr; // 깊은 복사: 인스턴스 안의 addr 값이 복사
this.addr.phone = rhs.addr.phone; // 깊은 복사: 인스턴스 안의 phone 값이 복사
}
public class Main() {
public static void main(String[] args) {
User user1 = new User("Hosung", "Hanam", "010-1111-1111");
User user2 = new User("Hoon", "Seoul", "010-2222-2222");
System.out.println(user1.getName());
System.out.println(user1.getAddress().address);
System.out.println(user1.getAddress().phone);
user1.copy(user2);// user1의 addr과 user2의 addr은 다른 Address 인스턴스 참조
user2.getAddress().address = "Busan";
user2.getAddress().phone = "010-3333-3333";
// user2의 addr이 참조하는 Address 인스턴스 내용만 변경
// user1의 addr이 참조하는 Address 인스턴스 내용엔 영향 없음
System.out.println(user1.getName());
System.out.println(user1.getAddress().address);
System.out.println(user1.getAddress().phone);
}
}
// 결과
Hosung
Hanam
010-1111-1111
Hoon
Seoul
010-2222-2222
user1.copy(user2) 실행 후(깊은 복사 발생 후), user2의 addr이 참조하는 Address 타입 객체의 내용(addr 값과 phone 값)이 user1의 addr이 참조하는 Address 타입 객체의 내용으로 복사된다.
참조가 복사되는 것이 아닌, 내용(값)이 복사되므로 user2의 addr이 참조하는 Address 타입 객체의 내용을 변경하더라도 user1의 addr이 참조하는 Address 타입 객체의 내용은 해당 변경에 대해 영향을 받지 않는다.
복사 생성자
자바에서 새롭게 생성하는 인스턴스에 기존 인스턴스의 내용을 깊은 복사하기 위한 방법으로, Object 클래스의 clone 메서드를 오버라이딩하여 사용하는 방법이 있다.
하지만 Object 클래스의 clone 메서드는 오버라이딩하여 사용하기가 어렵다.
Object 클래스의 clone 메서드
java.lang.Object 클래스에 정의된 protected Object clone() 메서드는 객체의 필드 값을 복사해서 새 인스턴스를 반환
기본적으로는 얕은 복사(shallow copy)만 수행
Cloneable 인터페이스를 구현해야 clone() 호출 시 CloneNotSupportedException이 발생하지 않는다.'
clone()을 오버라이딩해 사용하기 어려운 이유들
Cloneable 인터페이스가 마커 인터페이스다 (문제가 되는 이유)
Cloneable은 메서드가 전혀 없는 "마커 인터페이스"
단순히 clone() 호출 시 예외 발생 여부만 제어할 뿐, 진짜 복제 동작은 Object.clone()에만 정의
즉, 복제 로직은 Object가 알고 있음에도, 사용자가 구현 책임을 져야 함
Object.clone()은protected` 메서드다
clone() 메서드는 Object에서 protected로 선언되어 있어, 외부 클래스에서 바로 호출할 수 없음
즉, public으로 오버라이딩하지 않으면 사용 자체가 제한됨.
얕은 복사(shallow copy)의 한계
기본 clone()은 객체의 필드 값을 그대로 복사
하지만 필드가 참조 타입일 경우 내부 객체의 주소만 복사되므로 원본과 공유하게 됨.
깊은 복사를 구현하기 까다로움
객체 내에 다른 참조 타입이 있고, 그 내부에도 참조 타입이 있으면 재귀적 깊은 복사 필요.
따라서 모든 관련 클래스가 clone()을 적절히 구현하고 있어야 함 → 결합도 증가, 오류 가능성 증가**
불변 객체는 복사해서 변경하는 방식이 어울리나, clone()은 상태 변경 가능성을 열어둠
상속이 깊은 경우, clone()을 제대로 구현하려면 모든 하위 클래스에서 오버라이딩이 필요 → 유지보수성 저하]
이와 같은 이유들로 Object의 clone 메서드를 재정의하여 새롭게 생성한 인스턴스에 깊은 복사를 하기 위한 메서드를 구현하기 보다는 새롭게 생성한 인스턴스에 깊은 복사를 하기 위한 복사 생성자를 직접 구현할 것을 권장한다.
새롭게 생성한 인스턴스에 기존 인스턴스의 내용을 깊은 복사 하기 위한 복사 생성자 형식(자신 자신의 타입으로 정의된 기존 인스턴스의 참조자를 매개변수로 전달받아야 한다. 즉, 매개변수 타입이 자기 자신이어야만 한다.)
class Person {
String name;
Address address;
public Person(String name, String addr, String phone){
this.name = name;
this.address = new Address(addr, phone);
}
// 복사 생성자
public Person(Person other) {
// 내용(값) 복사
this(other.name, other.address.addr, other.address.phone);
}
}
클래스의 인스턴스 변수로 배열이 포함될 때, 새롭게 생성한 인스턴스에 해당 클래스 타입의 인스턴스 안의 배열을 깊은 복사하는 복사 생성자 구현
public class MyTest {
private int[] array = null;
public MyTest(int size) {
array = new int[size];
}
public MyTest(MyTest rhs) { // 자신 자신의 타입을 매개변수 타입으로 선언
this(rhs.array.length);
this.deepCopy(rhs); // 새로 생성한 인스턴스에 기존 인스턴스 내용 깊은 복사
}
public int getAt(int index) {
return array[index];
}
public void setAt(int index, int value) {
array[index] = value;
}
public void shallowCopy(MyTest rhs) {
array = rhs.array; // 참조값 대입으로 얕은 복사
}
public void deepCopy(MyTest rhs) {
// array = rhs.array.clone(); 배열 타입 안의 깊은 복사를 위한 멤버 메서드 clone
for(int i=0; i < rhs.array.length; i++){
// array는 private 멤버이지만 자신의 클래스 안이므로 접근 가능
array[i] = rhs.array[i];
}
}
}
public class Main() {
public static void main(String[] args) {
MyTest t1 = new MyTest(3);
t1.setAt(0, 10);
t1.setAt(1, 20);
t1.setAt(2, 30);
MyTest t2 = new MyTest(3);
t2.shallowCopy(t1);
MYTest t3 = new MyTest(3);
t3.shaalowCopy(t1);
MyTest t4 = new MyTest(t1);
t1.setAt(0, -1);
System.out.println("t1[0]: " + t1.getAt(0));
System.out.println("t2[0]: " + t2.getAt(0));
System.out.println("t3[0]: " + t3.getAt(0));
System.out.println("t4[0]: " + t4.getAt(0));
}
}