본문 바로가기

개발/이펙티브 자바

Effective Java ( 이펙티브 자바 ) - 아이템 13

Clone 재정의는 주의해서 진행하라


Cloenable은 복제해도 되는 클래스임을 명시하는 용도의 인터페이스지만, 의도한 목적을 제대로 이루지 못했다. 그 이유는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고, 그마저도 protected라는 데 있다. 그래서 Cloenable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다. 

 

  하지만 여러 문제점에도 불구하고 Cloneable 방식은 널리 쓰이고 있어서 잘 알아두는 것이 좋다. 이번 아이템에서는 clone 메서드를 잘 동작하게끔 해주는 구현 방법과 언제 그렇게 해야 하는지를 알려주고, 가능한 다른 선택지에 관해 논의하겠다.

 

  메서드 하나 없는 Cloneable 인터페이스는 대체 무슨 일을 할까? Object의 proteced 메서드인 clone의 동작 방식을 결정한다. Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환한다. 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다. 

 

  이는 인터페이스를 상당히 이례적으로 사용한 예이니 따라 하지는 말자. 인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위다.

 

  명세에서는 이야기하지 않지만 실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대한다. 그 결과, 위험하고, 모순적인 메커니즘이 탄생한다고 한다. 생성자를 호출하지 않고도 객체를 생성할 수 있게 되는 것이다.

 

  clone 메서드의 일반 규약은 허술하다. Object 명세에서 가져온 다음 설명을 보자.

 


이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음 식은 참이다.

x.clone() != x
또한 다음 식도 참이다.

x.clone().getClass() == x.getClass()

하지만 이상의 요구를 반드시 만족해야 하는 것은 아니다.
한편 다음 식도 일반적으로 참이지만, 역시 필수는 아니다.

x.clone().equals(x)

관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해서 얻어야 한다. 이 클래스와 모든 상위 클래스가 이 관례를 따른다면 다음 식은 참이다.

x.clone().getClass() == x.getClass()
관례상, 반환된 객체와 원본 객체는 독립적이야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다. 

 

강제성이 없다는 점만 빼면 생성자 연쇄와 살짝 비슷한 메커니즘이다. 즉, clone 메서드가 super.clone이 아닌, 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 불평하지 않을 것이다. 하지만 이 클래스의 하위 클래스에서 super.clone을 호출한다면 잘못된 클래스의 객체가 만들어져, 결국 하위 클래스의 clone 메서드가 제대로 동작하지 않게 된다.

 

  제대로 동작하는 clone 메서드를 가진 상위 클래스를 상속해 Cloneable을 구현하고 싶다고 해보자. 먼저 super.clone을 호출한다. 그렇게 얻은 객체는 원본의 완벽한 복제본일 것이다. 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 이 객체는 완벽히 우리가 원하는 상태라 더 손볼 것이 없다. 그런데 쓸데없는 복사를 지양한다는 관점에서 보면 불변 클래스는 굳이 clone 메서드를 제공하지 않는게 좋다. 

@Override
public PhoneNumber clone() {
   try {
      return (PhoneNumber) super.clone();
   } catch (CloneNotSupportedException e) {
      throw new AssertionError(); // 일어날 수 없는 일이다.
   }
}

간단했던 앞서의 구현이 클래스가 가변 객체를 참조하는 순간 재앙으로 돌변한다. 아이템 7에서 소개한 Stack 클래스의 필드는 아래와 같았다.

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

clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다. 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출해 주는 것이다.

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

 

  clone을 재귀적으로 호출하는 것만으로는 충분하지 않을 때도 있다. 책에서는 해시테이블용 clone 메서드를 예로 들고 있다. 해시테이블 내부는 버킷들의 배열이고, 각 버킷은 키-값 쌍을 담는 연결 리스트의 첫 번째 엔트리를 참조한다. 여기서는 연결리스트도 복사해야한다. 

 

 


 

이제 복잡한 가변 객체를 복제하는 마지막 방법을 살펴본다. 먼저 super.clone을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한 다음, 원본 객체의 상태를 다시 생성하는 메서드들을 호출한다. HashTable이라면, 직접 put(key, value) 메서드를 호출해 둘의 내용이 똑같게 해주면 된다. 아무래도 저수준에서 바로 처리할 때보다는 느리다. 

 

Object의 clone 메서드는 CloneNotSupportedException을 던진다고 선언했지만 재정의한 메서드는 그렇지 않다. public인 clone 메서드에서는 throws 절을 없애야 한다. 그래야 메서드를 사용하기 편하기 때문이다. 

 

  기억해둬야 할게 하나 더 남았다. Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다는 점이다. 

 

  요약하자면, Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다. 이때 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다. 이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다. 

 

  그런데 이 작업이 꼭 필요한 걸까? Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 그렇지 않은 상황에서는 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다. 

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


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

Cloneable / Clone 보다 나은 면이 많다. 생성자를 쓰지 않는 언어 모순적이고 위험천만한 객체 생성 메커니즘을 사용하지 않는 등의 장점이 많다.

  복사 생성자, 복사 팩터리는 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다. 그래서 더 정확한 이름은 '변환 생성자'와 '변환 팩터리'다. 이들을 이용하면 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다. 

 

 

기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고'라는 것이다. 단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.