6 minute read

item13 clone 재정의는 주의하여 진행하라

  • Cloneable은 복제해도 되는 인터페이스 임을 명시하는 용도의 믹스인 인터페이스이지만, 인터페이스 구현만으로는 외부에서 clone 메서드를 호출 할 수 없다.
    • clone메서드가 선언된 곳이 Cloneable이 아닌 Object이고, protected로 선언되어 있다.

Cloneable 인터페이스의 용도

  • Objectprotected 메서드인 clone의 동작 방식을 결정한다.
  • Cloneable 을 구현한 클래스의 인터페이스에서 clone을 호출하면, 그 객체의 필드들을 하나하나 복사한 객체를 반환한다.
  • Cloneable 을 구현하지 않은 클래스에서 clone을 호출하는 경우, CloneNotSupportedException을 던진다.

clone메서드의 규약

/**  
 * Creates and returns a copy of this object.  The precise meaning  
 * of "copy" may depend on the class of the object. The general  
 * intent is that, for any object {@code x}, the expression:  
 * <blockquote>  
 * <pre>  
 * x.clone() != x</pre></blockquote>  
 * will be true, and that the expression:  
 * <blockquote>  
 * <pre>  
 * x.clone().getClass() == x.getClass()</pre></blockquote>  
 * will be {@code true}, but these are not absolute requirements.  
 * While it is typically the case that:  
 * <blockquote>  
 * <pre>  
 * x.clone().equals(x)</pre></blockquote>  
 * will be {@code true}, this is not an absolute requirement.  
 * <p>  
 * By convention, the returned object should be obtained by calling  
 * {@code super.clone}.  If a class and all of its superclasses (except  
 * {@code Object}) obey this convention, it will be the case that  
 * {@code x.clone().getClass() == x.getClass()}.  
 * <p>  
 * By convention, the object returned by this method should be independent  
 * of this object (which is being cloned).  To achieve this independence,  
 * it may be necessary to modify one or more fields of the object returned  
 * by {@code super.clone} before returning it.  Typically, this means  
 * copying any mutable objects that comprise the internal "deep structure"  
 * of the object being cloned and replacing the references to these  
 * objects with references to the copies.  If a class contains only  
 * primitive fields or references to immutable objects, then it is usually  
 * the case that no fields in the object returned by {@code super.clone}  
 * need to be modified.  
 * <p>  
 * The method {@code clone} for class {@code Object} performs a  
 * specific cloning operation. First, if the class of this object does  
 * not implement the interface {@code Cloneable}, then a  
 * {@code CloneNotSupportedException} is thrown. Note that all arrays  
 * are considered to implement the interface {@code Cloneable} and that  
 * the return type of the {@code clone} method of an array type {@code T[]}  
 * is {@code T[]} where T is any reference or primitive type.  
 * Otherwise, this method creates a new instance of the class of this  
 * object and initializes all its fields with exactly the contents of  
 * the corresponding fields of this object, as if by assignment; the  
 * contents of the fields are not themselves cloned. Thus, this method  
 * performs a "shallow copy" of this object, not a "deep copy" operation.  
 * <p>  
 * The class {@code Object} does not itself implement the interface  
 * {@code Cloneable}, so calling the {@code clone} method on an object  
 * whose class is {@code Object} will result in throwing an  
 * exception at run time.  
 *  
 * @return     a clone of this instance.  
 * @throws  CloneNotSupportedException  if the object's class does not  
 *               support the {@code Cloneable} interface. Subclasses  
 *               that override the {@code clone} method can also  
 *               throw this exception to indicate that an instance cannot  
 *               be cloned.  
 * @see java.lang.Cloneable  
 */  
@HotSpotIntrinsicCandidate  
protected native Object clone() throws CloneNotSupportedException;

clone() 메서드는 현재 객체의 복사본을 생성하고 반환한다. “복사본”의 정확한 의미는 객체의 클래스에 따라 다를 수 있지만 일반적인 의도는 다음과 같다.


어떤 객체 x에 대해 다음 식이 true이어야 한다:

  • x.clone() != x
    또한 다음 식도 true이어야 하지만 엄격한 요구 사항은 아니다.
  • x.clone().getClass() == x.getClass()
    일반적으로는 다음 식이 true일 것이나, 엄격한 요구 사항은 아니다.
  • x.clone().equals(x)
    관례적으로, 이 메서드에서 반환된 객체는 super.clone을 호출하여 얻어져야 한다. 만약 어떤 클래스와 그 슈퍼클래스(단, Object 제외)가 이 관례를 따른다면, x.clone().getClass() == x.getClass()이 될 것이다.

또, 이 메서드에서 반환된 객체는 복제 대상 객체와 독립적이어야 한다.

이 독립성을 달성하기 위해, super.clone()을 통해 반환된 객체의 내부 가변 객체를 복사하고 이러한 객체에 대한 참조를 해당 복사본에 대한 참조로 대체해야 할 수도 있다.

재정의 시 주의사항

기본적인 재정의

class PhoneNumber implements Cloneable {  
  @Override  
  public PhoneNumber clone() {  
    try {  
      return (PhoneNumber) super.clone();  
    } catch(ClassNotSupportedException e) {  
       ...
    }  
  }  
}
  • super.clone()을 실행하면 PhoneNumber에 대한 완벽한 복제가 이루어진다.
  • super.clone()의 리턴 타입은 Object이지만, 자바의 공변 반환타이핑 기능을 통해 PhoneNumber 하위 타입으로 캐스팅하여 리턴하는 것이 가능하다. (사용 권장)
  • try-catch : super.clone() 메서드에서 ClassNotSupportedException이라는 checked exception을 리턴한다.
    • 하지만 PhoneNumberCloneable을 구현하기 떄문에 절대 실패하지 않는다. 따라서 이부분은 RuntimeException으로 처리하거나, 아무것도 설정하지 않아야 한다.

가변 객체 참조

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];
    }

    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);
        }
    }

    @Override
    public Stack clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
  • Stack 클래스에서 단순히 clone메서드를 사용해 super.clone()만 실행하게 된다면, new Stack()을 통해 새로운 객체가 생성되고 필드모두 원본 객체와 동일하게 초기화가 될 것이다.
  • 하지만, Object의 clone 기본규약에는 Deep copy가 아닌 Shallow Copy를 이용해 초기화를 진행하도록 되어 있으며 배열과 같은 가변필드는 원본 필드와 객체를 공유하게 된다.

Clone메서드는 사실상 생성자와 같은 효과를 낸다.

  • clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
    • 제대로 복제하기 위해서는, elements 배열을 Deep copy로 복사해 만들어줘야 한다.
    • 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출하는 것이다.
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];
    }

    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);
        }
    }

    @Override
    public Stack clone() {
        try {
            Stack clone = (Stack) super.clone();
            // 배열의 clone은 런타임 타입과 컴파일 타입 모두가 원본 배열과 같은 배열을 반환한다.
	        // 따라서 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다.
	        // 배열은 clone 기능을 제대로 사용하는 유일한 예이다.
            clone.elements = elements.clone();
            return clone;
        } catch(CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

문제점

  • Cloneable 아키텍처는 가변 객체를 참조하는 필드는 final로 선언하라 는 일반 용법과 충돌한다.
    • 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다는 말이다.
  • 또한, 배열은 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다.
    • 이를 해결하기 위해서는 각 버킷을 구성하는 연결 리스트를 복사 해야 한다.
    • 연결리스트 전체를 복사하려는 경우 재귀적으로 호출하게 된다.
    • 재귀 호출은 리스트의 원소 수만큼 스택 프레임을 소비하게 되기 때문에 스택 오버플로우를 일으킬 위험이 있다.
    • 문제를 피하기 위해서는 재귀 호출 대신에 반복문을 사용하여 순회하는 방향으로 수정해야 한다.

복잡한 가변 객체를 복제

  • super.clone() 을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정하고, 원본 객체의 상태를 다시 생성하는 고수준 메서드들을 호출한다.
    • 이 방식은 저수준에서 바로 처리할 때보다는 느리며, Cloneable 아키텍처의 기초가 되는 필드 단위 객체 복사를 우회하기 때문에 전체 Cloneable 아키텍처와는 어울리지 않는 방식이다.
  • [주의]
    • 생성자에서는 재정의될 수 있는 메서드를 호출하지 않아야 하는 것처럼, clone() 메서드도 마찬가지이다.
    • Object의 clone() 메서드는 CloneNotSupportedException을 던진다고 선언되어 있지만, 재정의한 메서드는 수정해야 한다.
    • public clone 메서드에서는 throws 절을 없애거나, RuntimeException으로 throw 한다.
    • 하위 클래스에서 clone이 동작하지 않게 만들수도 있다.
      @Override  
      protected final Object clone() throws CloneNotSupportedException {  
      throw new CloneNotSupportedException();  
      }
      

스레드 동기화

  • 스레드 안정성을 고려한다면, clone 메서드에 대해 적절히 동기화 처리가 필요하다.
  • super.clone() 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화 해줘야 한다.
    • 여러 스레드가 동시에 clone을 호출할 때 예측할 수 없는 결과가 발생할 수 있다.

복사 생성자와 복사 팩터리 메서드

Cloneable을 이미 구현한 클래스를 확장한다면 clone() 메서드를 잘 작동하도록 구현해야 하나, 그렇지 않은 상황에서 사용할 수 있는 방식이다. 이 방식은 Cloneable/clone 방식보다 더 나은 옵션을 제공한다. 이 방식은 변환 생성자 혹은 변환 팩터리라고도 한다. (Conversion constructor / factory)

public Yum(Yum yum) {}

public static Yum newInstance(Yum yum) {}
  • 언어 모순적이고 위험한 객체 생성 메커니즘을 사용하지 않는다. (super.clone())
  • clone 규약에 기대지 않는다.
  • final 필드 용법과 충돌하지 않는다.
  • 불필요한 예외 처리가 필요없다.
  • 형변환이 필요없다.
  • 복사 생성자와 복사 팩터리는 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.

Leave a comment