기본 원칙은 "복제 기능은 (배열을 제외하고) 생성자와 팩터리를 이용하는 게 최고"이다.
📌 Cloneable
✒️ clone()
단어가 주는 의미 그대로 자신을 복제하여 새로운 인스턴스를 생성해준다.
clone() 메서드를 오버라이딩 하기 위해서는 Cloneable을 우선 구현해주어야 한다.
구현할 때는 얕은 복사가 되지 않도록 주의하여야 한다.
public interface Cloneable {
}
메서드 하나 없는 Cloneable 인터페이스는 Object의 protected 메서드인 clone의 동작방식을 결정한다.
(보통 인터페이스라 하면 클래스가 해당 인터페이스에서 정의한 기능을 제공한다고 선언하는 행윈데, Cloneable은 상당히 이례적인 케이스니 따라하지 말자.)
Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면, 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 구현하지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportException을 던진다.
📌 Cloneable 인터페이스 주의사항
Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 Mixin interface이나 의도한 목적을 제대로 이루지는 못 했다.
1️⃣ clone 메서드가 선언된 곳이 Object이며, 그마저도 protected다.
Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
리플렉션(Item 65)을 사용하면 가능은 하지만, 이 또한 100% 성공을 보장하진 않는다.
해당 객체가 접근 허용된 clone 메서드를 제공한다는 보장이 없기 때문이다.
2️⃣ 실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며, 당연히 복제가 제대로 이루어져야 한다.
이를 만족시키려면 해당 클래스와 부모 클래스는 복잡하고, 강제할 수 없고, 허술하게 기술된 프로토콜을 지켜야 한다.
그 결과 생성자를 호출하지 않고도 객체를 생성할 수 있는 깨지기 쉽고, 위험하고, 모순적인 메커니즘이 수반된다.
3️⃣ clone 메서드의 일반 규약은 상당히 허술하다.
/*
* 이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라
* 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음 식은 참이다.
*
* x.clone() != x
*
* 또한 다음 식도 참이다.
*
* x.clone().getClass() == x.getClass()
*
* 하지만 이상의 요구를 반드시 만족해야 하는 것은 아니다.
* 한편 다음 식도 일반적으로 참이지만, 역시 필수는 아니다.
*
* x.clone().equals(x)
*
* 관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와
* (Object를 제외한) 모든 상위 클래스가 이 관례를 따른다면 다음 식은 참이다.
*
* x.clone().getClass() == x.getClass()
*
* 관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로
* 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.
*/
4️⃣ 생성자 연쇄(constructor chaining) 방식과는 다르다.
clone 메서드가 super.clone이 아닌, 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 문제 없다고 판단할 것이다.
하지만 하위 클래스에서 super.clone을 호출한다면 잘못된 클래스 객체가 만들어져 제대로 작동하지 않는다.
(A를 상속받은 B의 clone은 B 타입 객체를 반환해야 하는데, A가 생성자로 만든 객체를 반환하면 B의 clone도 A 타입 객체를 반환할 수밖에 없다.)
5️⃣ clone을 재정의한 클래스가 final이라면 걱정할 하위 클래스가 없으니 이 관례는 무시해도 된다.
하지만 final 클래스의 clone 메서드가 super.clone을 호출하지 않는다면 Cloneable을 구현할 이유도 없다.
애초에, Object의 clone 구현 동작 방식에 기댈 필요가 없다.
6️⃣ 모든 필드가 기본 타입이거나 불변 객체를 참조하는 경우, 이 객체는 완벽한 상태이므로 clone을 제공하지 않는 것이 좋다.
쓸데없는 복사를 지양한다는 관점에서 보면 불면 클래스는 굳이 clone 메서드를 제공하지 않는 것이 좋다.
7️⃣ 재정의한 메서드의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입일 수 있다.
따라서, 클라이언트가 별도로 형변환 하지 않도록 인터페이스를 제공하는 것이 좋다.
자바는 공변 반환 타이핑(covariant return typing)을 지원하니 이렇게 하는 것이 가능하며, 실제로 권장된다.
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없다.
}
}
8️⃣ try-catch 블록으로 Object의 clone 메서드가 검사 예외(checked exception)으로 제공되는 것을 비검사 예외(unchecked exception)으로 처리하도록 한다.
PhoneNumber가 Cloneable을 구현하니, super.clone()은 성공이 보장됨을 안다.
따라서, 해당 코드는 불필요한 기능이었다.
9️⃣ Cloneable을 구현한 thread-safe 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해주어야 한다. (Item 78)
Object의 clone 메서드는 동기화를 신경 쓰지 않았다.
그러니 super.clone 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화 해주어야 한다.
public class Foo implements Cloneable {
@Override public synchronized Foo clone() {
try {
return (Foo) super.clone();
} catech (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
🔟 Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다.
이때 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다.
이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다.
객체 내부의 깊은 복사를 통해, 복제본이 가진 객체 참조 모두가 복사된 객체를 가리키게 해야 한다.
📌 가변 객체 복사의 경우
clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
아이템 7에서 썼던 Stack 클래스를 예로 들어보자.
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0) throw new EmptyStackException();
return elements[--size];
}
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
이 클래스에서 단순히 super.clone의 결과를 반환한다고 가정해보자.
- 반환된 인스턴스의 size 필드는 올바른 값을 가지겠지만, elemets 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조하게 된다.
- 즉, 얕은 복사로 인해 불변식을 해친다.
- 이로 인해, 프로그램이 이상하게 동작하거나 NullPointerException을 던지게 된다.
이 문제를 해결하는 방법은 elements 배열 필드도 clone을 호출하여 복사하면 된다.
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
배열의 clone은 런타임 타입과 컴파일 타입 모두가 원본 배열과 같은 배열을 반환한다.
따라서 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장하는데, 사실상 clone 기능을 제대로 사용하는 유일한 예라고 볼 수 있다.
📌 근본적인 한계
위의 elements 필드가 final이었다면 이전 방식은 작동하지 않는다. (final 필드는 새로운 값을 할당할 수 없다.)
이는 직렬화와 마찬가지로 Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다. (단, 원본과 복제된 객체가 그 가변 객체를 공유해도 안전하다면 무시해도 좋다.)
이러한 이유로 복제할 수 있는 클래스를 구현하기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다.
clone을 재귀적으로 호출할 때도 고려해야 할 사항들이 있다.
해시 테이블 내부는 버킷들의 배열이고, 각 버킷은 key-value 쌍을 담는 연결 리스트의 첫 번째 엔트리를 참조하는 코드를 보자.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry nxt;
Entry(Object key, Object value, Entry nxt) {
this.key = key;
this.value = value;
this.nxt = nxt;
}
}
}
Stack에서 했듯이 단순히 버킷 배열의 clone을 재귀적으로 호출하면 어떤 문제가 생기겠는가?
복제본은 확실히 자신만의 버킷 배열을 갖는 것은 성공할 것이다.
하지만 이 배열은 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예상하지 못 했던 결과를 낳을 수 있다.
일반적인 해법은 연결 리스트를 재귀적으로 복사하는 것이다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
...
Entry deepCopy() {
return new Entry(key, value,
nxt == null? null : nxt.deepCopy());
}
}
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.bucket = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
private class인 HashTable.Entry에게 깊은 복사 기능을 지원하도록 보강하였다.
HashTable의 clone 메서드는 적절한 크기의 새로운 버킷 배열을 할당하여 원본 버킷 배열을 순회하며 깊은 복사를 수행한다.
이 기법은 비록 간단하지만, 재귀 호출 때문에 리스트 원소 수만큼 스택 프레임을 소비한다.
심지어 리스트가 길면 StackOverflow를 일으킬 위험성이 있다.
이 문제를 회피하려면 재귀 호출 대신 반복자로 대체하는 것이 좋다.
Entry deepCopy() {
Entry result = new Entry(key, value, nxt);
for (Entry p = result; p.nxt != null; p = p.nxt)
p.nxt = new Entry(p.nxt.key, p.nxt.value, p.nxt.nxt);
return result;
}
📌 복잡한 가변 객체를 복제하는 방법
- super.clone()을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정.
- 원본 객체의 상태를 다시 생성하는 고수준 메서드(ex. put(key, value))들을 호출
하지만 이 방법은 저수준에서 바로 처리할 때보다는 느리며, Cloneable 아키텍처의 기초가 되는 필드 단위 객체 복사를 우회하기 때문에, 전체 Cloneable 아키텍처와 어울리는 방식도 아니다.
심지어 고려해야할 점들이 한 두가지가 아니다.
- 생성자와 마찬가지로 clone 메서드도 재정의될 수 있는 메서드를 호출해서는 안 된다.
- 복제 과정에서 자신의 상태를 교정할 기회를 잃어 원복과 클론의 상태가 달라질 수 있다.
- 따라서, 앞에서 얘기한 put(key, value) 메서드는 final이거나 private이어야 한다. (private라면 final이 아니라 public 메서드가 사용하는 도우미 메서드가 될 것이다.)
- public인 clone 메서드에서는 throws 절을 없애야 한다.
- 비검사 예외로 수정해야 해당 메서드를 사용하기 편리하다.
- 상속해서 쓰기 위한 클래스 설계 방식 두 가지(아이템 19) 중 어느 쪽에서든, 상속용 클래스는 Cloneable을 구현해서는 안 된다.
- Object의 방식을 모방하여 제대로 동작하는 protected clone 메서드를 구현하고, Object를 바로 상속할 때처럼 CLoneable 구현 여부를 하위 클래스에서 선택할 수 있도록 만드는 것이 낫다.
- 혹은 clone을 동작할 수 없게 상위 클래스에서 구현해놓고 예외 처리를 해버릴 수도 있다.
- Cloneable을 구현한 thread-safe 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화 해주자.
📌 Why use clone()?
이 챕터에서 가장 이해가 안 가는 부분은 왜 굳이 클론 메서드를 사용하는지였다.
그냥 생성자를 호출해서 복사해도 충분할 텐데, "왜 굳이?"라는 생각이 떠나질 않았다.
그런데 뒤의 아이템들을 공부하다가 이유를 어렴풋이 짐작할 수 있게 되어 내용을 추가했다.
객체의 내부 상태를 복사해서 변경하지 않은 불변 객체를 생성할 때, 방어적 복사(Defensive copying)을 해야할 때가 있다.
보통 이 기법에서 생성자를 호출해서 새로운 객체를 생성하는 것은 일반적으로 문제가 없다.
하지만 만약 객체 내부 상태가 가변 객체를 참조하고 있다면, 생성자에서 단순히 복사본을 만들어 반환하는 것만으로는 불변 객체를 보장할 수 없다.
복사된 객체와 원래 객체가 같은 내부 상태를 참조하고 있다면, 예측 불가능한 문제를 불러온다.
이러한 문제를 해결하기 위해서, 방어적 복사는 객체의 내부 상태를 복사하는 대신, 내부 상태를 복사하는 새로운 객체를 생성하고, 원래 객체의 내부 상태를 그대로 복사한 후에 새로운 객체를 반환한다.
- clone 메서드는 Object 클래스에 정의되어 있으며, 모든 객체에서 호출할 수 있다. 이에 비해 생성자는 해당 클래스에서만 호출할 수 있다.
- clone 메서드는 객체의 내부 상태를 복사하는 기능을 제공한다. 이에 비해 생성자는 객체의 내부 상태를 직접 복사해야 한다.
- clone 메서드는 자식 클래스에서 오버라이드하여 사용할 수 있다. 이를 이용해 자식 클래스에서도 부모 클래스의 clone 메서드를 호출하여 부모 클래스와 같은 방식으로 방어적 복사가 가능하다.
📌 Summary
1️⃣ Clonealbe 인터페이스를 구현하는 모든 클래스는 clone을 재정의 해야한다.
2️⃣ 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다.
3️⃣ 이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다.
- 객체 내부 '깊은 구조'에 숨어 있는 모든 가변 객체를 복사하고, 복제본이 가진 객체 참조 모두가 복사된 객체를 가리키게 해야 한다.
- 주로 재귀적 호출을 사용하지만, 이 방식이 항상 최선은 아니다.
4️⃣ 기본 타입 필드와 불변 객체 참조만 갖는 클래스라면 아무 필드도 수정할 필요가 없다.
- 단, 일련번호나 고유 ID는 비록 기본 타입이나 불변일지라도 수정해주어야 한다.