아이템13. clone 재정의는 주의해서 진행하라
2021-07-04 00:00:00 # Effective_Java
  • 널리 쓰이는 Cloneable 방식에 대해 clone 메서드를 잘 동작하게끔 해주는 구현 방법
  • 언제 그렇게 해야 하는지 알려주고,
  • 가능한 다른 선택지는 무엇이 있는지

Cloneable의 문제점

  • clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, protected로 선언되어 있다.
  • 그래서 Cloneable을 구현하는 것만으로 외부 객체에서 clone 메서드를 호출할 수 없다.
  • 리플렉션을 사용한다고 하더라도 해당 객체가 접근이 허용된 clone 메서드를 제공한다는 보장이 없다.

Cloneable의 역할

  • Object의 protected 메서드인 clone의 동작 방식을 결정한다.

  • Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며,
    그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다. (이례적으로 인터페이스를 사용한 것이니 따라하지는 말기)

  • 실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대한다.

  • 생성자를 호출하지 않고도 객체를 생성하게 되는 모순적인 메커니즘을 만든다.

clone 메서드의 (허술한) 일반 규약

이 객체의 복사본을 생성해 반환한다. ‘복사’의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음 식은 참이다.

x.clone() != x

또한 다음 식도 참이다.

x.clone().getClass() == x.getClass();

하지만 이상의 요구를 반드시 만족해야 하는 것은 아니다.
한편 다음 식도 일반적으로 참이지만, 역시 필수는 아니다.

x.clone().equals(x)

관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와 (Object를 제외한) 모든 상위 클래스가 이 관례를 따른다면 다음 식은 참이다.

x.clone().getClass() == x.getClass()

관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.

  • clone 메서드가 super.clone이 아닌, 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 불평하지(차별하지) 않는다.
  • 하지만 이 클래스의 하위 클래스에서 super.clone을 호출한다면 잘못된 클래스의 객체가 만들어져,
    결국 하위 클래스의 clone 메서드가 제대로 동작하지 않게 된다.
    • clone을 재정의한 클래스가 final이라면 걱정해야 할 하위 클래스가 없으니 이 관례는 무시해도 안전하다.
    • 하지만 final 클래스의 clone 메서드가 super.clone을 호출하지 않는다면 Cloneable을 구현할 이유도 없다.
    • Object의 clone 구현의 동작 방식에 기댈 필요가 없으니까!

제대로 동작하는 clone 메서드를 가진 상위 클래스를 상속해 Cloneable을 구현하려면…

  1. super.clone을 호출한다. (원본의 완벽한 복제본)
  2. 반환하려고 하는 객체의 타입으로 형변환하여 확실히 한다. (재정의한 메서드의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입일 수 있다.)
  3. clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
  4. final로 선언된 필드에 대해서는 재정의가 불가능하기 때문에 Cloneable 아키텍처는 ‘가변 객체를 참조하는 필드는 final로 선언하라’는 일반 용법과 충돌한다.
  5. public인 clone 메서드에서는 throws 절을 없애야 한다.
  6. 상속해서 쓰기 위한 상속용 클래스에서는 어떤 상속 방식이든 Cloneable을 구현해서는 안 된다.
  7. Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드도 적절히 동기화해줘야 한다. [아이템 78]

복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드 (코드 13-4,5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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() {
Entry result = 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;
}
}
@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] = buckets[i].deepCopy(); // deepCopy()를 지원하는 buckets
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// 나머지 생략
}

요약

  • Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다.
  • 이때 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다. (형변환)
  • 이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다.
  • 일반적으로 이 말은 그 객체의 내부 ‘깊은 구조’에 숨어 있는 모든 가변 객체를 복사하고, 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 함을 뜻한다. (원래 객체를 가리키는 것이 아니라)
    • 내부 복사는 주로 clone을 재귀적으로 호출해 구현하지만, 항상 최선의 방법은 아니다.
  • 기본 타입 필드와 불변 객체 참조만 갖는 클래스라면 아무 필드도 수정할 필요가 없다.
  • 단, 일련번호나 고유 ID는 비록 기본 타입이나 불변일지라도 수정해줘야겠지?

Cloneable을 구현한 클래스가 아니라면?

  • 복사 생성자와 복사 팩터리 방식 (변환 생성자, 변환 팩터리) [아이템 1]
  • Cloneable 만큼 모순적이지도 않고, 해당 클래스가 구현한 ‘인터페이스’ 타입의 인스턴스를 인수로 받을 수도 있다.
  • 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다.
    • 예) HashSet 객체 s를 TreeSet 타입으로 복제할 수 있다.
    • clone으로는 불가능!!, 변환 생성자는 new TreeSet<>(s)으로 간단히 처리 가능

핵심 정리

  • 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 된다. 새로운 클래스도 이를 구현해서는 안 된다.
  • final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별 문제가 없을 때만 드물게 허용해라
  • 기본 원칙은 복제 기능은 생성자와 팩터리를 이용하는 게 최고라는 것이다.
  • 단, 배열 만큼은 clone 방식이 가장 깔끔한, 합당한 예외다.