13. clone 재정의는 주의해서 진행하라
Cloneable의 문제점
Mixin 인터페이스의 실패:
clone() 메서드가 선언된 곳
Object
접근제어자
protected
외부에서 직접 호출 불가
리플렉션은 불안정
이런 이유들로
Cloneable
구현만으로는 복제 기능 활용 불가
널리 쓰이는 방식:
문제점에도 불구하고
Cloneable
방식은 여전히 널리 사용되므로, 개발자는 그 동작 방식과 주의사항을 숙지할 필요가 있습니다.
Cloneable 인터페이스의 특이한 역할
메서드 없는 인터페이스
clone()
동작 방식:Cloneable
구현 클래스의 인스턴스에서 모든 필드를 복사한 객체 반환구현하지 않은 경우
CloneNotSupportedException
인터페이스의 역할 재정의:
인터페이스의 일반적인 역할 : 정의 기능 제공 선언
Cloneable
은 상위 클래스(Object
)의protected
메서드 동작 방식을 변경
따라 하지 말 것
Object.clone() 메서드 명세의 허술함
모호한 복사 정의:
Object.clone()
명세는 "복사"의 정확한 의미를 클래스의 구현에 의존.
느슨한 일반 규약:
x.clone() != x
,x.clone().getClass() == x.getClass()
x.clone().equals(x) == true
super.clone()
관례:clone()
메서드 내에서super.clone()
호출은 관례, 강제 사항 X.하위 클래스의 복제 기능 손상 위험
class BaseClass implements Cloneable { private String baseName; //... @Override public BaseClass clone() { try { BaseClass cloned = (BaseClass) super.clone(); return cloned; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } } class SubClass extends BaseClass { private int subValue; //... @Override public SubClass clone() { SubClass cloned = (SubClass) super.clone(); // cloned.subValue = this.subValue; return cloned; } }
클래스 종류별 clone() 구현 방식
final 클래스:
super.clone()
호출 불필요,Cloneable
구현 불필요생성자를 직접 호출하여 복제하는 것이 효율적일 수 있습니다.
상속 가능한 클래스:
super.clone()
호출 (관례)깊은 복사 구현 필요 (가변 객체 필드)
공변 반환 타이핑 (Covariant Return Typing)
하위 클래스 반환 타입:
Java 5부터 공변 반환 타이핑 지원으로, 하위 클래스에서
clone()
재정의 시 반환 타입을 하위 클래스 타입으로 지정 가능합니다.
형변환 불필요:
클라이언트 코드에서 불필요한 형변환을 줄여줍니다.
// Java 4 (JDK 1.4) class SubClass extends BaseClass { @Override public BaseClass clone() { // ... return super.clone(); } } // 사용 시 SubClass class = new SubClass(); SubClass clonedClass = (SubClass) class.clone(); // 명시적 타입 변환 필요
// Java 5 (JDK 1.5) 이상 class SubClass extends BaseClass { @Override public SubClass clone() { // SubClass 타입으로 반환 가능 (공변 반환 타이핑) // ... return (SubClass)super.clone(); } } // 사용 시 SubClass class = new SubClass(); SubClass clonedClass = class.clone(); // 타입 변환 불필요!
CloneNotSupportedException 예외 처리 비판
검사 예외의 부적절성:
CloneNotSupportedException
은 검사 예외이지만,Cloneable
구현 시super.clone()
은 항상 성공 하므로 비검사 예외로 설계되었어야 합니다.
불필요한 예외 처리:
Cloneable
구현 클래스에서super.clone()
호출 시 불필요한try-catch
블록이 필요합니다.@Override public BaseClass clone() { try { BaseClass cloned = (BaseClass) super.clone(); System.out.println("BaseClass clone() called - Constructor used!"); return cloned; } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } }
깊은 복사 구현 방법
재귀적 clone() 호출
가변 객체가 배열 또는 컬렉션인 경우, 내부 요소에 대해 재귀적으로
clone()
을 호출하여 깊은 복사를 구현
public class Stack { private Object[] elements; //... @Override public Stack clone() throws CloneNotSupportedException{ Stack result = (Stack) super.clone(); result.elements = elements.clone(); return result; } }
반복문:
복잡한 자료구조의 경우 반복문을 사용하여 깊은 복사를 구현
public class HashTable implements Cloneable { private Entry[] buckets = ...; private static class Entry { final Object key; Object value; Entry next; //... } //... private 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 return } @Override public HashTable clone() throws CloneNotSupportedException{ HashTable result = (HashTable) super.clone(); result.buckets = new Entry[buckets.length]; for (int i = 0; i < buckets.length; i++) if (buckets[i] != null) result.bucket[i] = buckets[i].deepCopy(); return result; } }
고수준 API 활용:
super.clone()
으로 얕은 복사 후, 복제본 객체의 필드를 초기화하고 원본 객체의 상태를 복제하는 고수준 메서드를 호출하여 깊은 복사를 구현HashTable의
put(key, value)
메서드 활용
저수준 처리보다 낮은 성능
전체 Cloneable 아키텍처와 어울리지 않는 방식
final 필드와 Cloneable의 충돌
final 필드의 제약:
final
필드는 생성자에서만 값 할당이 가능하므로,clone()
메서드에서 final 필드의 깊은 복사가 어려울 수 있습니다.
final 제약 완화:
복제 가능한 클래스를 만들기 위해 일부
final
제약자를 제거해야 할 수도 있습니다단, 원본과 복제본이 가변 객체를 공유해도 안전한 경우 제외
재정의 가능한 메서드 호출 주의
생성자와 clone()의 유사점:
생성자와 마찬가지로
clone()
메서드 내에서 재정의 가능한 메서드 호출을 피해야 합니다.
하위 클래스 상태 교정 문제:
clone()
이 하위 클래스에서 재정의한 메서드를 호출하면, 하위 클래스는 복제 과정에서 자신의 상태를 제대로 설정하지 못할 수 있습니다.
해결 방안:
복제 관련 메서드는
final
또는private
으로 선언하여 재정의를 막아야 합니다.
상속을 고려한 Cloneable 처리
상속용 클래스가 Cloneable 구현을 피하는 경우
class BaseClass { private String data; public BaseClass(String data) { this.data = data; } } class SubClass extends BaseClass implements Cloneable { private int count; public SubClass(String data, int count) { super(data); this.count = count; } @Override public SubClass clone() throws CloneNotSupportedException { return (SubClass) super.clone(); // Object의 clone() 호출 } } public class Main { public static void main(Stringargs) throws CloneNotSupportedException { SubClass original = new SubClass("test", 10); SubClass cloned = original.clone(); } }
하위 클래스가 필요에 따라 복제 기능을 추가
복제에 대한 어떤 가정도 하지 않기 때문에 더 안전한 설계
상속을 고려한 클래스는
Cloneable
구현을 피하는 것이 좋습니다.
Object 방식 모방
class BaseClass { //... @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } } class SubClass extends BaseClass implements Cloneable { //... @Override public SubClass clone() throws CloneNotSupportedException { return (SubClass) super.clone(); } }
상위 클래스에서
Object
처럼protected clone()
메서드를 제공하고CloneNotSupportedException
을 던지도록 선언하위 클래스에서 Cloneable을 구현하여 복제 기능을 선택적으로 추가 가능
clone() 퇴화
class BaseClass { //... @Override protected final Object clone() throws CloneNotSupportedException { throw new CloneNotSupportedException(); // or return null; // return this; //복제방지 } } class SubClass extends BaseClass implements Cloneable { //... }
clone()
을 동작하지 않도록 구현final
로 선언하여 하위 클래스에서 재정의를 방지
스레드 안전 클래스와 Cloneable
동기화 필요
Cloneable
을 구현한 스레드 안전 클래스는clone()
메서드 역시 동기화
Object.clone() 비동기화
Object.clone()
은 동기화를 고려하지 않았으므로,clone()
재정의 시 명시적인 동기화 처리가 필요
@Override public synchronized ThreadSafeStack clone() throws CloneNotSupportedException { ThreadSafeStack result = (ThreadSafeStack) super.clone(); result.elements = elements.clone(); return result; }
Clone 재정의 요약
필수 재정의:
Cloneable
구현 클래스는 반드시clone()
을 재정의.접근 제한자:
public
반환 타입: 클래스 자신
구현 내용:
super.clone()
호출 후 필요한 필드 수정 (깊은 복사)
깊은 복사
객체 내부의 모든 가변 객체를 복사하고, 복제본이 복사된 객체를 참조하도록 구현
기본 타입/불변 객체 필드:
수정 불필요 (단, 일련번호, 고유 ID 등은 수정 필요)
Cloneable 대안: 복사 생성자와 복사 팩토리
복사 생성자:
동일 클래스 인스턴스를 인수로 받는 생성자
public class Color { private final int red; private final int green; private final int blue; // 복사 생성자 public Color(Color original) { this.red = original.red; this.green = original.green; this.blue = original.blue; } }
명시적 초기화
모든 필드에 대한 초기화를 직접 제어
final 필드 호환
생성자 내에서 final 필드 초기화 가능 (Cloneable은 final 필드 복제 불가)
계층 구조 지원
상속 시 하위 클래스에서 super() 호출로 기반 클래스 복제 가능
복사 팩토리:
복사 생성자를 모방한 정적 팩토리 메서드
public class NetworkConfig { private final String ip; private final int timeout; // 복사 팩토리 public static NetworkConfig copyOf(NetworkConfig original) { return new NetworkConfig(original.ip, original.timeout); } }
Cloneable 대비 결정적 장점
특성Cloneable복사 생성자/팩토리객체 생성 제어
JVM 의존적
생성자 직접 제어
예외 처리
CheckedException 발생
예외 불필요
final 필드
복제 불가
완벽 지원
타입 안정성
형변환 필요
타입 보장
인터페이스 활용
구현 클래스에 종속
추상 타입으로 복제 가능
상속 구조
super.clone() 관리 어려움
명시적 super() 호출
결론
Cloneable 확장/구현 지양:
새로운 인터페이스 확장 시
Cloneable
사용 금지새로운 클래스
Cloneable
구현 지양
final 클래스 예외적 허용:
final
클래스의Cloneable
구현은 성능 최적화 관점에서 신중히 고려 (위험성 적음)
복사 생성자/팩토리 권장:
객체 복사 기능 구현 시 복사 생성자 또는 복사 팩토리 사용 권장
배열 clone() 예외 : 배열 복사는
clone()
메서드 방식이 가장 적합
Last updated