본문 바로가기
Books/Effective-Java 3판

13. clone 재정의는 주의해서 진행하라(2)

by 두두리안 2021. 10. 6.
728x90

코드 13-3 잘못된 clone 메서드 - 가변 상태를 공유한다!

// 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드 (79쪽)
    @Override public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();  // 일어날 수 없는 일이다.
        }
    }

- 복제본은 자신만의 버킷 배열을 갖지만, 이 배열은 원본과 같은 연결 리스트를 참조하여

- 원본과 복제본 모두 예기치 않게 동작할 가능성이 생긴다

- 이를 해결하려면 각 버킷을 구성하는 연결 리스트를 복사해야 한다


코드 13-4 복잡한 가변 상태를 갖는 클래스용 재귀적 clone 메서드

public class HashTable implements Cloneable{
    private Map.Entry[] buckets = ...;
    
    private static class Entry{
        final Object key;
        Object value;
        Entry next;

        public Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
        
        // 이 엔트리가 가리키는 연결 리스트를 재귀적으로 복사
        Entry deepCopy(){
            return new Entry(Key,value, next == null ? null : next.deepCopy());
        }
    }
    @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();
        }
    }
    ... // 나머지 코드 생략
}

- private 클래스인 HashTable.Entry는 깊은 복사(deep copy)를 지원하도록 보강되었다

- HashTable의 clone 메서드는 먼저 적절한 크기의 새로운 버킷 배열을 할당한 다음

- 원래 버킷 배열을 순회하며 비지 않은 각 버킷에 대해 깊은 복사를 수행한다

- 이때 Entry의 deepCopy 메서드는 자신이 가리키는 연결리스트 전체를 복사하기 위해서 자신을 재귀적으로 호출

- 하지만 연결리스트로 복제하는 방법은 그다지 좋지 않다

- 재귀 호출 때문에 리스트의 원소 수만큼 스택 프레임을 소비하여,

- 리스트가 길면 스택 오버플로를 일으킬 위험이 있다


코드 13-5 엔트리 자신이 가리키는 연결 리스트를 반복적으로 복사한다.

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

복잡한 가변 객체를 복제하는 마지막 방법을 살펴보자

- super.clone을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한 다음, 원복 객체의 상태를 다시 생성하는 고수준 메서드를 호출

- HashTable 예라면, buckets 필드를 새로운 버킷 배열로 초기화한 다음 원본 테이블에 담긴 모든 키-값 쌍 각각에 대해

- 복제본 테이블의 put 메서드를 호출해 둘의 내용을 같이 해준다


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

- 만약 clone이 하위 클래스에서 재정의한 메서드를 호출하면 , 하위 클래스는 복제 과정에서 자신의 상태를 교정할 기회를 잃게 되어

- 원본과 복제본의 상태가 달라질 가능성이 크다.

- put 메서드는 final 이거나 priave 이어야 한다


Object의 clone 메서드는 CloneNotSupportedException을 던진다고 선언했지만 재정의한 메서드는 그렇지 않다

- public인 clone 메서드에서는 throws 절을 없애야 한다

- 검사 예외를 던지지 않아야 그 메서드를 사용하기 편하기 때문이다


코드 13-6 하위 클래스에서 Cloneable을 지원하지 못하게 하는 clone 메서드

@Override
protected final Object clone() throws CloneNotSupportedException(){
	throw new CloneNotSupportedException();
}

- Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다

- Object의 clone 메서드는 동기화를 신경 쓰지 않았다

- 그러니 super.clone 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화해줘야 한다

 

요약 : Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다

- 접근자 : public

- 반환 타입 : 클래스 자신

- 이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다

- 일반적으로 이 객체의 내부 '깊은 구조'에 숨어 있는 모든 가변 객체를 복사하고,

- 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 함을 뜻한다


이 모든 작업이 꼭 필요한 걸까?

- Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다

- 그렇지 않은 상황에서는 복사 생성자와 복사 팩토리라는 더 나은 객체 복사 방식을 제공할 수 있다

코드 13-7 복사 생성자

public Yum(Yum) {...};

코드 13-8 복사 팩터리

public static Yum newInstance(Yum yum){...};

- 복사 생성자와 그 변형인 복사 팩토리는 Cloneable/clone 방식보다 나은 면이 많다

- 언어 모순적이고 위험천만한 객체 생성 메커니즘(생성자를 쓰지 않는 방식) 사용하지 않으며,

- 엉성하게 문서화된 규약에 기대지 않고, 정상적인 final 필드 용법과도 충돌하지 않으며,

- 불필요한 검사 예외를 던지지 않고, 형 변환도 필요치 않다


여기서 끝이 아니다

- 복사 생성자와 복사 팩토리는 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다

- 관례상 모든 범용 컬렉션 구현체는 Collection이나 Map 타입을 받는 생성자를 제공한다

- 인터페이스 기반 복사 생성자와 복사 팩토리의 더 정확한 이름은 '변환 생성자'와 '변환 팩토리' 다

- 이들을 이용하면 클라이언트는 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다

Cloneable이 몰고 온 모든 문제를 뒤집어봤을 때, 새로운 인터페이스를 만들 때는 절대
Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다.
final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서는 검토한 후
별다른 문제가 없을 때만 드물게 허용해야 한다. 기본 원칙은 '복제 기능은 생성자와 팩토리를 이용하는 게 최고'
단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다
728x90