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 인용.
'JAVA > Effective Java' 카테고리의 다른 글
클래스와 인터페이스 - 규칙 13 클래스와 멤버의 접근권한은 최소화하라. (0) | 2018.05.29 |
---|---|
모든 객체의 공통 메서드 - 규칙 12 Comparable 구현을 고려하라. (0) | 2018.05.29 |
모든 객체의 공통 메서드 - 규칙 10 toString은 항상 재정의하라 (0) | 2018.05.29 |
모든 객체의 공통 메서드 - 규칙 9 equals를 재정의할 때는 반드시 hashCode도 재정의하라 (0) | 2018.05.29 |
모든 객체의 공통 메서드 - 규칙 8 equeals 재정의할 때는 일반 규악을 따르라 (0) | 2018.05.29 |