이펙티브 자바/3장 모든 객체의 공통 메서드

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

말랑공룡 2023. 12. 21. 10:40

 

자바에서 Object 클래스의 clone() 메서드는 해당 객체를 복제하는데 사용된다.

그러나, Object 클래스의 clone() 메서드는 복제될 객체가 Cloneable 인터페이스를 구현하고 있지 않으면 'CloneNotSupportedException'을 던진다.

 

즉, 해당 객체가 clone() 메서드를 구현하기 위해서는 Cloneable을 implements 해야한다.

 

그런데, Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
왜냐하면, Cloneable은 인터페이스 자체가 메서드를 갖지 않는 마커 인터페이스이기 때문이다.

마커 인터페이스란?
메서드를 갖지 않는 인터페이스로,
해당 인터페이스를 구현한 클래스가 특정한 특성이나 동작을 갖고 있음을 나타낸다.
이 인터페이스는 단순히 클래스가 어떤 그룹에 속한다거나 특정한 속성을 갖는다는 것을 표시하기 위해 사용한다.

 

결국은 clone() 메서드를 오버라이딩하여 내부에서 super.clone()을 호출해서 메서드를 제대로 구현해야 한다.

 

class MyCloneableClass implements Cloneable {
    // 클래스의 내용

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {
    public static void main(String[] args) {
        MyCloneableClass original = new MyCloneableClass();

        try {
            // 외부에서 clone() 메서드 호출 가능
            MyCloneableClass copy = (MyCloneableClass) original.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
}

 

super.clone()을 하는 이유

clone 메서드가 생성자를 호출해 얻은 인스턴스를 반환해도 별 문제는 없어보인다.

하지만 이 클래스의 하위 클래스에서 super.clone()을 호출한다면 잘못된 클래스의 객체가 만들어져, 결국 하위 클래스의 clone 메서드가 제대로 동작하지 않게 된다.

재정의한 클래스가 final이라면 걱정해야 할 하위 클래스가 없으니 무시해도 된다.

아무튼 이런 부분을 고려해야 하기 때문에 clone 메서드를 오버라이딩하여 구현할 때, super.clone()을 하는 것이다.

 

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

 

클래스가 가변 객체를 참조할 때

clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.

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

 

예를 들어, Stack 클래스에서 단순하게 clone()을 구현하면 원본과 복사본의 elements는 같은 객체를 참조하게 된다.

그래서 clone() 구현 시에는 객체 참조를 고려하여 내용을 작성해야 한다.

 

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

 

(참고) 배열의 clone은 원본 배열과 똑같은 배열을 반환한다.

 

이런 작업들을 하면서까지 clone()을 구현해야할까?

Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다.

그렇지 않다면, 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.

 

복사 생성자(변환 생성자)
public class MyClass {
    private int value;

    // 일반 생성자
    public MyClass(int value) {
        this.value = value;
    }

    // 복사 생성자
    public MyClass(MyClass other) {
        this.value = other.value;
    }

    // 나머지 클래스 정의...
}

 

MyClass original = new MyClass(42);
MyClass copy = new MyClass(original);

 

복사 팩토리(변환 팩토리)
public class MyClass {
    private int value;

    // 일반 생성자
    public MyClass(int value) {
        this.value = value;
    }

    // 복사 팩토리 메서드
    public static MyClass copyFrom(MyClass source) {
        return new MyClass(source.value);
    }

    // 나머지 클래스 정의...
}
MyClass original = new MyClass(42);
MyClass copy = MyClass.copyFrom(original);

 

결론

Cloneable은 이래저래 문제가 많으므로 아래와 같은 방법을 쓰자.

단, 배열만은 clone 메서드 방식이 가장 깔끔하다.