본문 바로가기
Java/Effective Java 3E

[이펙티브자바 3판] ITEM13. clone 재정의는 주의해서 진행하라

by 잭피 2020. 9. 13.

이번장의 핵심은...

새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안되며,

새로운 클래스도 이를 구현해서는 안 된다.

final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만,

성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다.

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

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


Cloneable 인터페이스

클래스에서 clone을 재정의 하기 위해선 해당 클래스에 Cloneable 인터페이스를 상속받아 구현해야 한다

그런데 clone 메소드는 Cloneable 인터페이스가 아닌 Object에 선언되어 있다

Cloneable 인터페이스에는 아무것도 선언되어 있지 않은 빈 인터페이스이다

 

Cloneable 인터페이스의 역할

Cloneable 인터페이스는 상속받은 클래스가 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스이다

믹스인이란 클래스가 본인의 기능 이외에 추가로 구현할 수 있는 자료형으로, 어떤 선택적 기능을 제공한다는 사실을 선언하기 위해 쓰인다

Cloneable 인터페이스의 역할은 Object의 clone의 동작 방식을 결정한다

Cloneable을 상속한 클래스의 clone 메서드를 호출하면 해당 클래스를 필드 단위로 복사하여 반환한다

만약, Cloneable을 상속받지 않고 clone 메서드를 호출하였다면 'CloneNotSupportedExcetion'을 던진다

 

Object clone 메소드의 일반 규약

1. x.clone() != x -> 참이다 하지만 필수는 아니다

원본 객체와 복사 객체는 서로 다른 객체이다

2. x.clone().getClass() == x.getClass() -> 참이다 하지만 필수는 아니다 

3. x.clone().equals(x) -> 참이다 하지만 필수는 아니다

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

super.clone()을 호출해 얻은 객체를 clone 메서드가 반환한다면, 이 식은 참이다

관례상, 반환된 객체와 원본 객체는 독립적이어야 한다

이를 만족하려면 super.clone()으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다

 

상위 클래스(clone 메서드를 가진)를 상속해 Cloneable을 구현하고 싶다면?

먼저, super.clone()을 호출한다 (원본의 완벽한 복제본이다)

// 가변 상태를 참조하지 않는 클래스용 clone 메서드 
@Override public PhoneNumber clone() {
	try {
		return (PhoneNumber) super.clone();
	} cathch (CloneNotSupportedException e) {
		throw new AssertionError(); 
	}
}

이 메서드가 동작하게 하려면 PhoneNumber 클래스 선언에 Cloneable을 구현한다고 추가해야 한다

하지만, 가변 상태를 참조하는 클래스의 경우 위처럼 작성하면 문제가 생긴다

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

	public Stack() {
		this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
	}

	public void push(Object e) {
		ensureCapacity();
		elements[size++] = e;
	}

	public Object pop() {
		if (size == 0) {
			throw new EmptyStackException();
		}
		Object result = elements[--size];
		elements[size] = null; // 다 쓴 참조 해제
		return result;
	}
    
   
	// 가변 상태를 참조하는 클래스용 clone 메서드
	@Override
	public Stack clone() {
		try {
			Stack result = (Stack) super.clone();
			result.elements = elements.clone();
			return result;
		} catch (CloneNotSupportedException e) {
			throw new AssertionError();
		}
	}

	private void ensureCapacity() {
		if (elements.length == size) {
			elements = Arrays.copyOf(elements, 2 * size + 1);
		}
	}
}

위 Stack 클래스에서 Object 배열인 elements는 가변 상태이다

이에 대해 가변 상태를 참조하지 않는 클래스처럼 clone을 단순히 정의하게 되면,

clone을 단순히 정의하게 되면, clone된 객체에서의 elements에는 null 값만 존재한다

따라서 가변 상태인 elements에 대해 clone을 추가적으로 해주면 우리가 원하는 복제값을 얻을 수 있다

 

이보다 조금 더 복잡한 가변 상태를 참조하는 클래스의 clone을 살펴보자

class HashTable implements Cloneable {
	private Entry[] buckets = ...;

	private static class Entry{
		final Object key;
		Object value;
		Entry next;

		Entry(Object key, Object value, Entry next){
			this.key = key;
			this.value = value;
			this.next = next;
		}
	}
	...
    @Override
    public HashTable clone(){
        try{
            HashTable result = (HashTable) super.clone();
            result.buckets = buckets.clone();
            return result;
        } catch (CloneNotSupportedException e){
            throw new AssertionError();
        }
    }
}

현재 HashTable에서 Entry 배열은 또 다른 Entry를 참조하고 있는 형태이다

이러한 경우 Stack과 같이 buckets 들을 단순히 clone을 하면 문제가 있다

이는 연결리스트를 참조하게 되어 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다

이를 해결하기 위해 버킷을 구성하는 연결 리스트를 복사해야 한다

 

연결 리스트는 재귀를 통한 방식과 반복자를 통한 방식이 있다

class HashTable implements Cloneable {
	private Entry[] buckets = ...;

	private static class Entry{
		...
		Entry deepCopy(){
			// 재귀적 deepCopy
			return new Entry(key, value, next == null ? null : next.deepCopy());
			
			// 반복자 deepCopy
			Entry result = new Entry(key, value, next);
            for(Entry p = result; p.next != null; p = p.next){
				p.next = new Entry(p.next.key, p.next.value, p.next.next);
			}
            return result;
		}
	}

	@Override
	public HashTable clone(){
		try{
			HashTable result = (HashTable) super.clone();
			result.buckets = new Entry[buckets.length];
			for(int i = 0; i < buckets.length; i++){
				if(buckets[i] != null){
					result.buckets[i] = buckets[i].deepCopy();
				}
			}
			return result;
		} catch (CloneNotSupportedException e){
			throw new AssertionError();
		}
	}
}

위와 같이 연결 리스트를 deepCopy를 해주고,

이를 바탕으로 clone을 정의하여 연결 리스트를 참조하는 클래스도 올바르게 복제할 수 있다

Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절한 동기화가 필요하다

 

복사 생성자와 복사 팩토리

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

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

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

위와 같이 생성자 또는 정적 팩터리 메서드에 해당 객체를 전달하여

새로운 인스턴스를 만들도록 하여 인스턴스를 복사할 수 있다

 

이는 Cloneable과 같이

언어 모순적이고 위험한 객체 생성 메커니즘을 사용하지 않으며,

엉성하게 문서화된 규약에 기대지 않고,

정상적인 final 필드 용법과도 충돌하지 않으며,

불필요한 검사 예외를 던지지 않고,

형변환도 필요하지 않다

 

게다가 해당 클래스가 구현한 인터페이스타입의 인스턴스를 인수로 받을 수 있다는 장점도 있다

 

댓글