본문 바로가기

Language & Framework/Java

이펙티브 자바 (9) Cloneable 재정의는 주의해서 진행하라

 

 

이 글은 조슈아 블로크의 Clone 혐오가 너무 재미있어서 정리한 것이다.

따라서 저자인 조슈아 블로크가 인터뷰에서 언급한 Cloneable에 대한 의견부터, 책에 나온 단점까지 모두 열거한 뒤 본론은 뒤에서 짤막하게만 다룰 것이다.

 

안읽어도 되니까 쓰지 말자.. 아래 인터뷰는 무려 2002년 인터뷰다.

2002년부터 이렇게 쓰지 말라고 하는데 예의상 쓰지 말자.. ㅎ

 

그래도 궁금하다면 우선 아래 장문의 인터뷰를 읽으며 저자의 Cloneable 혐오를 느껴보자. ^^~~

나의 독해 실력은 믿을 것이 못되고 제멋대로 의역했기 때문에 영문으로 읽는 것이 좋다.

 

Copy Constructor versus Cloning
생성자 복제 VS Cloning

Bill Venners: In your book you recommend using a copy constructor instead of implementing Cloneable and writing clone. Could you elaborate on that?
당신의 책에서, 당신은 Cloneable을 구현하고 clone 메서드를 사용하는 것 대신 생성자 복제를 사용할 것을 권장했습니다.
그것에 대해 더 깊은 설명을 부탁드릴 수 있을까요?

Josh Bloch: If you've read the item about cloning in my book, especially if you read between the lines, you will know that I think clone is deeply broken.
만약 네가 내 책에서 Cloning에 대한 부분을 읽었다면, 특히나 밑줄 그어놓은 부분을 읽었다면, 너는 내가 clone이 애초에 글러먹은 존재라고 생각한다는 사실을 알 것이다.

There are a few design flaws, the biggest of which is that the Cloneable interface does not have a clone method. And that means it simply doesn't work: making something Cloneable doesn't say anything about what you can do with it.
그것에는 약간의 디자인적 결함이 있으며, 가장 심각한 것은 Cloneable 인터페이스가 clone 메서드를 가지지 않는다는 점이다.
그리고 그것은 의미한다, Cloneable이 간단하게 동작하지 않는다는 것을.
Cloneable을 만드는 것만으로 네가 그것으로 무엇을 할 수 있을지 아무런 의사도 표현해주지 않는다.

Instead, it says something about what it can do internally. It says that if by calling super.clone repeatedly it ends up calling Object's clone method, this method will return a field copy of the original.
대신, 이것은 내부적으로 무엇을 할 수 있는지를 말한다. (소스코드 주석 말하는 것인 듯.)
이것은 말한다, super.clone을 반복 호출하게 됨으로써 이것이 Object의 clone 메서드를 호출하게 될 때 원본의 복사본을 반환하게 될 것이라고,

But it doesn't say anything about what you can do with an object that implements the Cloneable interface, which means that you can't do a polymorphic clone operation.
하지만 이것은 전혀 말해주지 않는다, 네가 Cloneable을 구현한 객체로 무엇을 할 수 있는지.
즉 이것은 네가 clone 명령어로 다형성을 구현할 수 없음을 의미한다.

If I have an array of Cloneable, you would think that I could run down that array and clone every element to make a deep copy of the array, but I can't.
Cloneable의 배열을 가지고 있다면, 해당 배열의 모든 요소를 깊은 복제하여 새로운 배열을 생성할 수 있을 것이라고 생각하겠지만, 전혀 그렇지 않다.

You cannot cast something to Cloneable and call the clone method, because Cloneable doesn't have a public clone method and neither does Object.
너는 Cloneable을 캐스트하여 clone 메서드를 호출할 수 없다, 왜냐면 Cloneable은 public clone method를 가지고 있지도 않고, Object도 아니기 때문이다.

If you try to cast to Cloneable and call the clone method, the compiler will say you are trying to call the protected clone method on object.
만약 네가 Cloneable로 캐스트하고 clone 메서드를 호출한다면, 컴파일러는 네가 protected clone 메서드를 호출하고 있다고 말할 것이다.

The truth of the matter is that you don't provide any capability to your clients by implementing Cloneable and providing a public clone method other than the ability to copy.
문제의 진실은, 그것이 클라이언트에게 Cloneable 구현과 public clone 메서드를 던져주는 것 외에 복사에 대한 어떤 기능도 제공하지 않는다는 것이다.

This is no better than what you get if you provide a copy operation with a different name and you don't implement Cloneable. That's basically what you're doing with a copy constructor.
이것은 네가 다른 이름으로 copy 명령어를 만들고 Cloneable을 구현하지 않는 것보다 나은 점이 전혀 없다.
기본적으로 생성자 복제를 사용하는 방법이 있다.

The copy constructor approach has several advantages, which I discuss in the book. One big advantage is that the copy can be made to have a different representation from the original.
생성자 복제 접근 방식에는 몇가지 장점이 있다, 나는 그것들을 내 책에서 이야기했다.
가장 큰 장점은 이 복제 방식으로 원본과 다른 representaition을 만들 수 있다는 것이다. (다른 타입으로 변환하여 복제할 수 있다는 뜻인 듯.)

For example, you can copy a LinkedList into an ArrayList.
Object's clone method is very tricky. It's based on field copies, and it's "extra-linguistic." It creates an object without calling a constructor.
예를 들어, 너는 LinkedList를 ArrayList로 복제할 수 있다.
Object의 clone 메서드는 굉장히 까다롭다. 그리고 이것은 "extra-linguistic"하다. (자바 기본 원칙과 다르게 움직인다는 말인 듯). 이것은 생성자 없이 객체를 생성한다.

There are no guarantees that it preserves the invariants established by the constructors. There have been lots of bugs over the years, both in and outside Sun, stemming from the fact that if you just call super.clone repeatedly up the chain until you have cloned an object, you have a shallow copy of the object. The clone generally shares state with the object being cloned.
clone은 생성자로부터 불변성을 보존할 것을 보장하지 않는다.
clone의 반복적인 체인 호출 끝에 얕은 복사된 객체를 반환한다는 사실 때문에 Sun(자바 만든 회사)는 몇 년간 안밖으로 많은 버그를 겪었다.
clone은 일반적으로 복사된 객체와 상태를 공유한다.

If that state is mutable, you don't have two independent objects. If you modify one, the other changes as well. And all of a sudden, you get random behavior.
There are very few things for which I use Cloneable anymore.
만약 해당 값이 mutable하다면, 너는 독립적인 두 객체를 가질 수 없다.
만약 네가 하나를 수정한다면, 다른 객체도 수정될 것이다.
그리고 어느 순간, 너는 무작위 행동을 가지게 될 것이다. (아마 모르고 쓰면 버그의 원인이 된다는 말인 듯.)
Cloneable을 계속 사용하는 것은 아무런 의미가 없다.

I often provide a public clone method on concrete classes because people expect it. I don't have abstract classes implement Cloneable, nor do I have interfaces extend it, because I won't place the burden of implementing Cloneable on all the classes that extend (or implement) the abstract class (or interface). It's a real burden, with few benefits.
나는 가끔 public clone method를 구체 클래스에 제공한다, 그것을 원하는 사람들이 있기 때문이다.
하지만 추상클래스에는 절대 Cloneable을 구현하지 않으며 Interface를 확장하는 방식으로도 마찬가지다.
왜냐면 Cloneable을 구현하는 것이 (계속해서 상속받는 입장에서는 구현이 필요하기 때문에) 큰 부담이 되기 때문이다.
이는 부담에 비해서 아무런 이득이 없는 행동이다.

Doug Lea goes even further. He told me that he doesn't use clone anymore except to copy arrays. You should use clone to copy arrays, because that's generally the fastest way to do it. But Doug's types simply don't implement Cloneable anymore. He's given up on it. And I think that's not unreasonable.
Doug Lea는 한 술 더 뜬다, 그는 나에게 clone 메서드는 배열을 복제하는 예외적인 경우를 제외하고는 아예 쓰지 않는다고 말했다. clone은 배열 복제에 사용하는것은 좋다, 그것은 일반적으로 빠른 방법이기 떄문이다. 
그런 경우를 제외하고 Doug는 Cloneable은 아예 구현하지 않는다.

그는 Cloneable을 완전히 포기했으며, 난 그것이 합리적이라고 생각한다.

It's a shame that Cloneable is broken, but it happens. The original Java APIs were done very quickly under a tight deadline to meet a closing market window. The original Java team did an incredible job, but not all of the APIs are perfect. Cloneable is a weak spot, and I think people should be aware of its limitations.
Cloneable이 엉망이라는 사실은 부끄러운 것일 수도 있지만, 어쩔 수 없는 일이다. The Original Java APIs는 타이트한 일정에 맞춰서 급하게 만들어졌다.
The Original Java Team은 굉장히 믿을 수 없을 정도로 훌륭하지만, 그들이 만든 모든 API가 완벽하지는 않다.
Cloneable은 약한 부분이다, 그리고 나는 사람들이 그 한계를 알아야 한다고 생각한다.

 

이외에 책에 열거되어 있는 단점은 다음과 같다.

 

1. clone 메서드가 선언된 곳이 Clneable이 아닌 Object이며 protected이다. 따라서 Cloneable을 구현하는 것만으로 clone 메서드를 호출할 수 없다.

2. 예외를 굉장히 이상하게 처리하여, Cloneable을 구현하지 않은 객체가 clone을 호출할 경우 CloneNotSupportedException을 던지는데, 이게 Checked Exception이다.

아무런 해결 방법이 없는 예외인데 말이다.

3. clone 메서드의 규약이 굉장히 허술하다. 이는 아래에서 잠시 다루겠다.

4. 마치 완전한 복제가 될 것 같은 뉘앙스를 풍기지만, 실은 얕은 복사로 이를 알지 못하는 클라이언트에게 각종 버그를 유발한다.

5. clone의 구현 방식으로 인해, 사실상 불변 객체로 만들 수 없게 된다.

 

여기까지 읽었다면 정말 독한 사람이다. 

정 궁금하다면 clone의 실제 사용에 대해 살펴보자.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@ToString
public class PhoneNum implements Cloneable {
    private int areaCode;
    private int prefix;
    private int lineNum;
 
    public PhoneNum(int areaCode, int prefix, int lineNum) {
        this.areaCode = areaCode;
        this.prefix = prefix;
        this.lineNum = lineNum;
    }
 
    @Override
    public PhoneNum clone() {
        try {
            return (PhoneNum) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
 
    public static void main(String[] args) {
        PhoneNum phoneNum = new PhoneNum(2231114423);
        HashMap<PhoneNum, String> map = new HashMap<>();
        map.put(phoneNum, "냠냠");
 
        PhoneNum clone = phoneNum.clone();
        System.out.println(map.get(clone)); 
        System.out.println(clone != phoneNum); 
        System.out.println(clone.getClass() == phoneNum.getClass());
        System.out.println(clone.equals(phoneNum));
    }
}
cs

 

단점에 대해 다시 한 번 언급하는 일이긴 한데, 위 clone 메서드에서 보이는 CloseNotSupportedException은 잘못 만들어진 예외다.

해당 예외는 만약 Cloneable을 구현하지 않은 객체가 clone()을 호출할 경우 발생하는 예외인데, 이미 Cloneable을 구현하지 않은 상태에서 런타임 환경에 Cloneable을 붙여줄 수 있을까?

전혀 해결 방법이 없는데 쓸데 없이 checked exception을 던져서 불필요한 코드만 늘리고 있다.

우리는 이런 짓을 하지 말도록 하자.

 

다시 본론으로 돌아가서 위 clone을 구현한 PhoneNum 클래스의 인스턴스를 생성한 후, clone() 메서드를 이용해 복제하여 HashMap에 넣는다.

이후 clone 객체를 key로 사용하여 오리지널 인스턴스와 key-value로 이어진 값인 "냠냠"을 가져올 수 있을까?

clone != phoneNum은 true일까 false일까?

 

가져올 수 없고, clone != phoneNum은 true이다.

 

 

이런 기이한 규약에 그렇게 정해져있다.

 

1. x.clone은 x와 == 관계가 성립하지 않아야 한다.

근데 이걸 메서드에서 보장해주는 건 아니고 (ㅋㅋ) 클라이언트가 알아서 그렇게 구현해줘야 한다.

이를 위해서는 내부에 참조 객체가 있을 경우 구현을 추가해줘야 하는데, 덕분에 불변 객체를 만들 수 없게 된다.

 

2. x.clone().getClass() == x.getClass()여야 하는데, 꼭 그런 건 아니다. (ㅋㅋ)

 

3. x.clone().equals(x) 기본적으로 true여야 하는데, 이것도 꼭 그런 건 아니다.(ㅋㅋ)

=> equals를 재정의하지 않은 커스텀 클래스의 경우에 너네 알아서 해라~라는 의미인 것 같다.

 

이런 알쏭달쏭한 규약으로 인해서 클라이언트는 clone()메서드가 어떤 결과물을 반환할지 확신할 수 없거나, 혹은 잘못된 생각을 가지고 사용하게 만든다.

그리고 위 코드에서 clone != phoneNum은 true, clone.getClass() == phoneNum.getClass()는 true인데..

 

clone.equals(phoneNum)은 false다.

equals를 재정의하지 않았으니까요..

(참고로 Object의 기본 equals는 return this == obj를 리턴하기 때문에 해시코드 비교와 결과가 같다.)

 

벌써 어질어질함이 느껴질 것이다. 대체 이 메서드는 직접 구현하는 것보다 나은 게 뭘까?

다음 예시는 조금 더 복잡하다.

 

 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import java.util.Arrays;
import java.util.EmptyStackException;
import java.util.Map;
 
public class EffectiveStack<extends Object> implements Cloneable {
    public T[] elements;
    private int size = 0;
 
    private static final int DEFAULT_INITIAL_CAPACITY = 16;
 
    public EffectiveStack() {
        this.elements = (T[]) new Object[DEFAULT_INITIAL_CAPACITY];
    }
 
    public void push(T t) {
        ensureCapacity();
        elements[size++= t;
    }
 
    public Object pop() {
        if (size == 0throw new EmptyStackException();
        Object result = elements[--size];
        elements[size] = null;
        return result;
    }
 
    public void ensureCapacity() {
        if (elements.length == size) {
            elements = Arrays.copyOf(elements, 2*size+1);
        }
    }
 
    @Override
    public EffectiveStack clone() {
        try {
            EffectiveStack clone = (EffectiveStack) super.clone();
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
 
    public static void main(String[] args) {
        PhoneNum phoneNum1 = new PhoneNum(11111111111);
        PhoneNum phoneNum2 = new PhoneNum(2222222222);
 
        EffectiveStack effectiveStack = new EffectiveStack();
        effectiveStack.push(phoneNum1);
        effectiveStack.push(phoneNum2);
 
        EffectiveStack clone = effectiveStack.clone();
        PhoneNum pop1 = (PhoneNum) effectiveStack.pop();
        PhoneNum pop2 = (PhoneNum) clone.pop();
 
        System.out.println(pop1 == pop2);
        System.out.println(pop1.equals(pop2));
    }
}
 
cs

 

 

다른 부분은 크게 중요하지 않고 clone과 main 메서드만 보면 된다.

다음과 같은 Stack 클래스를 직접 구현했을 때, 만약 Stack 객체를 clone()하고 둘 중 하나의 내부 배열을 변경하면 어떻게 될까?

 

위에서 이야기했듯이 얕은 복사이기 때문에 하나를 변경하면 나머지 하나도 변경된다.

이런 문제점을 해결하려면 직접적으로 내부 객체도 복사해주는 처리가 필요하다.

 

 

 

1
2
3
4
5
6
7
8
9
10
11
12
    @Override
    public EffectiveStack clone() {
        try {
            EffectiveStack clone = (EffectiveStack) super.clone();
            clone.elements = this.elements.clone();
            // TODO: copy mutable state here, so the clone can't change the internals of the original
            return clone;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
cs

 

 

 

와 ! 이제 내부의 배열까지 clone()메서드를 활용하여 복사했기 때문에 깊은 복사가 완료되었다.

라고 생각했다면 반성해야 한다.

 

해당 배열의 타입은 <T extend Object>이다. 즉, 객체라는 것이다.

그러면 배열을 clone으로 복제한다고 내부의 객체까지 deep copy가 될까?

 

당연히 안된다.

이를 해결하기 위해서는 아마 다음과 같은 방법이 필요할 것이다.

우리가 복제하려는 단일 객체를 복사할 수 있는 메서드를 만들고, clone 내부에서 반복문을 돌리면서 하나하나 복사하여 clone.elements[i] 안에 넣어주는 것이다.

 

그런데 우리가 지금까지 봤을 때, 과연 Cloneable과 clone() 메서드를 그렇게까지 해가며 사용해야할 이유가 있을까?

상속해야 한다는 노골적인 디메리트까지 존재하는데 말이다.

우리에게는 정적 팩터리 메서드와 생성자 방식이 존재한다.

 

정적 팩터리 메서드는 아래 글에서 다룬 적이 있으니 궁금하면 읽어보자.

 

https://7357.tistory.com/259?category=1060668 

 

그래도 정말 굳이 굳이 굳이 clone을 사용해야겠다면 저자가 언급하는 주의점은 다음과 같다.

 

1. 배열 복제 시에는 배열의 clone 메서드를 사용해라.

2. 필요한 경우 직접 deep copy를 구현해야 한다.

3. 오버라이딩할 수 있는 메서드는 참조하지 말아라. (하위클래스에서 해당 메서드를 오버라이딩할 경우 동작 자체가 망가질수 있다.)

4. 상속용 클래스(Abstract, Interface)에서는 절대 Cloneable을 사용하지 말도록 하자.

5. 어쩔 수 없이 누군가가 Cloneable을 구현한 클래스를 상속해야 한다면 그냥 해당 메서드를 아예 못 쓰는 메서드로 막아버려라.

6. clone()은 Thread-Safe하지 않기 때문에 멀티 쓰레드 환경에서는 동기화 처리를 해라.

 

어려운 내용은 아니고, 사실 clone에 대해 깊이 설명하는 것이 가치 없게 느껴지기 때문에 예제는 따로 없다.

마지막으로 저자의 clone() 혐오를 다시 한 번 강렬하게 느끼며 "그냥 쓰지 말자"라는 6글자를 마음속에 새기도록 하자.

 

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