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