JAVA/Effective Java

모든 객체의 공통 메서드 - 규칙 11 clone을 재정의할 때는 신중하라

반응형

Cloneable 객체의 복제를 허용한다는 사실을 알리는데 쓰려고 고안된 인터페이스이다.
 
하지만
 
Cloneable 선언된 메서드가 없는 마커 인터페이스로,
 
상위 클래스의 protected 멤버가 어떻게 동작할지 규정하는 용도로 쓰인다.
 
또한 
 
객체의 cloneable 구현하면, Object clone 메서드는 해당 객체를 필드 단위로 복사한 객체를 반환한다.
 
Cloneable 구현하지 않았다면, clone 메서드는 CloneNotSupportedException 던질 것이다.
 
 
Java API 문서에 기재된 clone 메서드의 명세는 다음과 같다.
 
 객체의 복사본을 만들어서 반환한다.
 - "복사" 정확한 의미는 클래스마다 다르다.
 일반적으로 x.clone() != x  참이다.
 - x.clone().getClass() == x. getClass() 반드시 참인 것은 아니다.
 - x.clone().equals(x) 또한 반드시 참인 것은 아니다.
 
=> 객체를 복사하면 보통 같은 클래스의 새로운 객체가 만들어지며내부 자료 구조까지 복사되지만 어떠한 생성자도 호출되지 않는다.
 
일반적으로 clone 정상적으로 동작하려면 해당 객체의 상위 클래스에 public 또는 protected clone 메서드가 정의되어 있어야 한다.



1
2
3
4
5
6
7
8
@Override
public Test clone() {
    try {
        return (Test) super.clone();
    } catch ( CloneNotSupportedException ex ) {
        throw new AssertionError();
    }
}
cs



그러나 만약 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
// Test 객체 클래스
public class Test implements Cloneable {
    private int a;
    private int b;
    private String c;
    private List<String> list;
}
 
// Test 클래스의 재 정의 메서드 clone
@Override
public Test clone() {
    try {
        Test clone = (Test) super.clone();
        return clone;
    } catch ( CloneNotSupportedException ex ) {
        throw new AssertionError();
    }
}
 
 
// 참조필드가 있는 경우 얕은 복사로 인해 같은 값이 변경되는 문제
public static void main(String argsp[]) {
    Test a = new Test();
    
    Test cloneA = a.clone();
    cloneA.getList().add("test");
    System.out.println(printData(a.getList()));        
    System.out.println(printData(cloneA.getList()));
}
cs



그렇기 때문에 다음과 같이 복제를 하여 얕은 복사가 아닌 깊은 복사를 진행하여
 
복제된 객체와 원래 객체가 서로 다른 객체로 분리되어 복사가 진행되어야 한다.


해결된 상황



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
// 깊은 복사를 이용한 방법
@Override
public Test clone() {
    try {
        Test clone = (Test) super.clone();
        List<String> temp = new ArrayList<>();
        temp.addAll(list); // 또는 (List<String) list.clone()
        clone.list = temp;
        return clone;
    } catch ( CloneNotSupportedException ex ) {
        throw new AssertionError();
    }
}
 
 
public static void main(String argsp[]) {
    Test a = new Test();
    
    Test cloneA = a.clone();
    cloneA.getList().add("test");
    System.out.println(printData(a.getList()));        
    System.out.println(printData(cloneA.getList()));
}
 
public static String printData(List<String> list) {
    StringBuilder stb = new StringBuilder();
    for (String st : list) {
        stb.append(st);
    }
    
    return stb.toString();
}
 
 
결과 
ddaacc
ddaacctest
cs




Cloneable 재정의  경우에는 Object.clone() 기존 방법을 그대로 계승하여야 하며,
다중스레드에 안전해야 하는 클래스는 clone 동기화 매커니즘을 만들어야 한다.
 
그렇지만 이렇게 구태여서 힘들게 clone 메서드를 구현할 필요가 있는지부터 생각해  필요가 있다.
 
객체를 복사할 대안을 제공하거나아예 복제기능을 제공하지 않는 것도  다른 방법일 수도 있다.
 
예를 들어 변경 불가능한 클래스는 객체 복제를 허용하지 않거나객체 복제를 제공하는 복사 생성자나 복사 팩토리를 제공하는 것이  나을 수도 있다.
 
왜냐하면
 

만약 제대로된 clone 메서드를 정상적으로  정의하여 사용하지 못할 경우에는 더욱 큰 문제를 야기 할 수 있기 때문이다.

그러므로 

Cloneable 인터페이스를 계승하지 말고 복사생성자나 복사 팩토리를 이용하여 복사 개념을 사용하는 것이  현명할  있다.


출처 : 조슈아 블로크, 『 Effective Java 2/E』, 이병준 옮김, 인사이트(2014.9.1), 규칙11 인용.


반응형