본문 바로가기

reviews/Effective JAVA

013. clone 재정의는 주의해서 진행하라

Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스(mixin interface)이다.

인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위이지만, Cloneable의 경우에는 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한다.

clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 그마저도 protected이다.

그래서 Cloneable을 구현하는 것만으로는 외부 객체에서 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을 구현할 이유가 없다.
  • 불변 클래스는 굳이 clone 메서드를 제공하지 않는 것이 쓸데없는 복사를 지양할 수 있다.

제대로 구현하는 방법

  • 제대로 동작하는 clone 메서드를 가진 상위 클래스를 상속해 super.clone을 호출하는 Cloneable을 구현한다.

▼ 가변 상태를 참조하지 않는 클래스용 clone 메서드

class PhoneNumber implements Cloneable {

    @Override 
    public PhoneNumber clone() { 
        try {
            return (PhoneNumber) super.clone(); 
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(); // 일어날 수 없는 일이다. 
        }
...
}

▼ 가변 객체를 참조하는 클래스

public class Stack {
    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); 
    }
}

# 가변 객체를 참조하는 클래스용 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은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.

⇒ elements 배열의 clone을 재귀적으로 호출해 복사한다.

elements 필드가 final이었다면 final 필드는 새로운 값을 할당할 수 없으므로 앞선 방식이 작동하지 않는다.

⇒ 복제할 수 있는 클래스를 만들기 위해 일부 필드에서 final 한정자를 제거해야 할 수도 있다.

(가변 객체를 참조하는 필드는 final로 선언하라는 일반 용법과 충돌한다.)

배열을 복제할 때는 배열의 clone 메서드 사용을 권장한다.

반응형