본문 바로가기

study/effective java

[item13] clone 오버라이딩은 주의해서 진행해라

Cloneable 인터페이스

Cloneable 인터페이스는 이 클래스는 복제가 가능함을 명시하는 목적으로 사용하는 mix interface이다. 그러나 clone() 메소드는 인터페이스가 아니라 최상위 객체인 Object 클래스에 작성이 되어 있다. 그래서 Cloneable 인터페이스에는 아무것도 적혀있지 않는다.

public interface Cloneable {
}

Cloneable 인터페이스를 상속 받지 않으면 clone를 호출할 시 CloneNotSupportedException 예외가 발생한다.

clone 메소드 일반 규약

  1. x.clone() != x 는 true이다.
  2. x.clone().getClass() == x.getClass() 는 true 이다.
  3. x.clone().equals(x) 는 true이다.

위의 일반 규약을 한 문장으로 요약하자면 clone으로 복제한 객체는 기존의 객체와 동일성은 만족하지 못하지만 동등성은 만족해야 한다 는 규약이다. 때문에 clone()로 만들어진 객체는 기존의 객체와 독립적이어야 하며 이를 준수하기 위해 clone()으로 복제된 객체에서 일부 필드를 수정해야하는 상황이 발생할 수 있다.

clone 메소드 오버라이딩 주의사항

1. 오버라이딩하지 않는 clone() 메소드는 shallow copy로 동작한다

별도의 오버라이딩을 하지 않는 한 clone()를 통해 복제된 메소드는 shallow copy로 만들어진다. 때문에 clone를 통해 객체를 복제할 때는 오버라이딩을 하여 deep copy로 만들었는지 확인해야 할 필요가 있다.'

간단하게 예시를 들어보겠다.

public class MyClass implements Cloneable {

    private int[] values;

    public MyClass(int[] values) {
        this.values = values;
    }

    public int[] getValues() {
        return values;
    }

    public void setValues(int[] values) {
        this.values = values;
    }

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

MyClass는 clone() 메소드를 오버라이딩하였다. Object객체의 clone() 메소드로 복제하고 이를 반환하였다.

이 코드는 다음과 같은 main() 함수에서 실행시키겠다.

import java.util.Arrays;

public class Main {

    public static void main(String[] args) {
        int[] values = {1, 2, 3};
        MyClass original = new MyClass(values);
        MyClass cloned = (MyClass) original.clone();

        System.out.println("Original values: " + Arrays.toString(original.getValues()));
        System.out.println("Cloned values: " + Arrays.toString(cloned.getValues()));

        values[0] = 100;

        System.out.println("Original values after modification: " + Arrays.toString(original.getValues()));
        System.out.println("Cloned values after modification: " + Arrays.toString(cloned.getValues()));
    }
}

실행결과는 다음과 같다.

Task :Main.main() Original values: [1, 2, 3] Cloned values: [1, 2, 3] Original values after modification: [100, 2, 3] Cloned values after modification: [100, 2, 3]

원본 객체를 만들 때 사용했던 매개변수인 values 배열의 값을 바꾸니 원본 객체와 복제된 객체 모두 바뀌었다. 이처럼 단순히 clone() 메소드를 작성한다고 해서 원하는 복사가 일어나지 않을 수도 있다.

2. clone 메소드는 생성자와 같은 효과를 낸다

clone는 내용이 동일한 객체를 만드는 것이다. 그럼 상속 관계인 객체는 어떻게 복제할까. 바로 부모 클래스의 clone 객체를 통해 복제받고 다운캐스팅 후에 자식 클래스의 필드를 별도로 복제하는 방식으로 작성한다. 이 방식은 생성자 체인(Constructor Chaining) 과 비슷한 과정으로 동작한다. 즉 완전한 복제를 위해서는 생성자처럼 사용해야한다는 의미이다.

3. 부모 클래스로 부터 복제받아온 객체의 final 필드를 수정할 수 없다

clone으로 복제된 객체는 동등성을 만족해야 한다. 때문에 부모 클래스로 부터 받아온 final 필드를 수정해야하는 경우가 발생할 수 있다. 그러나 final 필드는 한번 값이 지정되면 수정할 수 없기에 자식 클래스에서 재작성이 어려워 final 키워드를 제거해야하는 경우가 발생할 수 있다.

4. clone 메소드는 동기화를 신경쓰지 않고 만들어졌다

스레드 환경에서 clone을 수행할 경우 동기화에 대한 작업을 별도로 수행해줘야 한다.

clone 메소드의 대안: 복사 생성자(팩토리)

  • 복사 생성자/팩토리 는 매개변수로 객체 자신을 받는 방법이다. 이 방식을 생성자로 만들었는지, 팩토리로 만들었는지에 대한 차이이다.

  • 복사 생성자

public class MyClass {
    private int value;
    private String name;

    public MyClass(MyClass other) {
        this.value = other.value;
        this.name = other.name;
    }
}
  • 복사 팩토리
public class MyClass {
    private int value;
    private String name;

    public static MyClass copyOf(MyClass other) {
        MyClass copy = new MyClass();
        copy.value = other.value;
        copy.name = other.name;
        return copy;
    }

}

결론

clone() 메소드가 deep copy를 쉽게 해줄 수 있는 도구라고 생각하였는데 아니여서 실망하였다. 그럼에도 clone()이라는 메소드에 대해 알아볼 수 있었고 복사 생성자, 복사 팩토리라는 용어도 배워갔다.


Reference