본문으로 바로가기

1. Cloneable

Cloneable은 복제해도 되는 클래스임을 명시하는 믹스인 인터페이스이지만, 여러 이슈가 존재한다.

 

상위 클래스에 정의된 protected 메서드의 동작 방식을 변경하는 패턴은 이례적인 케이스다.

clone() 메서드는 Object에 protected로 정의되어 있다. Cloneable은 Object의 clone() 메서드의 동작 방식을 결정한다. Cloneable을 구현하지 않은 클래스가 Object.clone()을 호출하면 예외가 호출된다. 해당 예외는 Checked Exception이다.

 

2. Object의 clone 명세

Object의 clone() 메서드 명세는 다소 허술하다.

 

  • x.clone() != x와 x.clone().getClass() == x.getClass()  x.clone().equals(x)는 일반적으로 참이지만 필수는 아니다.
  • 객체의 복사본을 생성해서 반환하지만, ‘복사’는 구현 클래스에 따라 의미가 다를 수 있다.
  • 관례상, 복사 메서드가 반환하는 객체는 super.clone()을 호출해 얻는다.
    • 해당 관례를 따르면, x.clone().getClass() == x.getClass()는 참이다.
  • 관례상, 반환 객체와 원본 객체는 독립적이어야 한다.
    • 이를 만족하려면 super.clone()으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.

관례에 따라 하위 클래스에서 super.clone()을 호출한다면 결국 부모 클래스 객체가 만들어지기 때문에 하위 클래스의 clone()이 제대로 동작하지 않는다.

 

  • 즉, 재정의한 clone()의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입이다.
  • 하위 클래스가 없는 final 클래스는 해당 관례를 무시해도 된다.

 

3. clone과 필드 타입

원본 객체의 필드가 기본 타입이거나 불변 객체라면 clone() 반환 객체는 완벽한 복사본이다. 그러나 원본 객체의 필드가 배열 등 가변 상태를 가지는 객체를 참조하고 있다면, 복사본 또한 원본 필드와 동일한 레퍼런스를 참조하게 된다.

복사본을 수정하면 원본 객체에도 변경이 가해진다.

 

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

Stack.java

@Override public Stack clone() {
    try {
        Stack result = (Stack) super.clone();
        result.elements = elements.clone(); //배열 별도 복사
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

배열의 clone() 메서드로 별개의 주소값을 가진 배열을 생성해서 복사본이 참조하도록 한다.

그러나 배열 필드가 final이라면 위 방식은 사용이 불가능하다.

 

  • Cloneable 인터페이스는 직렬화와 마찬가지로 가변 객체를 참조하는 필드는 final로 선언하라는 용법과 충돌한다.

만약 배열 내부 원소들이 기본 타입이 아닌 가변 객체라면 단순히 배열을 clone()하는 것에 그치는 것이 아니라, 해당 원소들 또한 새로 복사(사실상 새로운 객체 생성)해야 한다. 다시 말해 복사본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 해야한다.

 

3. 기타

생성자와 마찬가지로 clone()에서도 재정의될 수 있는 메서드를 호출하지 않아야 한다.

 

  • 사용할 메서드는 final 혹은 private이어야 한다.

재정의한 public clone() 메서드는 CloneNotSupportedException throws 절을 없애야 한다.

 

  • 검사 예외를 두지 않아야 해당 메서드를 사용하기 편하다.

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

 

  • Object처럼 Cloneable을 구현하지 않으면 예외를 던지거나, clone()에서 예외를 던져 동작하지 않게 퇴화시켜두어 하위 클래스에서 재정의하지 못하게 한다.
  • 기본적으로 Cloneable은 확장하지 않고, 새로운 클래스도 이를 구현해서는 안 된다.

Cloneable을 구현한 스레드 세이프 클래스는 clone() 메서드 역시 동기화해줘야 한다.

 

  • Object의 clone() 메서드는 동기화를 신경쓰지 않았다.

복사 생성자나 복사 팩토리를 사용하면 clone()으로는 불가능한 복사 작업을 수행할 수 있다.

 

  • 단순히 자신과 같은 클래스의 인스턴스를 매개변수로 받는 생성자 혹은 팩토리이다.
    • 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.
  • HashSet 객체 s를 new TreeSet<>(s)를 통해 TreeSet 타입으로 복제할 수 있다.

 

요약

Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을

확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을

구현해도 위험이 크지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.

(아이템 67)

 

기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고' 라는 것이다.

단, 배열만은 clone 메서드 방식이 가장 깔끔한 이 규칙의 합당한 예외라 할 수 있다.