객체를 복사했다고 말할 때 어디까지가 복사된 것인지 갈리는 문제. 참조형 변수가 값 자체를 두지 않고 주소를 담기 때문에 생긴다.

얕은 복사

값이 아니라 주소를 복사한다. 복사본과 원본이 같은 것을 참조하니 한쪽을 고치면 다른 쪽도 바뀐다.

int[] a = {1, 2, 3};
int[] b = a;        // 같은 배열을 가리킨다
b[0] = 99;          // a[0]도 99가 된다

컬렉션도 다르지 않다. ArrayList<User> list2 = list;로 받아둔 뒤 list.remove(2)를 하면 list2.size()까지 함께 2가 된다. 리스트를 새로 만들어 list2.add(list.get(i))로 원소를 옮겨 담아도 리스트만 새것일 뿐, 안에 든 User 객체는 여전히 같은 것을 가리키므로 이것 또한 얕은 복사다.

깊은 복사

내부 요소까지 전부 새로 만든다. 주소가 모두 다르니 원본이 바뀌든 그 내부 요소가 바뀌든 복사본은 영향을 받지 않는다.

clone()을 재정의해 내부 객체까지 복사하게 만들거나 복사 생성자를 두는 방법이 있다. 직렬화를 쓰는 길도 있다. 리스트를 ObjectOutputStream으로 byte[]에 써두었다가 ObjectInputStream으로 다시 읽으면 진짜 인스턴스가 두 쌍이 된다. 쓰고 난 뒤에 list.remove(2)를 해도 읽어낸 쪽에는 원소 셋이 그대로 남아 있다. 간단한 대신 느리고, 대상이 기본형이거나 Serializable을 구현한 객체여야 한다.

방어적 복사

경계에서 복사해 내부 필드를 바깥으로부터 지키는 것이 목적이다. 생성자에서는 인자로 받은 객체를 복사해 필드에 담으므로, 넘겨준 쪽이 나중에 원본을 고쳐도 내부는 그대로 남는다. getter에서는 내부 객체의 참조 대신 복사본을 돌려준다. 받아간 쪽이 실수로도 원본을 건드릴 수 없다.

Unmodifiable Collection

unmodifiableList()가 돌려주는 리스트처럼 수정 메서드를 막아둔 읽기 전용 뷰다. getter로 컬렉션을 돌려줄 때 방어적 복사 대신 골라도 된다.

다만 unmodifiableimmutable은 다르다. 수정 메서드가 막혔을 뿐이라 원본 자체가 바뀌면 이 리스트에도 그대로 반영된다. 복사본이 아닌 창이라서 그렇다. 원본과의 주소 공유를 끊고 싶다면 깊은 복사나 방어적 복사를 해야 한다.

메서드에 넘길 때

메서드에 넘길 때 가장 자주 걸린다.

void doSomething(int[] arr) {
    arr[0] = 99;    // 호출한 쪽의 배열이 바뀐다
}

배열도 참조형이라 같은 배열이 넘어간다. 원본을 지키려면 clone()이나 Arrays.copyOf()로 복사본을 만들어 넘긴다. Arrays.copyOf(원본, 크기)가 만든 배열은 원본과 전혀 다른 인스턴스라 == 비교가 거짓이고, int[] copyTo = copyFrom;은 같은 인스턴스라 참이다. 배열을 복사하는 것과 참조하는 것은 전혀 다른 이야기다. 배열

getter로 돌려줄 때

리스트나 맵을 getter로 그대로 돌려주면 받은 쪽이 원본을 수정할 수 있다. 방어적 복사나 읽기 전용 뷰가 필요해지는 자리이고, 필드를 애초에 불변 객체로 두면 이 고민 자체가 사라진다.

참고

원본은 clone()을 재정의해 깊은 복사를 만드는 이야기만 한다. 재정의하지 않은 Object.clone()이 어디까지 하는지는 javadoc에 적혀 있다. “이 객체가 속한 클래스의 새 인스턴스를 만들고 모든 필드를 이 객체의 대응 필드 내용으로 대입하듯 초기화한다. 필드의 내용 자체는 복제되지 않는다. 따라서 이 메서드는 깊은 복사가 아니라 얕은 복사를 수행한다.” Object javadoc

관련

출처