Cloneable은 객체를 복제할 수 있음을 명시하는 용도의 믹스인 인터페이스이다.
믹스인 인터페이스: 객체 지향 프로그래밍 언어에서 믹스인은 다른 클래스의 부모 클래스가 되지 않으면서 해당 클래스에서 사용할 수 있는 메서드를 제공하는 클래스이다.
Cloneable 인터페이스와 clone 메서드
java.lang.Cloneable 인터페이스는 Object의 clone() 메서드가 지원되는지 여부를 결정한다. 일반적으로 인터페이스를 구현하는 것은 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다는 것을 의미하지만, Cloneable은 예외적인 경우로, 이 인터페이스는 상위 클래스에 정의된 메서드의 동작 방식을 변경하는 역할을 한다.
다음은 Object 클래스에 정의된 clone() 메서드의 기본 형태이다:
protected native Object clone() throws CloneNotSupportedException;
clone() 메서드를 재정의하면서 올바르게 동작하게끔 구현하는 방법과 그 대안을 알아보자.
clone 메서드의 일반 규약
clone 메서드의 일반 규약은 다소 불명확하며, 이에 대한 강제성은 없다. 다음은 clone 메서드를 재정의하는 기본적인 예시이다:
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
하지만, 이 방식으로는 상속 구조에서 문제가 발생할 수 있다. 하위 클래스가 super.clone()을 호출할 때, 잘못된 클래스의 객체가 만들어질 수 있어 하위 클래스의 clone 메서드가 제대로 동작하지 않게 된다. 만약 clone을 재정의한 클래스가 final이라면 이러한 문제가 발생하지 않는다.
clone 메서드 구현 예제
다음은 Cloneable을 구현한 클래스를 정의하고, clone 메서드를 재정의하는 예제이다:
// PhoneNumber에 clone 메서드 추가
public final class PhoneNumber implements Cloneable {
private final short areaCode, prefix, lineNum;
public PhoneNumber(int areaCode, int prefix, int lineNum) {
this.areaCode = rangeCheck(areaCode, 999, "지역코드");
this.prefix = rangeCheck(prefix, 999, "프리픽스");
this.lineNum = rangeCheck(lineNum, 9999, "가입자 번호");
}
private static short rangeCheck(int val, int max, String arg) {
if (val < 0 || val > max)
throw new IllegalArgumentException(arg + ": " + val);
return (short) val;
}
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
}
위의 PhoneNumber 클래스는 간단한 불변 객체이기 때문에 super.clone()만으로 충분히 객체 복제가 가능하다. 하지만, 가변 객체를 복제할 때는 주의가 필요하다.
가변 객체를 가진 클래스의 clone 메서드 구현
가변 상태를 가진 클래스는 단순히 super.clone()을 호출하는 것만으로는 올바른 복제를 보장할 수 없다. 내부적으로 참조하는 가변 객체까지 모두 복제해야 하기 때문이다. 예를 들어, 다음과 같은 Stack 클래스의 복제 구현을 살펴보자:
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];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 다 쓴 참조 해제
return result;
}
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
// 원소를 위한 공간을 적어도 하나 이상 확보한다.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
위 clone() 메서드에서 elements 배열을 복제하여 새로운 배열을 생성함으로써 원본 객체와 복제된 객체가 서로 다른 배열을 참조하게 만든다. 이를 통해 원본 객체와 복제된 객체 간의 상호 간섭을 방지할 수 있다.
깊은 복사가 필요한 경우
가변 객체가 중첩되어 있는 경우에는 단순한 clone() 호출로는 충분하지 않으며, 깊은 복사를 수행해야 한다. 예를 들어, 해시 테이블의 경우 각 버킷의 연결 리스트를 복사해야 한다.
public class HashTable implements Cloneable {
private Entry[] buckets;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
@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 AssertionError();
}
}
}
위의 코드에서는 각 버킷을 깊은 복사하여 원본 해시 테이블과 복제본이 서로 다른 연결 리스트를 가지도록 한다.
clone 메서드 사용 시의 주의점
Cloneable을 구현하는 모든 클래스는clone()메서드를 재정의해야 한다. 이때 접근 제어자는public으로, 반환 타입은 클래스 자신으로 변경해야 한다.super.clone()을 호출한 후 필요한 필드를 모두 적절히 수정해야 한다. 특히, 객체의 내부 깊은 구조에 있는 가변 객체들을 복사하고, 복제본이 원본 객체와 독립적인 상태를 유지하도록 해야 한다.- 기본 타입 필드와 불변 객체 참조만 가진 클래스는 특별한 수정 없이
super.clone()만으로 충분히 복제가 가능하다.
대안: 복사 생성자와 복사 팩터리
Cloneable과 clone 메서드는 설계가 불완전하고, 구현하기 어렵다. 따라서 객체 복사가 필요할 때는 복사 생성자나 복사 팩터리를 사용하는 것이 더 좋은 대안이 될 수 있다.
예를 들어, 다음과 같은 복사 생성자를 사용할 수 있다:
public PhoneNumber(PhoneNumber original) {
this.areaCode = original.areaCode;
this.prefix = original.prefix;
this.lineNum = original.lineNum;
}
또는, 복사 팩터리 메서드를 사용할 수도 있다:
public static PhoneNumber newInstance(PhoneNumber original) {
return new PhoneNumber(original.areaCode, original.prefix, original.lineNum);
}
이러한 방식은 clone 메서드보다 명확하고, 오류를 줄일 수 있다.
결론
clone 메서드를 재정의할 때는 가변 객체를 올바르게 복제하도록 주의해야 하며, 깊은 복사가 필요한 경우에는 모든 가변 객체를 복사해야 한다. 그러나, 복사 생성자와 복사 팩터리를 사용하는 것이 더 안전하고 명확한 대안이 될 수 있다. Cloneable의 사용은 가급적 피하고, 더 나은 객체 복사 방식을 고려해야 한다.
'Programming Language > Java' 카테고리의 다른 글
| [이펙티브 자바]Item.15-클래스와 멤버의 접근 권한을 최소화해라 (0) | 2024.11.26 |
|---|---|
| [이펙티브 자바]Item.14-Comparable을 구현할지 고려하라. (0) | 2024.11.25 |
| [이펙티브 자바]Item.12-toString을 항상 재정의하라 (0) | 2024.05.14 |
| [이펙티브 자바]Item.11-equals를 재정의하려거든 hashCode도 재정의하라 (0) | 2024.05.14 |
| [이펙티브 자바]tem.10-equals는일반 규약을 지켜 재정의하라 (0) | 2024.05.14 |