IT/Programming / / 2023. 4. 28. 09:15

<Effective Java> RULE 11 Clone을 재정의할 때는 신중하라

반응형

 

 
Clone?

Cloneable은 어떤 객체가 복제(Clone)을 허용한다는 사실을 알리는데 고안된 인터페이스

특징

 clone메소드가 없다.

 Object의 clone메소드는 protected로 선언되어 있다.

그러면 리플렉션을 사용하여 clone메소드를 호출해야 하나?

 호출 가능한 clone메소드가 구현 되어 있는지 모르기 때문에 부적절

 

1. clone메소드를 구현하는 방법

 

clone메소드의 일반 규약

 객체의 복사본을 만들어서 반환한다.

 복사의 정확한 의미는 다음의 조건이 충족되어야 한다.

 x.clone() != x 참

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

 x.clone().equals(x) 참

 

객체를 복사하면 보통 같은 클래스의 새로운 객체가 만들어 지는데 어떤 생성자도 호출되지 않으며 내부 자료구조까지 복사해야 될 수도 있다.

 

유의점 1 : 비-final 클래스(재정의 가능)에 clone을 재정의 할 때는 반드시 super.clone을 호출해 얻은 객체를 반환해야 한다.

모든 상위 클래스가 이 규칙을 따르면 super.clone은 최종적으로 Object의 clone메소드를 호출하게 될 것이고 원하는 클래스의 객체가 만들어 질 것이다.

public class PhoneNumber {
    @Override 
    public PhoneNumber clone() {
        try {
            return (PhoneNumber)super.clone();
        } catch (CloneNotSupportedException e) {
            // TODO: handle exception
            throw new AssertionError();
        }        
    }
}
 

위의 clone메소드는 Object가 아니라 PhoneNumber를 반환한다.

 

PhoneNumber.clone은 객체를 반환하기 전에 super.clone이 반환하는 객체를 PhoneNumber로 형변환 해야 할 필요가 없어진다. (라이브러리가 할 수 있는일을 클라이언트에게 미루지 말라)

 

유의점 2 : 복제할 객체가 변경 가능 객체에 대한 참조 필드를 가지고 있을 때 위의 clone을 그대로 사용하면 안된다.

import java.util.Arrays;
import java.util.EmptyStackException;

public 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;
    }
    private void ensureCapacity() {
        if(elements.length == size) {
            elements = Arrays.copyOf(elements, 2*size+1);
        }
    }
}
 

만약 clone 메서드가 단순히 super.clone()이 반환한 객체를 그대로 반환하도록 구현 한다면

그 복사본의 size필드는 올바른 값이겠지만 elements필드는 원래 Stack객체와 같은 배열을 참조하게 되어 이후 복사본을 변경할 경우 NullPointerException예외를 발생하게 된다.

 

clone메소드를 또 다른 형태의 생성자이다. 원래 객체를 손상시키는 일이 없도록 해야하며 복사본의 불변성도 만족시켜야한다.

출처 입력

 

Stack의 Clone메소드를 구현하려면 따라서 스택의 내부구조도 복사해야 한다는 것이다.

가장 간단한 방법은 elements배열에도 clone을 재귀적으로 호출하는 것이다.

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

 

유의점 3 : 재귀적 호출로 충분하지 않을 때 > 버킷(bucket)배열로 구성된 해시테이블의 clone메소드

import java.util.Map.Entry;

public class HashTable implements Cloneable{
    private Entry[] bucket =...;
    
    private static class Entry{
        final Object key;
        Object value;
        Entry next;
        
        Entry(Object key, Object value, Entry next){
            this.value = value;
            this.key = key;
            this.next = next;
        }
    }
    
    @Override
    public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.bucket = bucket.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            // TODO: handle exception
            throw new AssertionError();
        }
    }
}
 

복사본이 자신만의 버킷 배열을 갖긴 하지만 복제된 배열의 각 원소는 원래 배열 원소와 동일한 연결 리스트를 참조하게 된다.

따라서 각 버킷을 구성하는 연결 리스트까지도 복사해야한다.

 ...
        //이 Entry객체가 첫 원소인 연결리스트를 재귀적으로 복사
        Entry deepCopy() {
            return new Entry(key,value,next == null? null : next.deepCopy())    ;
        }
    }
    
    @Override
    public HashTable clone() {
        try {
            HashTable result = (HashTable) super.clone();
            result.bucket = new Entry[bucket.length];
            for(int i = 0 ; i < bucket.length ; i++) {
                if(bucket[i] != null) {
                    result.bucket[i] = bucket[i].deepCopy();
                }
                return result;
            }
        } catch (CloneNotSupportedException e) {
            // TODO: handle exception
            throw new AssertionError();
        }
    }
}
 

DeepCopy를 통해 HashTable의 clone메소드가 적절한 크기의 새로운 bucket배열을 할당하고 원래 bucket배열을 돌면서 비어있지 않은 모든 버킷에 DeepCopy를 실행한다.

단점으로는 리스트 원소마다 스택 프레임을 하나씩 사용하게 된다. 따라서 리스트가 길면 쉽게 오버플로우가 생길 수 있으므로 재귀식이아니라 순환문을 사용하여 DeepCopy를 구현한다.

        Entry 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;
        }
 

유의점 4 : 생성자와 마찬가지로 clone메소드를 복사본의 비-final메소드, 즉 재정의 가능 메서드를 복사 도중에 호출하면 안된다.

호출할 경우 복사본 상태가 완성되기 전에 호출하게 되어 상태를 망가뜨리게 된다. 따라서 put은 final이나 private메서드여야 한다.

 

유의점 5 : 다중 스레드에 안전해야하는(thread-safe)클래스를 Cloneable로 만들려면 clone메서드에도 동기화메커니즘을 적용해야 한다. (규칙 66)

 

유의점 6 : 변경 불가능 클래스는 객체 복제를 허용하지 않는 것이 좋다.

복사본과 원본과의 논리적 구별이 어렵기 때문이다.

 

2.Clone메소드의 대안

객체복제를 지원하는 좋은 방법은, 복사 생성자나 복사 팩터리를 제공하는것이다.

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

장점

 외적 언어(extralinguistic)객체 생성 수단(native method)에 의존하지 않는다.

 어려운 규약을 요구하지 않는다.

 final 필드 용법과 충돌하지 않는다

 불필요한 예외 검사를 하지 않는다.

 형변환을 하지 않는다.

 

단점

 인터페이스 안에 넣을 수 없다.(하지만 Clone메소드를 public으로 선언하지 않기 때문에 Cloneable도 제대로 구실을 못하고 있다)

 

결론

 이렇게 복잡한 clone 메서드가 정말 필요한지 책에서도 반문을 제기하며 차라리 객체를 복사할 대안을 제공하거나 아에 복제 기능을 제공하지 말라고 조언한다.

 

 

반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유