ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Item 13. clone 재정의는 주의해서 진행하라
    Book/Effective Java 3E 2022. 10. 19. 20:00
    반응형

      Cloneable 인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정합니다. Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던집니다. 인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위인데, Cloneable의 경우에는 상위 클래스에 정의된 protected 메서드의 동작 방식을 변경한 것으로 상당히 이례적으로 사용한 예입니다(따라 하지 말것!).

     

    Object 명세

    이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음 식은 참이다.

    x.clone() != x

    또한 다음 식도 참이다.

    x.clone().getClass() == x.getClass()

    하지만 이상의 요구를 반드시 만족해야 하는 것은 아니다.
    한편 다음 식도 일반적으로 참이지만, 역시 필수는 아니다.

    x.clone().equals(x)

    관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와 (Object를 제외한) 모든 상위 클래스가 이 관례를 따른다면 다음 식은 참이다.

    x.clone().getClass() == x.getClass()

    관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.

     

      강제성이 없다는 점만 빼면 생성자 연쇄(constructor chaining)와 살짝 비슷한 메커니즘입니다. 즉, clone 메서드가 super.clone이 아닌, 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 불평하지 않을 것입니다.

     

      clone 메서드는 사실상 생성자와 같은 효과를 냅니다. 즉, clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 합니다.

     

      Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 합니다. 이때 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경합니다. 이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정합니다. 일반적으로 이 말은 그 객체의 내부 '깊은 구조'에 숨어 있는 모든 가변 객체를 복사하고, 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 함을 뜻합니다. 이러한 내부 복사는 주로 clone을 재귀적으로 호출해 구현하지만, 이 방식이 항상 최선인 것은 아닙니다. 기본 타입 필드와 불변 객체 참조만 갖는 클래스라면 아무 필드도 수정할 필요가 없습니다. 단, 일련번호나 고유 ID는 비록 기본 타입이나 불변일지라도 수정해줘야 합니다.

    package effectivejava.chapter3.item13;
    
    import java.util.HashMap;
    import java.util.Map;
    
    // 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, "가입자 번호");
        }
        
        ... 생략 ...
    
        // 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드
        @Override public PhoneNumber clone() {
            try {
                return (PhoneNumber) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new AssertionError();  // 일어날 수 없는 일이다.
            }
        }
    
        public static void main(String[] args) {
            PhoneNumber pn = new PhoneNumber(707, 867, 5309);
            Map<PhoneNumber, String> m = new HashMap<>();
            m.put(pn, "제니");
            System.out.println(m.get(pn.clone()));
        }
    }
    package effectivejava.chapter3.item13;
    import java.util.Arrays;
    
    // 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;
        }
    
        public boolean isEmpty() {
            return size ==0;
        }
    
        // 코드 13-2 가변 상태를 참조하는 클래스용 clone 메서드
        @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이 동작하는 모습을 보려면 명령줄 인수를 몇 개 덧붙여서 호출해야 한다.
        public static void main(String[] args) {
            Stack stack = new Stack();
            for (String arg : args)
                stack.push(arg);
            Stack copy = stack.clone();
            while (!stack.isEmpty())
                System.out.print(stack.pop() + " ");
            System.out.println();
            while (!copy.isEmpty())
                System.out.print(copy.pop() + " ");
        }
    }

     

      Clonealbe을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 합니다. 그렇지 않은 상황에서는 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있습니다.

    // 복사 생성자
    public Yum(Yum yum) { ... };
    
    // 복사 팩터리(정적 팩터리)
    public static Yum newInstance(Yum yum) { ... };

      복사 생성자와 복사 팩터리는 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있습니다. 인터페이스 기반 복사 생성자와 복사 팩터리의 더 정확한 이름은 '변환 생성자(conversion constructor)''변환 팩터리(conversion factory)'입니다. 예컨대 HashSet 객체 s를 TreeSet 타입으로 복제할 수 있습니다. clone으로는 불가능한 이 기능을 변환 생성자로는 간단히 new TreeSet<>(s)로 처리할 수 있습니다.

     

    새로운 인터페이스를 만들 때 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다.

    final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만,

    성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다(Item 67).

    기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고'라는 것이다.

    단, 배열만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다(위 Stack 코드 참고).

     

    [참고 정보]

    이펙티브 자바 Effective Java 3/E 도서 [조슈아 블로크 ]

    이펙티브 자바 깃허브 저장소

    <이펙티브 자바, 3판> 번역 용어 해설

    반응형

    댓글

Designed by Tistory.