clone 재정의는 주의해서 진행하라
Cloneable 인터페이스
Object의 protected 메서드인 clone의 동작 방식을 결정한다. Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
Cloneable 인터페이스의 문제
Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 아쉽게도 의도한 목적을 제대로 이루지 못 했다. 가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 그마저도 protected라는 데 있다. 그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다. 리플렉션을 사용하면 가능하지만, 100% 성공하지 않는다. 해당 객체가 접근이 허용된 clone 메서드를 제공한다는 보장이 없기 때문이다. 그래서 clone 메서드의 구현 방법을 알아야 한다.
Object 명세의 clone 규약
이 객체의 복사본을 생성해 반환한다. ‘복사’의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다.
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
위의 조건이 반드시 true를 만족해야 하는 것은 아니다.
관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.
강제성이 없다는 점만 빼면 생성자 연쇄와 살짝 비슷한 메커니즘이다.
클래스 B가 클래스 A를 상속할 때, 하위 클래스인 B의 clone은 B 타입 객체를 반환해야 한다. 그런데 A의 clone이 자신의 생성자, 즉 new A(…)로 생성한 객체를 반환한다면 B의 clone도 A 타입 객체를 반환할 수밖에 없다. 달리 말해 super.clone을 연쇄적으로 호출하도록 구현해두면 clone이 처음 호출된 하위 클래스의 객체가 만들어진다.
가변객체를 참조하지 않는 클래스의 Cloneable 구현
super.clone을 호출하여 원본의 복제본을 얻는다. 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 더 이상 손볼 것이 없다. 그런데 쓸데없는 복사를 지양한다는 관점에서 보면 불변 클래스는 굳이 clone 메서드를 제공하지 않는게 좋다.
@Override
public final class PhoneNumber implements Cloneable {
private final short areaCode, prefix, lineNum;
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch(CloneNotSupportedException e) {
// 일어나지 않지만 clone메서드가 검사 예외이므로 구현해야함.
throw new AssertionError();
}
}
}
Object의 clone메서드는 Object를 반환하지만 PhoneNumber의 clone 메서드는 PhoneNumber를 반환하게 했다. 자바가 공변 반환 타이핑을 지원하니 이렇게 하는 것이 가능하고 권장하는 방식이기도 하다.
- 공변 반환 타이핑 : 재정의한 메서드의 반환 타입을 상위 클래스의 메서드가 반환하는 타입의 하위 타입으로 정의
가변 객체를 참조하는 클래스의 Cloneable 구현
방법1
public class Stack implements Cloneable {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
...
}
clone 메서드가 단순히 super.clone의 결과를 그대로 반환한다면, 반환된 Stack 인스턴스의 size 필드는 올바른 값을 갖겠지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다.
Stack 클래스의 하나뿐인 생성자를 호출한다면 이러한 상황은 절대 일어나지 않는다. clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다. 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출해주는 것이다.
elements.clone의 결과를 Object[]로 형변환할 필요는 없다. 배열의 clone은 런타임 타입과 컴파일타임 타입 모두가 원본 배열과 똑같은 배열을 반환한다. 따라서 배열을 복제할 때는 배열의 clone 메서드를 사용하기 권장한다.
방법2
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
// 연결 리스트를 재귀적으로 복사
Entry deepCopy() {
return new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
}
// 잘못된 clone - 가변 상태를 공유한다.
// @Override
// public HashTable clone() {
// try {
// HashTable result = (HashTable) super.clone();
// result.buckets = buckets.clone();
// return result;
// } catch (CloneNotSupportedException e) {
// throw new AssertionError();
// }
// }
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++) {
if(buckets[i] != null)
result.buckets[i].deepCopy;
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
...
}
Stack 클래스와 똑같이 구현하게 되면 복제본은 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다. 이를 해결하기 위해 적절한 크기의 새로운 버킷 배열을 할당한 다음 원래의 버킷 배열을 순회하며 비지 않은 각 버킷에 대해 깊은복사를 수행한다.
방법3
super.clone을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한 다음, 원본 객체의 상태를 다시 생성하는 고수준 메서드를 호출한다. HashTable 예에서라면, buckets 필드를 새로운 버킷 배열로 초기화한 다음 원본 테이블에 담긴 모든 키-값 쌍 각각에 대해 복제본 테이블의 put(key, value) 메서드를 호출해 둘의 내용이 똑같게 해주면 된다.
고수준 API를 활용해 복제하면 간단하지만, 저수준에서 바로 처리할 때보다는 느리다. 또한 Cloneable 아키텍처의 기초가 되는 필드 단위 객체 복사를 우회하기 때문에 전체 Cloneable 아키텍처와는 어울리지 않는 방식이기도 하다.
clone 재정의 시 주의사항
재정의될 수 있는 메서드를 호출하지 않아야 한다.
생성자에서와 같이 clone 메서드도 마찬가지다. 만약 clone이 하위 클래스에서 재정의한 메서드를 호출하면, 하위 클래스는 복제 과정에서 자신의 상태를 교정할 기회를 잃게 되어 원본과 복제본의 상태가 달라질 가능성이 크다.
public인 clone 메서드에서는 throws 절을 없애야 한다.
검사 예외를 던지지 않아야 메서드를 사용하기 편하기 때문이다.
상속용 클래스는 Cloneable을 구현해서는 안 된다.
Cloneable 구현 여부를 하위 클래스에서 선택하도록 해준다. 다른 방법으로는, clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의하지 못하게 할 수도 있다.
스레드 안전 클래스를 작성할 때는 clone 메서드 역시 동기화해야 한다.
Object의 clone 메서드는 동기화를 신경쓰지 않았다. 그래서 super.clone 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화해줘야 한다.
복사 생성자와 복사 팩터리
Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 하지만 그렇지 않은 상황에서는 복사 생성자와 복사 팩터리 방식을 사용하자.
// 복사 생성자
public Yum(Yum yum) { ... };
// 복사 팩터리
public static Yum newInstance(Yum yum) { ... };
새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다. 복제 기능은 생성자와 팩터리를 이용하는게 최고다. 단, 배열만은 clone 메서드 방식이 가장 깔끔하다.
Comments