본문 바로가기

공부 기록/Java

[이펙티브 자바 3/E] 아이템13 - clone 재정의는 주의해서 진행하라

- Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.

- Cloneable 인터페이스는 놀랍게도 Object의 protected 메서드인 clone의 동작 방식을 결정한다. => 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한 것이다.


- 가변 객체를 참조하는 클래스를 복제할 수 있도록 만들어보자.

public class Stack {
    private Object[] elements;
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
    
    ...
    
}

- clone 메서드가 단순히 super.clone의 결과를 그대로 반환한다면 어떻게 될까? 반환된 Stack 인스턴스의 size 필드는 올바른 값을 갖겠지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다. 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다는 이야기다. => 프로그램이 이상하게 동작하거나 NullPointerException을 던질 것이다.

 

- clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.


- Stack의 clone 메서드가 제대로 동작하려면 스택 내부 정보를 복사해야 하는데, 가장 쉬운 방법은 elements 배열의 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 필드가 final이었다면 앞서의 방식은 작동하지 않는다. => 직렬화와 마찬가지로 Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다.


- clone을 재귀적으로 호출하는 것만으로는 충분하지 않을 때도 있다.

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

- Stack에서처럼 단순히 버킷 배열의 clone을 재귀적으로 호출하면, 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다. 이를 해결하려면 각 버킷을 구성하는 연결 리스트를 복사해야 한다. => 연결 리스트를 재귀적으로 복사하는 deepCopy() 기법은 간단하지만 스택 오버플로를 일으킬 위험이 있다. 이 문제를 피하려면 반복자를 써서 순회하는 방향으로 수정해야 한다.

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

- 복잡한 가변 객체를 복제하는 마지막 방법은 먼저 soper.clone을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한 다음, 원본 객체의 상태를 다시 생성하는 고수준 메서드들을 호출하는 것이다. => 보통은 간단하고 제법 우아한 코드를 얻게 되지만, 비교적 느리며 Cloneable 아키텍처의 기초가 되는 필드 단위 객체 복사를 우회하기 때문에 전체 Cloneable 아키텍처와는 어울리지 않는 방식이기도 하다.

 

- Object의 clone 메서드는 CloneNotSupportedException을 던진다고 선언했지만 재정의한 메서드는 그렇지 않다. public인 clone 메서드에서는 throws 절을 없애야 한다. 검사 예외를 던지지 않아야 그 메서드를 사용하기 편하기 때문이다.(아이템 71)

 

- 상속용 클래스는 Cloneable을 구현해서는 안 된다. => 제대로 작동하는 clone 메서드를 구현해 protected로 두고 CloneNotSupportedException도 던질 수 있다고 선언하거나, clone을 동작하지 않게 구현해놓고 하위 클래스에서 재정의하지 못하게 할 수도 있다.

@Override
protected final Object clone() throws CloneNotSupportedException {
    throw new CloneNotSupprotedException();
}

 

- Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다.(아이템 78)


- 요약하자면, Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다.

- Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 그렇지 않은 상황에서는 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.

// 복사 생성자
public Yum(Yum yum) { ... };

// 복사 팩터리
public static Yum newInstance(Yum yum) { ... };

- 복사 팩터리는 복사 생성자를 모방한 정적 팩터리(아이템 1)다.

- 복사 생성자와 그 변형인 복사 팩터리는 Cloneable/clone 방식보다 나은 면이 많다.


[핵심 정리]

새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다. 기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고'라는 것이다. 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외다.