- 이 글은 Effective Java 를 기반으로 작성되었습니다.
- Cloneable 인터페이스
Cloneable 은 객체의 복제를 허용한다는 사실을 알려주는 믹스인(mixin) 인터페이스이다. 하지만 문제점이 한 가지 있는데 clone 메소드가 없고 Object 의 clone 메소드의 접근제어자가 protected 라는것이다. 이렇게 되면 clone 메서드가 구현되어있으리라는 보장이 없는데 그럼에도 불구하고 널리 사용되므로 알아둘 필요가 있다.
Cloneable 은 protected 접근제어자를 갖는 Object 의 clone 메소드가 어떻게 동작할지를 정하는 역할을 한다. clone 메서드가 정의되어있지도 않은 Cloneable 인터페이스를 도대체 왜 신경써야하는지 의문이 들 수 있다. 그냥 Cloneable 인터페이스를 신경쓰지 않고 Object의 clone 을 override 하면 되는거 아닌가?
* @return a clone of this instance.
* @throws CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see java.lang.Cloneable
위의 문구는 clone 메서드에 명시된 주석이다. 객체의 클래스가 Cloneable 인터페이스를 구현하지 않으면 CloneNotSupportedException 을 던진다고 되어있다.
public class CloneTest{
public static void main(String[] args) throws CloneNotSupportedException {
CloneTest test = new CloneTest();
test.clone();
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
.........
Exception in thread "main" java.lang.CloneNotSupportedException: effectivejava.clone.CloneTest
at java.lang.Object.clone(Native Method)
at effectivejava.clone.CloneTest.clone(CloneTest.java:14)
at effectivejava.clone.CloneTest.main(CloneTest.java:8)
위의 코드를 실행해보면 clone() 메소드를 재정의해도 주석에 명시된 Exception이 발생한다. Cloneable 인터페이스를 구현해야만 오류가 발생하지 않는다.
인터페이스를 이런식으로 이용하는것은 좋은 사례가 아니다. 일반적으로 인터페이스란 사용자에게 어떤 기능이 수행될 수 있다는것을 알려주어야 한다. 하지만 Cloneable 은 상위 클래스의 protected 멤버가 어떻게 동작하는지 규정하는 용도로 사용하는데 이런식으로 인터페이스를 사용하지는 말도록 하자.
- Clone 메서드의 일반 규약
equals 의 일반규약은 상당히 엄격한 편이었지만 clone 의 일반규약은 다소 느슨한편이다. 객체 x에 대하여
- x.clone() != x 가 참이다.
- x.clone().getClass() == x.getClass() 는 참이지만 반드시 그래야만 하는것은 아니다.
- x.clone().equals(x) 는 참이 되겠지만 반드시 그래야 하는것은 아니다. 객체 복사시 같은 클래스의 신규 객체가 생성되는데 내부 자료구조까지 복사해야할수도 있다. 이때 어떤 생성자도 호출하지 않는다.
3 의 어떤 생성자도 호출하지 않는다는 규약은 너무 엄격하다. final 인 경우 생성자를 사용해서 반환하도록 clone()을 구현할 수도 있기 때문이다.
반면 2의 규약은 너무 느슨하다. 프로그래머들은 하위 클래스에서 super.clone() 을 호출하면 하위 클래스 객체가 반환될거라고 생각한다. 하위 클래스에서 super.clone() 을 호출했을 때 상위클래스의 clone() 이 호출될것이고, 상위클래스의 clone() 에서 이런기능을 제공하려면 다시 super.clone() 을 호출해야 한다. 만일 상위클래스의 clone() 메소드 안에서 생성자를 호출해서 객체를 반환하면 반환된 객체의 클래스는 사용자가 원하던 클래스(하위 클래스의 clone() 을 호출했을때 기대한 결과)가 아니게 된다. 따라서 비-final 클래스의 clone을 재정의할 때는 반드시 super.clone을 호출해서 얻은 객체를 반환해야 한다. 이 매커니즘은 생성자들이 연결되는 매커니즘과 닮았지만 clone 에서는 이 사항이 강제적인것이 아니라는데에 차이점이 있다.
- 올바른 clone() 메서드
실질적으로 Cloneable 인터페이스를 구현하는 클래스는 제대로 동작하는 public clone 메서드를 제공해야 한다. 또한 해당 클래스의 상위 클래스도 제대로된 public 또는 protected clone 을 제공해야 한다.
- 기본 자료형 + immutable 클래스의 clone
모든 Field 가 기본 자료형이거나 변경 불가능 클래스라면 clone 메소드 작성시 특별한 작업을 하지 않아도 괜찮다. Cloneable 인터페이스를 구현하고 Object 클래스의 clone 메소드에 접근할 수 있게 public clone 을 제공하면 된다.
public class PhoneNumber implements Cloneable{
private final short areaCode;
private final short prefix;
private final short lineNumber;
public PhoneNumber(int areaCode, int prefix, int lineNumber) {
this.areaCode = (short) areaCode;
this.prefix = (short) prefix;
this.lineNumber = (short) lineNumber;
}
@Override public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
위의 clone 메소드에서는 Object 가 아니라 PhoneNumber 형을 반환했다. 이렇게 하면 사용하는 Client 는 형변환을 하지 않아도 된다.
- 변경 가능한 FIeld 의 Clone
복제할 객체가 변경 가능한 Field 를 참조하고 있다면 Clone 메소드 작성시 주의를 해야 한다. Stack 의 경우에는 원소들을 보관하고 있는 Object 의 배열을 갖고 있다.
public class Stack {
private Object[] elements;
private int size;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
이런 상황에서 단순하게 clone() 을 구현하면 원본과 복사본의 elements 는 같은 객체를 참조하게 된다. 원본을 변경할 때 복사본도 변경되어 올바르게 동작하지 않을 수 있다. clone 은 또 다른 생성자다. 이런 변경 가능한 Field 를 참조하고 있는 클래스의 clone 작성시에는 내부구조도 복사해야 한다.
@Override public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = this.elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
- clone 시 DeepCopy 가 필요한 경우
위의 Stack 의 예제처럼 내부 구조를 복사해도 충분치 않은 경우가 있다. bucket 을 사용하는 HashTable 의 경우 아래와 같은 구조로 되어있을 것이다.
public class HashTable {
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;
}
}
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = this.buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
위의 코드처럼 작성하면 배열은 복사본을 갖지만 배열의 각 원소는 원래의 배열 원소와 동일한 연결리스트를 참조하게 된다. 이 문제를 해결하려면 동일한 연결리스트를 갖지 않도록 Entry 의 원소도 복사하도록 deepCopy 를 지원해야 한다.
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() {
return new Entry(key, value,
next == null ? null : next.deepCopy());
}
}
clone 메소드도 수정해주도록 하자.
@Override public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for(int i = 0; i < buckets.length; i += 1){
if(buckets[i] != null) {
result.buckets[i] = buckets[i].deepCopy();
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
buckets 복사시 자신의 길이를 할당하고, 각 버킷의 원소(Entry)를 deepCopy 한다. Entry 복사시에도 자신의 다음 참조를 재귀적으로 deepCopy 하므로 원본 객체와 복사본 간의 사이드 이펙트 없이 복제를 할 수 있게 되었다.
clone 메소드 재정의의 영역을 벗어나긴 하지만 위의 deepCopy 는 원소가 작을 때에만 잘 동작한다. 원소가 커지면 재귀호출시 원소마다 스택 프레임을 사용하기 떄문에 스택 오버플로우가 발생한다. 이를 방지하려면 재귀호출이 아닌 순환문으로 변경해야 하는데 deepCopy를 아래와 같이 수정해준다.
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;
}
- clone 과 스레드 세이프
알아둬야할 세부사항이 한 가지 더 있는데 다중 스레드에서 안전한 클래스에 대해 Cloneable 을 구현하려면 clone 메소드도 스레드 세이프(동기화 지원)하게 작성되어야 한다. Object의 clone은 스레드-세이프하지 않기 때문이다.
- 복자 생성자와 복사 팩토리
여태까지 clone을 올바르게 작성하는법을 알아보았지만 사실 clone을 제공하면서 이 모든 복잡한 세부사항을 지켜야 하는가에 대해서 근본적으로 생각해볼 필요가 있다. 만약 Cloneable 인터페이스를 구현하는 클래스를 계승한다면 clone 메소드를 구현해야 하지만 그런 경우가 아니라면 복제 기능을 제공하지 않는 쪽이 더 낫다.
객체의 복제가 필요하다면 아래와 같이 복사 생성자나 복사 팩토리를 제공하는 쪽을 생각해보자.
public HashTable(HashTable hashTable)
public static HashTable newInstance(HashTable hashTable)
'Language > Java' 카테고리의 다른 글
이펙티브 자바 - 클래스와 멤버 접근 권한 (0) | 2021.10.19 |
---|---|
이펙티브 자바 - Comparable (0) | 2021.10.17 |
이펙티브 자바 - toString 재정의 (0) | 2021.10.04 |
이펙티브 자바 - hashCode 재정의 (2) | 2021.10.02 |
이펙티브 자바 - equals 재정의 (0) | 2021.09.23 |
댓글