item13 clone 메서드를 신중하게 오버라이드하라

Cloneable 인터페이스는 클론이 가능하다는 것을 알리는(advertize) 클래스들을 위해 mixin인터페이스로 의도되었다. 안타깝게도 위의 목표는 실패했다.

  • 주요한 결함은 clone 메서드의 부재이다.
  • Object의 clone 메서드는 protected 이다.
  • 단순히 Cloneable 을 구현 했다고 해서, reflection을 사용하지 않고는, clone 메서드를 실행할 수 없다.
  • 해당 오브젝트가 clone메서드에 접근 권한이 있을지 없을지 모르기에, 심지어 reflection 을 사용한 것도 실패할 수 있다.
  • 이런 결함들에도 불고하고, clone 메서드는 합리적으로 여러곳에 사용되고 있어서 이해하는데 도움이 된다.

Cloneable 이 하는 일이 뭔가?

  • 아무런 메서드를 가지고 있지 않다.
  • Object 클래스의 protected 메서드인 clone의 구현시의 동작을 결정한다.
  • Cloneable 을 구현하면 Object 클래스의 clone 메서드는 오브젝트의 각필드에 대한 복사본을 리턴한다.
  • Cloneable 없이 clone을 호출하면 CloneNotSupportException 익셉션을 발생시킨다.
  • 보통 인터페이스는 클래스가 무엇을 해줄 수 있는 가에 대해 알려주지만, 이번 경우에는 인터페이스가 서브클래스에 있는 오버라이드한 메서드의 동작을 바꾼다.

스펙은 제대로 안되어 있지만, 컨벤션은 있다.

  • Cloneable을 구현한 클래스는 적절히 동작하는 clone 메서드를 제공할 것으로 예상된다.
  • 위의 목표를 달성하기 위해서는 복잡하고, 강제할수 없고, 문서화 되지 않은 절차를 따라야 한다.
  • 결과물의 메커니즘은 부서지기 쉽고, 위험하고, 극단적인 것이 되었다.
  • 생성자를 실행하지 않고 오브젝트를 만들어낸다.
In [1]:
class CloneTest implements Cloneable {
    public void echo(){
        System.out.println("ping");
    }
    
    public Object clone() {
        // Object의 clone은 CloneNotSupportedException을 발생시키기 때문에 try catch 가 필요하다. 
        try{
            return (CloneTest)super.clone();
        } catch(CloneNotSupportedException e) {
            System.out.println("CloneNotSupportedException comes out : "+e.getMessage());
            return null;
        }
        
    }
}
In [2]:
CloneTest x = new CloneTest();

// 컨벤션 1
x.clone() != x;
Out[2]:
true

clone의 일반적인 사용은 위의 코드와 같다.

아래의 코드또한 true 이다.

In [21]:
// 컨벤션 2
x.clone().getClass() == x.getClass()
Out[21]:
true

오브젝트가 clone메서드에 의해서 리턴될때 super.clone() 을 포함 하는 것이 관례이다.

해당 클래스와 그 클래스의 슈퍼클래스가 위의 컨벤션을 준수한다면 아래의 코드도 성립한다.

In [25]:
// 컨벤션 3 
// 책에는 true라고 되어 있는데, equals를 제대로 안만들어서 그런것 같음
x.clone().equals(x)
Out[25]:
false
  • 컨벤션에 의하면, 리턴된 오브젝트와 클론당한 오브젝트는 독립적이어야 한다.
  • 독립성을 유지하기 위해서 super.clone()에 의해 리턴된 하나이상의 필드를 수정할 필요가 있다.
  • 이런 메커니즘은 (강제하지 않는다는 것만 빼면) 생성자 체이닝과 묘하게 닮았다.
    • clone메서드에서 super.clone을 빼먹고 호출해도 컴파일러는 침묵한다.
    • 그렇지만, final 클래스의 clone은 호출할 수 없다. (final인데 clone 호출되면 final 아니니까)

클래스의 멤버로 프리미티브 타입과 불변 객체의 레퍼런스(final)만 가지고 있는 경우

  • 부모 클래스의 clone()를 캐스팅 하면 된다.

그러나 불변 클래스는 clone 메서드를 제공해서는 안된다.

* 그냥 그건 자원 낭비다. 
* 아이템10에 나왔던 PhoneNumber 클래스에 clone을 적용시켜 보자. 
In [13]:
public final class PhoneNumber {
    private final short areaCode, prefix, lineNum;
    
    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "area code");
        this.prefix = rangeCheck(prefix, 999, "prefix");
        this.lineNum = rangeCheck(lineNum, 9999, "line num");
        
    }
    private static short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max) 
            throw new IllegalArgumentException(arg + " : " + val);
        return (short) val;
    }

    @Override public boolean equals(Object o) {
        if (o == this) 
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
    }

    // 여기까지 10장

    // 여기부터 13장
    @Override public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch(CloneNotSupportedException e) {
            throw new AssertionError(); // Can't happen
        }
    }
}
  • PhoneNumber에 clone() 메서드를 쓰고 싶으면, PhoneNumber는 final 을 바꾸고 Cloneable을 구현(implement) 해야한다.
  • Object 클래스의 clone은 Object를 리턴하고, PhoneNumber의 clone은 PhoneNumber를 리턴한다.
  • 위의 동작은 바람직한 것이다. 왜냐면, 자바는 공변 리턴 타입(covariant return types)을 제공하기 때문이다.
    • 이게 무슨 말이냐면, 오버라이딩한 메서드의 리턴타입은 서브 클래스의 리턴타입이 되어도 된다는 말이다.
    • 그러니까 super.clone() 를 실행하고 나서 반드시 캐스팅을 한 오브젝트를 리턴해줘라.
In [15]:
PhoneNumber pn = new PhoneNumber(1,1, 1);

pn.clone() == pn;
---------------------------------------------------------------------------
java.lang.AssertionError
	at PhoneNumber.clone(#21:32)
	at .(#26:1)

보시다시피 에러가 난다.

In [26]:
// Cloneable 구현한 버전
public class PhoneNumber implements Cloneable {
    private final short areaCode, prefix, lineNum;
    
    public PhoneNumber(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "area code");
        this.prefix = rangeCheck(prefix, 999, "prefix");
        this.lineNum = rangeCheck(lineNum, 9999, "line num");
        
    }
    private static short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max) 
            throw new IllegalArgumentException(arg + " : " + val);
        return (short) val;
    }

    @Override public boolean equals(Object o) {
        if (o == this) 
            return true;
        if (!(o instanceof PhoneNumber))
            return false;
        PhoneNumber pn = (PhoneNumber) o;
        return pn.lineNum == lineNum && pn.prefix == prefix && pn.areaCode == areaCode;
    }

    // 여기까지 10장

    // 여기부터 13장
    @Override public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch(CloneNotSupportedException e) {
            throw new AssertionError(); // Can't happen
        }
    }
}

PhoneNumber pn = new PhoneNumber(1,1, 1);

위에서 이야기 했던 컨벤션 3가지가 잘 적용되는지 테스트 해보자.

In [27]:
pn.clone() != pn;
Out[27]:
true
In [19]:
pn.clone().getClass() == pn.getClass();
Out[19]:
true
In [24]:
pn.clone().equals(pn)
Out[24]:
true

mutable 오브젝트를 필드로 가지고 있는 클래스에 clone 메서드를 구현하는 것을 해보자. item7 에 나왔던 Stack 클래스를 써보도록 하자.

In [3]:
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();
        Object result =  elements[--size];
        elements[size] = null;
        return result;
    }
    
    // 메모리가 더 필요하면 걍 두배로 늘림
    private void ensureCapacity() {
        if (elements.length == size) 
            elements = Arrays.copyOf(elements, 2 * size + 1);
    }
    
    // 레퍼런스(상태가 변하는)를 가지고 있는 클래스의 clone 메서드
    @Override public Stack clone() {
        try {
            Stack result = (Stack) super.clone();
            result.elements = elements.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}
  • elements.clone() 을 다시 Object[] 로 캐스팅 하지 않아도 된다. 컴파일시에 적절한 타입으로 바뀐다.
  • [주의] elements 필드가 final 이면 clone을 사용할 수 없다. 왜냐면 필드에 새로운 값을 할 당할 수 없으니까
  • Cloneable 의 아키텍쳐는 mutable 오브젝트의 final 필드를 참조하는 것과는 호환되지 않는다.
In [3]:
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 netx) {
            this.key = key;
            this.value = value;
            this.next =next;
        }
    }
    
    // 깨진 clone 메서드 - 변하는 상태를 공유함
    @Override public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.buckets = buckets.clone();
            return result;
        } catch(CloneNotSupprtedException e) {
            throw new AssertionError();
        }
    }
}
  • 클론은 그 자신의 bucket 배열을 가지고 있긴 하지만, 배열의 레퍼런스는 오리지날과 같다.
  • 이 문제를 수정하려면 각 버킷의 링크드 리스트를 카피해야한다.
In [ ]:
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 netx) {
            this.key = key;
            this.value = value;
            this.next =next;
        }
    }
    
    // 엔트리를 재귀적으로 카피
    Entry deepCopy() {
        return new Entry(key, value, next == null ? null : next.deepCopy());
    }
    

    @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();
            return result;
        } catch(CloneNotSupprtedException e) {
            throw new AssertionError();
        }
    }
}
  • 위의 방법은 버킷의 크기가 크지 않은경우에는 좋지만, 큰 경우 링크드리스트를 클론하는 것은 좋은 방법이 아니다.
  • 리스트가 긴경우에 위의 방법을 사용하면 스택 오버플로우가 나기 쉽다.
  • deepCopy 의 재귀 실행중 스택 오버 플로우가 나는 것을 방지하려면 아래와 같이 해야한다.
In [ ]:
// Entry 를 반복문으로 카피
Entry deepCopy() {
    Enter 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;
}

다른 접근법 (마지막)

  • 복잡하고 변하는 오브젝트를 복제하는 마지막 접근 법은 super.clone을 실행하고, 결과물 오브젝트에 모든 초기값을 세팅하고, 상위레벨의 메서드를 호출해서 원본의 상태를 다시 만드는것이다.
  • 해쉬테이블의 예를 들어본다면, bucket 필드는 새로운 bucket array로 초기화되어야한다.
  • 그리고 put(key, value) 메서드(안보임)를 호출해서 모든 값을 새로운 해쉬테이블에 넣어주어야한다.
  • 이런 접근법은 직접적으로 내부의 clone을 실행해서 값을 직접 할당하는 것 보다는 느리지만, 일반적이고 간단하며, 합리적이고 우아하게 clone 메서드를 대신한다.
  • 위의 방법이 깔끔하긴하지만, 무턱대고 오브젝트의 필드와 필드를 카피하기때문에, Cloneable 아키텍쳐와는 정반대의 입장이긴 하다.

clone 메서드 실행시 오버라이드된 메서드를 호출하지 말라

  • 생성자와 마찬가지로 clone 메서드는 clone으로 생성중일때 오버라이드 가능한 메서드를 절대로 호출해서는 안된다.
  • clone 실행시에 서브클래스에서 오라이드된 메서드를 부를 수 있다면, 서브클래스에서 clone 하는 시점에서 원본과 다른 것을 만들어 낼 수 있다.
  • 그러므로 앞에 나온 put(key, value) 메서드는 final 이거나 private 이어야 한다.
  • Object의 clone 메서드는 CloneNotSupportedException을 던지도록 선언되어 있지만, 오버라이드 하는 메서드는 그럴 필요가 없다.
  • public clone 메서드는 throws 절을 무시해야한다. checked exception은 던지지 않도록 하는 것이 사용하기도 편하다. (Item 71)
  • 클래스 상속을 디자인 할 때에는 2가지 선택이 있다.
    • 한가지는 Cloneable 을 구현하지 않는것이다.
    • Object에 있는 protected clone 메서드를 적절히 구현하는 방법도 있다.

clone 메서드를 구현하지 않기로 했다면 서브클래스에서 사용하지 못하도록 막을 수도 있다.

In [ ]:
// Cloneable 을 지원 하지 않음
@Override
protected final Object clone() throws CloneNotSupportedException { 
    throw new CloneNotSupportedException();
}

clone메서드는 thread-safe 하지 않다.

  • Cloneable 을 스레드 안정하게 하려면, clone 메서드는 다른 메서드들과 적절히 동기화되어야 한다.
  • Object의 clone 메서드는 동기화 되지 않는다.
  • super.clone() 을 리턴하는 동기화된 clone() 메서드를 작성해야한다.

정리

  • 모든 Cloneable을 구현하는 메서드는 public이고 클래스 그 자체를 리턴하는 clone 메서드를 오버라이드 해야한다.
  • 해당 메서드는 먼저 super.clone 을 호출 해야 한다.
  • 다른 필드들을 수정해야하는 경우는 수정해야한다.
  • 레퍼런스들은 deep copy 해야한다.
  • 레퍼런스 카피가 재귀적으로 호출되는 것은 항상 좋은 방법은 아니다. (사이즈가 작은경우만 괜찮음)
  • 만약에 클래스가 기본타입형과 불변 객체만 가지고 있으면 따로 필드를 수정할 필요는 없다.

이렇게 복잡한게 진짜 필요한가?

  • 드물지
  • 이미 Cloneable을 구현한 클래스를 상속 반는 경우에는 선택지가 별로 없다.
  • 잘 동작하는 clone 메서드를 구현해야한다.
  • 아니면, 다른 대안으로 객체를 복사하는 방법을 마련해야 한다.
  • 오브젝트를 복사하는 더 나은 접근법은 복제 생성자나 복제 팩토리를 제공하는 것이다.

복사 생성자는 아래와 같이 생긴 그 자신의 타입을 파라메터로 받는 생성자 이다.

In [ ]:
// 복제 생성자
public Yum(Yum yum) {...};

복제 팩토리는 복제 생성자의 아날로그 방식 static 팩토리 메서드이다.

In [ ]:
// 복제 팩토리
public static Yum newInstance(Yum yum) {...};

복제 생성자, 팩토리 메서드는 Cloneable/clone 보다 많은 장점을 가지고 있다.

  • 위험에 대비한 과도한 객체 생성 메커니즘에 의존하지 않는다.
  • 강제할수없고 문서화되지 않은 것을 쓰지 않아도 된다. (Cloneable의 특징이다...)
  • final 필드들과 충돌이 없다.
  • 불필요한 checked 익셉션을 던지지 않는다.
  • 캐스팅이 필요가 없다.
  • 해당 클래스에 구현된 타입을 인자로 받을 수 있다.
  • 이러한 생성자와 팩토리를 제공하는 일반적인 타입으로 Collection 혹은 Map 이있다.
  • 인터페이스 기반의 복사 생성자와 팩토리는 (변환 생성자, 변환 팩토리라고 부름)
  • 클라이언트에서 원본의 타입을 강제하기 보다는 선택할 수 있도록 한다.
    • HashSet을 TreeSet으로 변환가능
  • Cloneable과 관련된 모든 문제를 고려할 때, 새로운 인터페이스는 그것을 확장하지 않아야 하며, 새로운 확장 가능 클래스는 그것을 구현하지 않아야 한다.
  • 반면 구현해도 되는 드문경우로는, final 클래스에 구현하는 것은 성능 최적화 관점에서 덜 해롭다.