반응형

ITEM 13 "clone 재정의는 주의해서 진행하라"

 

현재 객체를 지금 상태로 복사해서 생성하고 싶을 때가 있다.

자바에서는 최고 조상 Object 에서 clone 메서드를 통해 객체를 복사 생성할 수 있게 기능을 제공하고 있다. 하지만 하위 클래스의 어떤 필드가 있는지 어떻게 알고 객체를 복사해준다는 것일까?...

 

Cloneable 이라는 인터페이스를 추가로 제공하고 있는데 인터페이스에서 Object clone 메서드의 동작 방식을 결정한다. (Cloneable 인터페이스 구현이 공개되어 있지 않아 정확한 동작 방식은 모르지만, Object clone 메서드 내용을 바꾸는 것으로 추정된다.) 하위 모듈에서 상위 모듈의 동작 방식에 관여한다는 것이다. 객체지향 세계에서는 상위 모듈에서 하위 모듈로 실행흐름이 전이되는 것이 국룰인데 지금 방식은 하극상(?) 지원하는 것이다.

 

심지어 Object clone 메서드가 protected 라서 단순히 Cloneable 인터페이스를 구현한다고 하여 clone 메서드를 사용할 없다. Cloneable 인터페이스를 구현한 클래스의 인스턴스에서 clone 메서드를 호출해야만 객체의 필드들을 하나씩 복사해 객체를 반환한다. Cloneable 인터페이스를 구현하지 않은 클래스에서 clone 메서드를 호출하면 CloneNotSupportedException 예외를 던진다.

 

Object 정의된 clone 메서드의 일반 규약도 허술하다.

x.clone() != x, x.clone().getClass() == x.getClass() 속성만 필수로 지원하고 x.clone().equasl(x) 속성이 필수는 아니라고 적혀있다.

동치성을 보장하지 않는다면 복사한 의미가 있을까? 같은 객체라고 있을까? 필수 속성만 고려한다면 생성자로도 충분히 인스턴스를 반환할 있다. super.clone 호출하지 않고 생성자를 생성해서 반환한다면 클래스를 상속 받은 하위 클래스에서 super.clone 메서드를 호출했을 잘못된 클래스의 객체가 만들어지는 불상사가 생긴다.

 

cloneable 인터페이스와 clone 메서드 제대로 활용하기

위험한 설계를 가진 복사체계이지만, 여전히 많은 자바 라이브러리에서 사용한다고 한다. 제대로 활용하는 방법은 숙지할 필요가 있다.

 

class PhoneNumber implements Cloneable {
    @Override
    public PhoneNumber clone() {
        try {
            return (PhoneNumber) super.clone();
        } catch(ClassNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

 

모든 필드가 기본 타입이거나, 불변 객체를 참조한다면, 위와 같이 super.clone 메서드만 호출하고 PhoneNumber 객체로 로 변환해서 반환하기만 하면 된다. (covariant return typing)

 

문제는 가변 객체를 참조할 때이다. 이 문제는 shallow copy 문제점이라 다른 언어에서도 고려해야 하고 맨 마지막에서 서술할 복사 생성자, 복사 팩토리 메서드에 작성할 때도 고려해야 할 대상이라는 점임을 참고하자.

 

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

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

    /* 생략 */

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

 

위 스택 코드에서 size 필드는 기본 타입이므로 올바른 값을 가지지만, 복사된 elements 배열은 원본 Stack 인스턴스의 elements 배열을 참조할 것이다. 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다. 이런 문제점을 shallow copy 라고 한다. 이 반대 개념이 deep copy 라고 하는데 참조 변수들을 모두 생성하여 내용을 복사한다.

 

다행히도 배열 내부에 정의된 clone 메서드가 충분히 제 역할을 하고 있다. 결과를 Object[] 로 형변환하지 않아도 알아서 원본 배열과 같은 타입을 반환한다. 그래서 위와 같이 배열의 clone 문구만 추가해주면 끝난다.!

 

그런데 만약 저 elements 필드가 final 이라면 복제할 수가 없다. 이는 Cloneable 아키텍처가 '가변 객체를 참조하는 필드는 final 로 선언하라' 라는 용법과 충돌한다. 복제가 가능한 클래스로 만드려면 final 한정자를 제거해야 될 수도 있다.

 

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

    @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 Assertion();
        }
    }
}

 

이번엔 객체를 담은 배열이다. 객체 내부에 연결 리스트가 있어서 위와 같이 배열이라고 생각하고 clone 만 명시할 경우 같은 객체를 참조하게 된다. 위와 같이 직접 모두 생성해서 연결해주어야 한다.

 

끝으로 이런 상위 클래스에 의존하는 설계는 하위 클래스에서 독이 된다. 상속용 클래스는 Cloneable 을 구현해서는 안 된다. 제대로 clone 메서드를 작성해도 하위 클래스에서 cloneable 구현 여부를 직접 결정한다.

또, Object 의 clone 메서드는 thread-safe 하지 않다. 동기화를 신경쓰지 않았다. 재정의해서 동기화해주어야 한다.

 

요약하면, 아래와 같다.

1 . Cloneable 인터페이스를 구현한 모든 클래스는 clone 메서드를 재정의해야 한다.

이 때 clone 메서드는 public 이어야 하며, 반환 타입은 자신의 클래스로 변경해야 한다.

2 . 이 메서드는 가장 먼저 super.clone() 을 호출하여 주요 필드를 전부 적절히 수정한다.

이 객체 내부에 있는 모든 가변 객체들을 복사하고, 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리켜야 한다.

다시 말해, 원본 객체의 필드와 공유되지 않도록 deep copy 를 해야 한다는 소리이다.

=> 가급적 사용하지 말자. 복잡하다.

 

복사 생성자와 복사 팩토리 메서드가 더 나을 수 있다.

사실 자바 clone 메서드는 다른 언어에서 복사 생성자와 같은 역할을 한다. 같은 유형의 객체를 복사 생성하면서 독립적인 객체를 만들어 준다는 면에서 거의 똑같다. 복사 생성자와 복사 팩토리 메서드는 자신과 같은 클래스의 인스턴스를 받아 자기 자신의 인스턴스를 생성한다. 프로그래머 입장에서 더 유연하고 clone 을 충분히 대체할 수 있다.

 

대체할 수 있다고 해서 위에서 소개한 가변 필드들을 deep copy 안 하면 안 된다. 단지 선언 공간만 바뀐 것이다.

 

shallow copy 의 위험성

deep copy 안 했을 경우, 다른 객체를 통해 필드들을 제어할 수 있어 보안사고로 이어진다. 아래는 C++ 의 shallow copy 의 위험성을 알리기 위해 만들어진 pwn 문제(바이너리의 취약점을 찾아 쉘을 획득하는 문제) 로 풀어보는 것을 권장한다. pwnable.tw(pwnable.tw 의 Ghost Party 문제)

 

필자는 풀었다.

 

반응형

+ Recent posts