Effective Java - Item 13

Updated:

Effective Java

Item 13. 재정의는 주의해서 진행하라

Clonealbe

Cloneable은 어떤 클래스를 복제해도 된다는 사실을 알리기 위해서 만들어진 믹스인 인터페이스(item 20)이다.
단, clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이며,
Cloneable의 인터페이스를 구현한 클래스는 Object의 메소드인 clone()을 어떤 식으로 사용할 것인지를 결정할 수 있게 한다.
즉, Cloneable을 구현한 인스턴스에서 clone() 메소드를 호출하게 되면, 해당 객체를 복사한 객체를 반환하게 된다.

Mixin Interface

실제 클래스가 가지고 있는 주된 기능에 특정 타입을 구현함으로써 선택적인 기능을 혼합하게 하는 인터페이스

public class MyCloneable implements Cloneable{
  private String name;

  public MyCloneable(String name) {
    this.name = name;
  }
  
  @Override
  public MyCloneable clone() throws CloneNotSupportedException {
    return (MyCloneable)super.clone();   // Object의 clone 메서드를 호출한다.
  }
}

Object의 clone 메서드를 통하여 새로운 MyCloneable 객체를 반환하게 하였다.
단, super.clone()은 object를 반환하여 MyCloneable로 type cast를 해주었다. 이는 공변 반환 타입으로 인해 상위 클래스의 메소드가 반환하는 타입(Object)의 하위 타입이라서 가능한 것이다.

가변 객체의 Cloneable

위의 예와 같이 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 아무 이슈가 없지만, 만약 리스트와 같은 가변 객체가 포함되어 있으면 추가로 수정해주어야 한다.

public class MyCloneable implements Cloneable{
  private String name;
  private int[] arr;

  public MyCloneable(String name) {
    this.name = name;
    arr = new int[100];
  }
  
  @Override
  public MyCloneable clone() throws CloneNotSupportedException {
    return (MyCloneable)super.clone();   // Object의 clone 메서드를 호출한다.
  }
}

위와 같은 int 배열이 추가 된 상황에서 clone으로 새로운 객체를 복사하였다고 하자.
이의 결과는 반환된 MyCloneable 인스턴스의 String 필드는 올바른 값을 갖겠지만, arr 필드는 원본 MyCloneable 인스턴스와 똑같은 배열을 참조할 것이다.
즉, 원본이나 복제본 둘 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다는 이야기다.
이를 얕은 복제(Shallow Copy)라고 한다.

clone 메서드는 사실상 생성자와 같은 효과를 낸다. 따라서 가변 객체도 복제될 수 있는 로직을 추가하여 제대로 된 복사를 진행한다.

public class MyCloneable implements Cloneable{
  private String name;
  private int[] arr;

  public MyCloneable(String name) {
    this.name = name;
    arr = new int[100];
  }
  
  @Override
  public MyCloneable clone() throws CloneNotSupportedException {
    Stack result = (MyCloneable)super.clone();   // Object의 clone 메서드를 호출한다.
    result.arr = arr.clone();   // Deep Copy
    return result;
  }
}

Clonealbe 구현 주의사항

  1. clone()을 구현할 때, 재정의될 수 있는 메서드를 호출하지 않아야 한다.
    clone이 하위 클래스에서 재정의한 메서들르 호출하면, 하위 클래스는 자신의 상태를 잃게되어 원본과 복제본의 상태가 달라질 가능성이 크다.
  2. Object의 clone 메서드와는 달리, public인 clone 메서드를 재정의 할 때에는 throws 절을 없애야한다.
  3. 상속용 클래스는 Cloneable을 구현해서는 안된다.
  4. Cloneable을 구현한 Thread safe class를 작성할 때는 clone 메서드 역시 동기화를 해주어야한다.
  5. 기본 타입 필드와 불변 객체 참조만 갖는 클래스라면, 아무 필드도 수정할 필요가 없다.
    단, 일련 번호고유 ID는 비록 기본 타입이나 불변일지라도 수정해주어야 한다.

복사 생성자와 복사 팩터리

cloneable을 구현하지 않고 복사 생성자와 복사 팩터리를 통해 이를 해결할 수 있다.

// 복사 생성자
public Yum(Yum yum) { ... }
//복사 정적 팩터리 메소드
public static Yum newInstance(Yum yum) { ... };

위와 같이 생성자 또는 정적 팩터리 메서드에 해당 객체를 전달하여 새로운 인스턴스를 만들도록 하여 인스턴스를 복사할 수 있다.
이는 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다는 장점도 가지고 있다.

정리

Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다. 이때 접근 제한자는 public으로, 반환 타입은 Class 자신으로 변경한다.
이후 clone을 재정의 할 때, 다음의 과정을 거친다.

  1. super.clone을 호출한다.
  2. 가변 객체와 같은 Deep Copy가 필요한 객체들을 제대로 복사 될 수 있게 적절히 수정한다.

얕은 복사는 객체 내부에 있는 참조 객체가 복제되지 않는 것이고, 깊은 복사는 객체 내부에 있는 참조 객체까지 복제되는 것이다.

출처

Joshua Bloch. Effective Java 3/E. n.p.: 인사이트, 2018년 11월 1일.

참고 페이지

https://happy-playboy.tistory.com/entry/Item-13-clone-%EC%9E%AC%EC%A0%95%EC%9D%98%EB%8A%94-%EC%A3%BC%EC%9D%98%ED%95%B4%EC%84%9C-%EC%A7%84%ED%96%89%ED%95%98%EB%9D%BC