pungjoo

Object.clone() 본문

JAVA

Object.clone()

pungjoo.kim 2008. 12. 11. 10:09
들어가면서
Clone이란 무엇일까요?
단일세포 또는 개체로부터 무성적인 증식에 의하여 생긴 유전적으로 동일한 세포군 또는 개체군을 말한다.
중요합니다. 유전적.. 뜻은 명확히 모르겠지만 어떤 형태적인 느낌이 듭니다..

Java에서 clone(복제)이 의미하는 것은 무엇일까요?

Copy Vs Clone(Shadow Copy)
이런 저런 정보(객체/field)를 소유하고 있는 객체를 어느 시점에 세포 분열(clone)을 통해서 2개의 객체로 만들어 서로 다른 길을 걷게 할때 clone을 일반적으로 사용하려고들 합니다. 이때 조건은 2개로 분열된 객체가 소유하고 있는 정보(객체)에 대한 변경을 가했을때 다른 한 객체에게 영향을 주지 않아야 합니다. 만약 영향을 받을 것이라면 애초에 복제 할 필요가 없겠지요. (그러나 그런 상황이 필요할 때도 있긴 합니다. 어찌 보면 더욱 의미 있을 수 있습니다.)

복제란?..
만약 100Kg이 나가는 소를 복제한다고 가정하면 제가 이런 쪽은 전혀 모르겠지만, 복제하려하는 소의 유전적 형질을 이용해 새끼 소를 복제하고 이렇게 복제한 것을 유전적 형질이 동일한 복제 소라고 할 겁니다. 이때(복제가 되자 마자) 복제하기 위해 유전 형질을 제공한 소와 제공을 받은 소는 무게가 100Kg이 아니겠지요.  즉, 복제라 함은 유전적 형질이 동일하다는 것을 의미하지 외형적으로 동일하다는 의미는 아니라는 것 입니다.

복사란?
A4 종이에 쓰여 있는 내용을 복사한다고 가정하면(복사기는 없다고 가정하고)  복사하려고 하는 것과 최대한 비슷한 A4 종이, 펜, 필체 등등을 통해 쓰여진 내용을 모사하겠지요. 이처럼 복사는 원본에서 어떤한 것을 추출해서 이용하는 것은 아닌 개념입니다.

위에서 범위를 축소해 설명한 복제와 복사의 의미처럼 copy와 clone은 사실상 다른 사상입니다.

Object.clone()
java에서 모든 객체의 조상(?)은 Object라고 합니다. 즉, Object를 상속 받는 다는 것 입니다. 그중에 우리가 관심을 갖는 부분은 clone()이라는 method입니다. sun의 api를 보면 Object.clone() method는
 protected native Object clone() throws CloneNotSupportedException;

위와 같이 되어 있습니다. native이군요. 그럼 이 native 부분을 한번 보겠습니다. java가 아니라서 머리가 아프겠지만 그냥 그런가 보다 하고 보면..
Object.c

#include <stdio.h>
#include <signal.h>
#include <limits.h>

#include "jni.h"
#include "jni_util.h"
#include "jvm.h"

#include "java_lang_Object.h"

static JNINativeMethod methods[] = {
    {"hashCode",    "()I",                    (void *)&JVM_IHashCode},
    {"wait",        "(J)V",                   (void *)&JVM_MonitorWait},
    {"notify",      "()V",                    (void *)&JVM_MonitorNotify},
    {"notifyAll",   "()V",                    (void *)&JVM_MonitorNotifyAll},
    {"clone",       "()Ljava/lang/Object;",   (void *)&JVM_Clone},
};

JNIEXPORT void JNICALL
Java_java_lang_Object_registerNatives(JNIEnv *env, jclass cls)
{
    (*env)->RegisterNatives(env, cls,
       methods, sizeof(methods)/sizeof(methods[0]));
}

JNIEXPORT jclass JNICALL
Java_java_lang_Object_getClass(JNIEnv *env, jobject this)
{
    if (this == NULL) {
 JNU_ThrowNullPointerException(env, NULL);
 return 0;
    } else {
 return (*env)->GetObjectClass(env, this);
    }
}



실제 구현되는 부분을 보면..
jvm.cpp

JVM_ENTRY(jobject, JVM_Clone(JNIEnv* env, jobject handle))
  JVMWrapper("JVM_Clone");
  Handle obj(THREAD, JNIHandles::resolve_non_null(handle));
  const KlassHandle klass (THREAD, obj->klass());
  JvmtiVMObjectAllocEventCollector oam;

#ifdef ASSERT
  // Just checking that the cloneable flag is set correct
  if (obj->is_array()) {
    guarantee(klass->is_cloneable(), "all arrays are cloneable");
  } else {
    guarantee(obj->is_instance(), "should be instanceOop");
    bool cloneable = klass->is_subtype_of(SystemDictionary::cloneable_klass());
    guarantee(cloneable == klass->is_cloneable(), "incorrect cloneable flag");
  }
#endif

  // Check if class of obj supports the Cloneable interface.
  // All arrays are considered to be cloneable (See JLS 20.1.5)
  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }

  // Make shallow object copy
  const int size = obj->size();
  oop new_obj = NULL;
  if (obj->is_array()) {
    const int length = ((arrayOop)obj())->length();
    new_obj = CollectedHeap::array_allocate(klass, size, length, CHECK_NULL);
  } else {
    new_obj = CollectedHeap::obj_allocate(klass, size, CHECK_NULL);
  }
  // 4839641 (4840070): We must do an oop-atomic copy, because if another thread
  // is modifying a reference field in the clonee, a non-oop-atomic copy might
  // be suspended in the middle of copying the pointer and end up with parts
  // of two different pointers in the field.  Subsequent dereferences will crash.
  // 4846409: an oop-copy of objects with long or double fields or arrays of same
  // won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead
  // of oops.  We know objects are aligned on a minimum of an jlong boundary.
  assert(MinObjAlignmentInBytes >= BytesPerLong, "objects misaligned");
  Copy::conjoint_jlongs_atomic((jlong*)obj(), (jlong*)new_obj,
                               (size_t)align_object_size(size) / HeapWordsPerLong);
  // Clear the header
  new_obj->init_mark();

  // Store check (mark entire object and let gc sort it out)
  BarrierSet* bs = Universe::heap()->barrier_set();
  assert(bs->has_write_region_opt(), "Barrier set does not have write_region");
  bs->write_region(MemRegion((HeapWord*)new_obj, size));

  // Caution: this involves a java upcall, so the clone should be
  // "gc-robust" by this stage.
  if (klass->has_finalizer()) {
    assert(obj->is_instance(), "should be instanceOop");
    new_obj = instanceKlass::register_finalizer(instanceOop(new_obj), CHECK_NULL);
  }

  return JNIHandles::make_local(env, oop(new_obj));
JVM_END 

처럼되어 있습니다. 그냥 복잡합니다.. ^^;

그럼 어떻게 사용하나?
모든 객체는 Object를 묵시적으로 상속 받기 때문에 다음과 같이 합니다.
package com.pungjoo.edu.clone;

public class SomeThingClass {

 public static void main(String[] args) {
  SomeThingClass clazz = new SomeThingClass();
  SomeThingClass cloneClazz = null;
  try {
    cloneClazz = (SomeThingClass) clazz.clone();
  } catch (CloneNotSupportedException e) {
   e.printStackTrace();
  }
 }
 
}

그러나 실행해 보면 아래와 같이 CloneNotSupportedException이 발생합니다.
java.lang.CloneNotSupportedException: com.pungjoo.edu.clone.SomeThingClass
 at java.lang.Object.clone(Native Method)
 at com.pungjoo.edu.clone.SomeThingClass.main(SomeThingClass.java:9)

왜 그럴까요? 이는 jvm.cpp의 붉은색 block에서 현재 class가 'Cloneable interface'를 implement 했는지 확인하기 때문에 그렇습니다. 
  // Check if class of obj supports the Cloneable interface.
  // All arrays are considered to be cloneable (See JLS 20.1.5)
  if (!klass->is_cloneable()) {
    ResourceMark rm(THREAD);
    THROW_MSG_0(vmSymbols::java_lang_CloneNotSupportedException(), klass->external_name());
  }


따라서 올바르게 사용하려면 다음과 같이해야 합니다.

package com.pungjoo.edu.clone;

public class SomeThingClass implements Cloneable {

 public static void main(String[] args) {
  SomeThingClass clazz = new SomeThingClass();
  SomeThingClass cloneClazz = null;
  try {
    cloneClazz = (SomeThingClass) clazz.clone();
  } catch (CloneNotSupportedException e) {
   e.printStackTrace();
  }
  
 }
 
}


생각 보다는 간단하게 사용할 수 있습니다. 그러나 이후부터 혼란의 세계에 빠져들기 시작합니다.

혼란스러워하기...

package com.pungjoo.edu.clone;

import java.util.Set;
import java.util.TreeSet;

public class Animal implements Cloneable {

        private Set<String> animals;

        public Animal() {
                animals = new TreeSet<String>();
        }

        public void init() {
                animals.add("lion");
                animals.add("tortoise");
                animals.add("bee");
        }

        public void print() {

                System.out.println(animals);
                System.out.println();

        }

        public static void main(String[] args) {
                Animal clazz = new Animal();
                clazz.init(); //--> A
               
                Animal cloneClazz = null;
                try {
                        cloneClazz = (SomeThingClass) clazz.clone();
                        // clazz.init(); //--> B
                       
                        printAll(clazz, cloneClazz);
                } catch (CloneNotSupportedException e) {
                        e.printStackTrace();
                }

        }

        private static void printAll(Animal clazz, Animal cloneClazz) {
                System.out.println("원본 ");
                clazz.print();

                System.out.println("복제 ");
                cloneClazz.print();

        }
}

위  결과는 아래와 같습니다.
원본
[거북이, 벌, 사자]

복제
[거북이, 벌, 사자]
그럼 파란색으로 되어 있는 'A'라인을 주석처리하고 주석처리되어 있는 'B'라인의 주석을 제거하고 실행하면 어떻게 될까요?
결과는 동일하게 나옵니다. 왜 그럴까요? 이제부터 혼란스럽기 시작합니다.

논리적으로 cloneClazz = (Animal) clazz.clone();를 하는 시점에 clazz.init()가 호출되지 않았기 때문에 cloneClazz는 null또는 []으로 나와야겠지요. 그러나 clone()이라는 것은 복제하려고 하는 원본에 설정된 '레퍼런스'를 복제하는 개념이기 때문에 animals 객체에 무엇이 들어 있는지는 중요하지 않습니다.  중요한 것은 animals 객체가 바라보고 있는 레퍼런스가 중요하고 결론적으로 원본의 animals 객체가 바라 보는 레퍼런스를 복제된 animals 객체도 바라 보게 됩니다. 따라서 복제된 후에 원본의 clazz.init()를 통해서 add되었다 하더라도 cloneclazz의 animals 객체는 원본의 animals 객체와 동일한 레퍼런스를 바라 보기 때문에 동일한 결과가 나오게 됩니다.  ( 단, Animal 생성자에서 animals를 설정하지 않으면 생각했던 결과가 나옵니다. )


위와 동일한 효과가 발생하는 다른 예를 한번 보겠습니다.
package com.pungjoo.edu.clone;

import java.util.Set;
import java.util.TreeSet;

public class Animal implements Cloneable {

       private Set<String> animals;

       public Animal() {
              animals = new TreeSet<String>();
       }

       public void init() {
              animals.add("사자");
              animals.add("거북이");
              animals.add("벌");
       }

       public void remove() {
              animals.remove("거북이");
       }
      
       public void print() {

              System.out.println(animals);
              System.out.println();

       }

       public static void main(String[] args) {
              Animal clazz = new Animal();
              clazz.init();
             
              Animal cloneClazz = null;
              try {
                     cloneClazz = (Animal) clazz.clone();
                     printAll(clazz, cloneClazz);
                    
                     System.out.println( "원본에서 거북이를 삭제함.");
                     clazz.remove();
                     printAll(clazz, cloneClazz);
              } catch (CloneNotSupportedException e) {
                     e.printStackTrace();
              }

       }

       private static void printAll(Animal clazz, Animal cloneClazz) {
              System.out.println("원본 ");
              clazz.print();

              System.out.println("복제 ");
              cloneClazz.print();

       }
}

결과는 다음과 같습니다.
원본
[거북이, 벌, 사자]
복제

[거북이, 벌, 사자]

원본에서 거북이를 삭제함.
원본
[벌, 사자]

복제
[벌, 사자]

원본(clazz)에서 삭제했으면 복제(cloneclazz) 객체도 적용되게 됩니다.

본래 의도하는 복제(복사) 객체 만들기
이런 저런 책을 보면 위와 같이 '레퍼런스'를 동일하게 갖게 되기때문에 clone() method를 override하라고 되어 있습니다.
@Override
public       Object       clone() throws CloneNotSupportedException{
       Animal retObj = (Animal) super.clone();
       return retObj;
}
그러나 위와 같이 override를 한다고 해서 본질이 해결되지는 않습니다. 이유는 override를 하지 않든 위와 같이 하든 결론은 본질적인 부분에 대한 추가가 없기 때문에 그렇습니다.  그럼 어떻게 해야 하나?.. 결론은 Set<String> animals 객체를 복제(복사)를 해 줘야 합니다.
@Override
public       Object       clone() throws CloneNotSupportedException{
       Animal retObj = (Animal) super.clone();
       retObj.animals = (Set<String>) ((TreeSet<String>) this.animals).clone();
       return retObj;
}

위와 같이 override하고 animals 객체에 add하는 method를 추가하고 cloneclazz에서 호출해 봅시다.
package com.pungjoo.edu.clone;

import java.util.Set;
import java.util.TreeSet;

public class Animal implements Cloneable {

       private Set<String> animals;

       public Animal() {
              animals = new TreeSet<String>();
       }

       @Override
       public       Object       clone() throws CloneNotSupportedException{
              Animal retObj = (Animal) super.clone();
              retObj.animals = (Set<String>) ((TreeSet<String>) this.animals).clone();
              return retObj;
       }
      
       public void init() {
              animals.add("사자");
              animals.add("거북이");
              animals.add("벌");
       }

       public       void       add() {
              animals.add("낙타");
       }
       public void remove() {
              animals.remove("거북이");
       }
      
       public void print() {

              System.out.println(animals);
              System.out.println();

       }

       public static void main(String[] args) {
              Animal clazz = new Animal();
              clazz.init();
             
              Animal cloneClazz = null;
              try {
                     cloneClazz = (Animal) clazz.clone();
                     printAll(clazz, cloneClazz);
                    
                     System.out.println( "원본에서 거북이를 삭제함.");
                     clazz.remove();
                    
                     System.out.println( "복제에 낙타를 추가함");
                     cloneClazz.add();

                     printAll(clazz, cloneClazz);
              } catch (CloneNotSupportedException e) {
                     e.printStackTrace();
              }

       }

       private static void printAll(Animal clazz, Animal cloneClazz) {
              System.out.println("원본 ");
              clazz.print();

              System.out.println("복제 ");
              cloneClazz.print();

       }
}



결과를 한번 볼까요?
원본
[거북이, 벌, 사자]

복제
[거북이, 벌, 사자]

원본에서 거북이를 삭제함.
복제에 낙타를 추가함
원본
[벌, 사자]


복제
[거북이, 낙타, 벌, 사자]


이제 의도하는 형태로 복제(복사)가 되었습니다.

참고로 TreeSet.clone() method를 보면
public Object clone() {
      TreeSet<E> clone = null;
      try {
            clone = (TreeSet<E>) super.clone();
      } catch (CloneNotSupportedException e) {
            throw new InternalError();
      }

      clone.m = new TreeMap<E,Object>(m);

      return clone;
}


남은 의문점? clone() method를 그럼 new Object(); 하면 안되나?

복제가 복제아닌 복사처럼 되어 버린다면 즉, clone method를 호출한 것으로 끝나지 않고 사용되는 data field를 각각 복제를 해야 한다면 다음과 같은 형태로 하면 안 될까요?
@Override
public       Object       clone() throws CloneNotSupportedException{
       Animal retObj = new Animal();
       retObj.animals = new TreeSet<String>( this.animals);
       return retObj;
}

위와 같이 clone() method를 수정해도 결론은 아래와 같습니다.
원본
[거북이, 벌, 사자]

복제
[거북이, 벌, 사자]

원본에서 거북이를 삭제함.
복제에 낙타를 추가함
원본
[벌, 사자]

복제
[거북이, 낙타, 벌, 사자]
차이가 없네요?

어짜피 data field를 다시 채우기 위해서 기술한다면 그냥 new Object()하고 field를 채우면 되는 것을 복잡하고 혼란스럽게 super.clone()을 할까요?

sample 소스의 생성자( public Animal(){} ) 부분에 System.out.println(...)을 추가하고 비교해 봅시다....
public Animal() {
       animals = new TreeSet<String>();
       System.out.println( getClass().getName() + " 을 호출 합니다.");
}
위와 같이 생성자를 변경하고 super.clone() 형태로 실행하면..
com.pungjoo.edu.clone.Animal 을 호출 합니다.
원본
[거북이, 벌, 사자]

복제
[거북이, 벌, 사자]

원본에서 거북이를 삭제함.
복제에 낙타를 추가함
원본
[벌, 사자]

복제
[거북이, 낙타, 벌, 사자]

이번에는 new Object(); 형태로 실행하면.
com.pungjoo.edu.clone.Animal 을 호출 합니다.
com.pungjoo.edu.clone.Animal 을 호출 합니다.

원본
[거북이, 벌, 사자]

복제
[거북이, 벌, 사자]

원본에서 거북이를 삭제함.
복제에 낙타를 추가함
원본
[벌, 사자]

복제
[거북이, 낙타, 벌, 사자]
다르지요? super.clone() 형태일때는 생성자가 1회 ( Animal clazz = new Animal(); ) 호출되고 new Object() 형태일때는 생성자가 2회 ( Animal clazz = new Animal(); 에서 한번 Animal retObj = new Animal(); 에서 한번) 호출 됩니다.

정리하면
supe.clone()을 호출하면 생성자 호출 없이 원본의 객체를 native Level에서 복제합니다. 역시나 복제 개념상 data field가 바라 보는 레퍼런스도 동일한 곳을 바라 보도록 복제가 됩니다. 이때까지는 복제입니다. 이제 실제 우리가 의미를 두는 즉, data field는 복제 개념이 아닌 복사 개념으로 명시적으로 복제를 해 줘야 합니다. 이때 중요한 부분은 data field도 new Object() 형태로 설정하면 안됩니다. 즉, data field 객체도 clone()을 동일한 형태로 구현하고 있어야 한다는 것 입니다. 가급적말이죠. (조상에 조상까지 가면 어쩔수 없이 new Object를 하게 됩니다만..)

만약에 후자( new Object() ) 형태로 했을 때 예시에는 단순히 문자열 출력을 했으나 database를 오고 가거나 타 시스템과 연동, 또는 타 Object를 호출하거나 하는 형태가 들어 있다면 중복적인 행위가 유발되게 됩니다. 따라서 이런 부분을 피하면서 객체를 복제 하는 것이 Object.clone()의 역할입니다.

덧말
간혹 Primitive Data Types 또는 String 객체로 하고 '어라 난 잘되는데'라고 생각하시겠지만, String 객체는 String type에 대한 본래 특성상 혼란을 가중시키는 요소가 있습니다. 해당 부분을 언급하고자 했으나 글의 내용상 Object.clone()을 다루는 글이기 때문에 제외했습니다.

@
3 Comments
댓글쓰기 폼
Prev 1 2 3 4 5 6 7 8 9 Next