본문 바로가기
이펙티브 자바

아이템 13. clone 재정의는 주의해서 진행하라

by 아토로 2022. 2. 12.

Cloneable 인터페이스

  • Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스(mixin interface, 아이템 20)이다.
  • Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
  • 여러 문제점에도 불구하고 Cloneable 방식은 널리 쓰이고 있다.clone

믹스인 인터페이스: 클래스가 구현할 수 있는 타입으로, 믹스인을 구현한 클래스에 원래의 '주된 타입' 외에도 특정 선택적 행위를 제공한다고 선언하는 효과를 줄 수 있다.

의도한 목적을 제대로 이루지 못한 이유

  • clone 메서드가 선언된 곳인 Clonable이 아닌 Object 이다.
  • clone 메서드는 protected 이고 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.

clone 메서드의 일반 규약

  1. x.clone() ≠ x
  2. x.clone().getClass() == x.getClass(), 필수는 아니다.
  3. x.clone().getClass().equals(x), 필수는 아니다.
  4. x.clone().getClass() == x.getClass(), clone()이 반환하는 객체는 super.clone()을 호출해서 얻어야 한다. 이 클래스와 모든 상위 클래스가 이 관계를 따른다면 이 식은 참이다.

clone 메서드 재정의

@Override
protected PhoneNumber clone() {
    try {
        return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

이 메서드가 동작하게 하려면 PhoneNumber의 클래스 선언에 Cloneable을 구현한다고 추가해야 한다. Object의 clone 메서드는 Object를 반환하지만 PhoneNumber의 clone 메서드는 PhoneNumber를 반환하게 했다. 자바가 공변 반환 타이핑(covariant return typing)을 지원하니 가능한 방식이다.

공변 반환 타입: 객체 지향 프로그래밍에서 메서드가 오버라이딩될 때 더 좁은 타입으로 교체할 수 있다는 것

super.clone 호출을 try-catch 블록으로 감싼 이유는 Object의 clone 메서드가 checked exception인 CloneNotSupportedException을 던지도록 선언되었기 때문이다. Cloneable을 구현하면 super.clone이 성공할 것임을 알기 때문에 CloneNotSupportedException가 사실은 unchecked exception이었어야 했다는 신호다.

 

만약 clone 메서드가 super.clone이 아닌, 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일 에러는 발생하지 않겠지만, 하위 클래스에서 super.clone을 호출한다면 잘못된 클래스의 객체가 만들어져, 결국 하위 클래스의 clone 메서드가 제대로 동작하지 않게 된다.

public class PhoneNumberParent implements Cloneable {

    private final short areaCode, prefix, lineNum;

    public PhoneNumberParent(int areaCode, int prefix, int lineNum) {
        this.areaCode = rangeCheck(areaCode, 999, "지역코드");
        this.prefix = rangeCheck(prefix, 9999, "프리픽스");
        this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
    }

    private short rangeCheck(int val, int max, String arg) {
        if (val < 0 || val > max) {
            throw new IllegalArgumentException(arg + ": " + val);
        }
        return (short) val;
    }

    @Override
    protected PhoneNumberParent clone() {
        return new PhoneNumberParent(areaCode, prefix, lineNum);
    }
}

public final class PhoneNumberChild extends PhoneNumberParent {

    public PhoneNumberChild(int areaCode, int prefix, int lineNum) {
        super(areaCode, prefix, lineNum);
    }

    @Override
    protected PhoneNumberChild clone() {
        return (PhoneNumberChild) super.clone();  // ClassCastException 예외가 발생한다.
    }
}

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

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

배열의 clone은 런타임 타입과 컴파일타임 타임 모두가 원본 배열과 똑같은 배열을 반환한다. 따라서 배열을 복제할 때는 배열의 clone 메서드를 사용하라고 권장한다. 사실, 배열은 clone 기능을 제대로 사용하는 유일한 예라고 할 수 있다.

복사 생성자와 복사 팩터리

Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 그렇지 않은 상황에서는 복사 생성자와 복사 팩터리가 더 나은 객체 복사 방식을 제공할 수 있다. 복사 생성자란 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다.

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

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

핵심 정리

  • 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안 된다.
  • final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.
  • 기본 원칙은 ‘복제 기능은 생성자와 팩터리를 이요하는 게 최고'라는 것이다.
  • 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.

예제 코드

https://github.com/jsyang-dev/study-effective-java/tree/master/src/item13

댓글