[Effective Java] 아이템13 - clone 재정의는 주의해서 진행하라

Cloneable 은 메소드 하나 없는 인터페이스이다.

화면 캡처 2021-02-03 221702

이 인터페이스의 용도는 이 인터페이스를 구현한 Object의 protected 메서드인 clone의 동작 방식을 결정한다.

인터페이스를 구현하면 통상적으로 그 클래스가 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위인데, Cloneable의 경우에는 구현체의 protected 메서드를 동작하게 하도록 변경한 것이다.

그래서 생성자를 호출하지 않고도 객체를 생성할 수 있게되어 모순적인 메커니즘이 탄생한다.

불변객체를 참조하는 객체의 clone

Cloneable을 implment하고 clone() 메소드를 재정의해주면 된다. 원래는 Object 형으로 반환하지, 원본과 완벽히 동일한 복제본이므로 형변환을 시켜서 리턴한다.

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

가변객체를 참조하는 객체의 clone

PhoneNumber 클래스는 private final short areaCode, prefix, lineNum;처럼 값만 가지고 있는 클래스라 불변객체를 참조해 위와 같이 재정의해줘도 문제가 없다.

하지만 Array와 같이 가변 객체를 참조하는 순간, 동일한 배열을 참조할 것 이기 때문에 (deep copy가 아니다) 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 버린다.

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

Stack 클래스는 elements 라는 배열이 있다. 이 클래스의 clone 메소드를 PhoneNumber처럼 super.clone()을 써버리면 같은 객체를 참조하게 되어 안된다.

따라서 clone 메서드는 생성자와 같은 효과를 내도록 만들어줘야 한다.

@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone();
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

배열을 복제할 때는 배열의 clone 메서드를 사용하면 된다.

여기서 elements가 final이었다면 위 방식은 작동하지 않는다. final 필드에는 새로운 값을 할당할 수 없기 때문이다. 그래서 clone 메서드를 구현하려면 가변 객체에 final 한정자를 제거해야 할 수도 있다.

복잡한 가변 객체를 갖는 객체의 clone

해시테이블의 내부는 버킷들의 배열이고, 각 버킷은 키-값 쌍을 담는 연결리스트의 첫 번째 엔트리를 참조한다.

private Entry[] buckets = ...;

이러한 형태의 배열을 buckets.clone()을 해버리면 원본과 같은 연결 리스트를 참조하여 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다.

따라서 각 버킷을 구성하는 연결리스트를 복사해야 한다.

buckets [i] .deepCopy() ...  재귀 호출한다.

이 방법은 버킷이 너무 길지 않다면 잘 작동한다. 하지면 연결 리스트를 복제하는 방법으로는 그다지 좋지 않다. 재귀호출 보다는 반복문으로 순회해서 deepcopy를 하면 된다.

상속용 클래스는 Cloneable을 구현해서는 안된다.

상속해서 쓰기 위한 클래스는 Cloneable을 구현해서는 안된다.(왜인지는 모르겠다.)

CloneNotSupportedException 을 던지거나, 하위클래스에서 Cloneable을 재정의하지 못하게 하면 된다.

Cloneable을 대체할 복사생성자나 복사 팩터리

지금까찌 살펴보았듯이 Cloneable 을 구현한 클래스는 clone 메서드를 재정의하고 이 메서드는 super.clone을 호출한 후 필요한 필드를 모두 복제하고, 참조 객체들도 deppcopy하여 복사한다.

그러나 복사 생성자와 복사팩터리가 더 나은 방안일 수도 있다.

복사생성자

단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다.

public Yum(Yum yum) {...}; // 나 자신을 인수로 받아서 반환한다.

복사팩터리

복사생성자를 모방한 정적팩터리다.

public static Yum newInstance(Yum yum) {...}

이러한 복사생성자와 복사 팩터리는 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 파라미터로 받을 수 있다.

화면 캡처 2021-02-03 225659

TreeSet이 구현한 인터페이스 Collection의 타입 인스턴스를 매개변수로 받아서, copy 할 수 있다.

원본의 구현 타입과 상관없이 복제본의 타입을 직접 선택할 수 있다는 뜻이다.

HashSet<String> hashSet = new HashSet<>();
TreeSet<String> treeSet = new TreeSet<>(hashSet);

SUMMARY: final 클래스라면 cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.
기본 원칙은 ‘복제 기능은 생성자와 팩터리를 이용하는게 최고’라는 것이다.
다만 배열만은 clone 메서드 방식이 가능 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.

댓글남기기