본문 바로가기
Java/Java

Object 클래스의 clone() 메서드

by oneny 2023. 7. 30.

clone() 메서드

clone() 메서드는 특정 클래스의 인스턴스를 복제하여 새로운 인스턴스를 생성하려고 할 때 사용한다. clone()을 사용하면 이전의 값은 보존되고, 작업에 실패해서 원래의 상태로 되돌리거나 변경하지 전의 값을 참고하는데 도움이 된다. clone은 Object 클래스에 정의되어 있는데 단순히 인스턴스의 값을 복사하기 때문에 참조 타입의 필드가 있는 클래스는 완전한 복제가 이루어지지 않는다. 

 

Cloneable 인터페이스의 clone 메서드 살펴보기

API 문서의 clone 메서드에 대한 내용을 살펴보면 다음과 같다. Object 클래스에 대한 clone 메서드는 특정 복제 작업을 수행한다. 먼저, 클래스가 Cloneable 인터페이스를 구현하지 않는 경우에는 CloneNotSupportedException 예외가 던져진다. 그리고 모든 배열에 대해서는 Cloneable 인터페이스가 구현되어 있어 clone 메서드를 호출하는 경우에는 해당 타입에 대한 배열이 반환된다.

 

이렇듯 clone() 메서드는 얕은 복사(shallow copy)를 수행한다. 즉, 객체가 가지고 있는 모든 필드들을 새로운 객체로 복사하는 것이 아니라, 필드들이 참조하는 객체들의 레퍼런스만을 복사한다. 따라서 필드가 참조하는 객체는 원본 객체와 복제 객체에서 공유될 수 있다. 이를 해결하기 위해서는 원본 객체나 복제 객체에서 참조 객체의 상태를 변경하면 다른 객체에도 영향을 미칠 수 있기 때문에 깊은 복사(deep copy)를 수행하도록 만들어야 한다.

 

clone() 메서드의 얕은 복사

public class Point implements Cloneable {
  int x;
  int y;

  public Point(int x, int y) {
    this.x = x;
    this.y = y;
  }

  @Override
  protected Point clone() {
    Object obj = null;
    try {
      obj = super.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }

    return (Point) obj;
  }

  @Override
  public String toString() {
    return "Point{" +
            "x=" + x +
            ", y=" + y +
            '}';
  }

  public static void main(String[] args) {
    Point p1 = new Point(1, 2);
    Point p2 = p1.clone();
    System.out.println(p1 == p2); // false
    System.out.println(p1.equals(p2)); // false
    p2.x = 3; p2.y = 4;

    System.out.println("p1 = " + p1); // p1 = Point{x=1, y=2}
    System.out.println("p2 = " + p2); // p2 = Point{x=3, y=4}

    int[] arr = {1, 2, 3, 4, 5};
    int[] arrClone = arr.clone();
    arrClone[0] = 9;

    System.out.println(Arrays.toString(arr)); // [1, 2, 3, 4, 5]
    System.out.println(Arrays.toString(arrClone)); // [9, 2, 3, 4, 5]
  }
}

clone()을 사용하려면 복사하려는 클래스의 Cloneable 인터페이스를 implements 구현하여 clone() 메서드를 재정의하면 된다. 그러면 Point p2 = p1.clone();을 실행했을 때 원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성한다. 이렇게 복제하는 이유는 원본 객체를 안전하게 보호하기 위해서 새로운 객체를 생성하는 것이다. clone 메서드의 API 문서의 내용와 마찬가지로 int 배열을 선언해 초기화했고, clone 메서드를 바로 호출할 수 있는 것을 확인할 수 있다.

 

Cloneable 인터페이스

이렇게 Cloneable 인터페이스를 구현해야하는 이유는 Cloneable 인터페이스는 메서드가 없는 빈 마커 인터페이스(marker interface)로서, 해당 클래스가 객체 복제를 지원한다는 것을 나타낸다. 즉, Object 클래스에 정의된 clone() 메서드를 호출하기 위한 인터페이스로서 작동하므로 구현하지 않는 경우에는 CloneNotSupportedException 예외가 발생한다.

 

참고: 마커 인터페이스(marker interface)
Java의 마커 인터페이스는 인터페이스 내부에 상수도, 메서드도 없는 인터페이스를 말한다. 아무 내용도 없어서 쓸모가 없어 보이지만 마커 인터페이스는 객체의 타입과 관련된 정보를 해준다. 따라서 컴파일러와 JVM은 이 마커 인터페이스를 통해 객체에 대한 추가적인 정보를 얻을 수 있어 만약 마커 인터페이스의 규칙을 지키지 않는 경우 컴파일 시점에 오류를 발견할 수 있다.

 

clone() 메서드의 주의사항

class Info {
  private String birthDay;
  private String age;

  public Info(String birthDay, String age) {
    this.birthDay = birthDay;
    this.age = age;
  }

  public String getBirthDay() {
    return birthDay;
  }

  public void setBirthDay(String birthDay) {
    this.birthDay = birthDay;
  }

  public String getAge() {
    return age;
  }

  public void setAge(String age) {
    this.age = age;
  }

  @Override
  public String toString() {
    return "Info{" +
            "birthDay='" + birthDay + '\'' +
            ", age='" + age + '\'' +
            '}';
  }
}

public class Member implements Cloneable {

  private String name;
  private Info info;

  public Member() {
  }

  public Member(String name, Info info) {
    this.name = name;
    this.info = info;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Info getInfo() {
    return info;
  }

  public void setInfo(Info info) {
    this.info = info;
  }

  public Member copy() {
    Member ref = null;

    try {
      ref = (Member) this.clone();
    } catch (CloneNotSupportedException e) {
      e.printStackTrace();
    }
    return ref;
  }

  public static void main(String[] args) {
    Member oneny = new Member("oneny", new Info("19960309", "28"));

    Member twony = oneny.copy();
    twony.setName("twony");
    twony.getInfo().setBirthDay("20230729");
    twony.getInfo().setAge("0");

    System.out.println(oneny.getName()); // oneny
    System.out.println(twony.getName()); // twony

    System.out.println(oneny.getInfo()); // Info{birthDay='20230729', age='0'}
    System.out.println(twony.getInfo()); // Info{birthDay='20230729', age='0'}
  }
}

마지막 main 메서드에서 출력된 결과를 확인해보면 oneny.getInfo()와 twony.getInfo()의 출력을 통해 oneny 인스턴스와 twony 인스턴스가 같은 Info 인스턴스를 공유하고 있는 것을 확인할 수 있다. 이는 앞서 말했듯이 객체가 가지고 있는 모든 필드들을 새로운 객체로 복사하는 것이 아니라, 필드들이 참조하는 객체들의 레퍼런스를 복사하기 때문에 다음과 같은 문제가 발생한 것이다. 이를 위해서 다음과 같이 깊을 복사(deep copy)를 하도록 만들어줘야 한다.

 

public Member copy() {
  Member ref = null;

  try {
    ref = (Member) this.clone();
    ref.setInfo(new Info(this.info.getBirthDay(), this.info.getAge()));
  } catch (CloneNotSupportedException e) {
    e.printStackTrace();
  }
  return ref;
}

copy 메서드를 호출해서 새로운 Member 인스턴스 객체를 생성할 때 참조 타입의 필드도 새로운 객체를 생성하도록 만들어 줌으로써 oneny 인스턴스와 twony 인스턴스가 가진 필드가 공유되지 않도록 만들어 해결해줄 수 있다.

 

clone 메서드를 지양하는 이유

clone() 메서드를 사용하면 악의적인 구현으로 인해 하위 클래스의 인스턴스가 반환될 수 있다.

class Parent implements Cloneable {

  private String name;

  public Parent(String name) {
    this.name = name;
  }

  /**
   * clone() 메서드를 악의적으로 재정의하여 하위 크래스
   */
  @Override
  protected Object clone() {
    return new Child("Child Object");
  }
}

class Child extends Parent {

  public Child(String name) {
    super(name);
  }

  public void doSomething() {
    System.out.println("Child class is doing something!");
  }
}

public class Main {

  public static void main(String[] args) {
    Parent parent = new Parent("Parent Object");

    // Parent 클래스의 clone 메서드 호출
    Parent cloneParent = (Parent) parent.clone();

    System.out.println("원본 객체 클래스: " + parent.getClass().getName());
    System.out.println("복제 객체 클래스: " + cloneParent.getClass().getName());

    ((Child) cloneParent).doSomething();
  }
}

위 코드 예시처럼 원본 객체와 복제 객체의 클래스 이름을 출력 뒤, 복제 객체를 Child 타입으로 캐스팅하여 doSomething() 메서드를 호출했는데 Child 클래스의 인스턴스로 변환된 것을 확인할 수 있다. 이러한 상황에서 복제된 객체가 원본 객체와 예기치 않은 동작을 하게 될 수 있으며, 악의적인 구현으로 인해 보안과 안정성 문제가 발생할 수 있다. 따라서 clone 메서드 대신 다른 복제 방법을 사용하거나 방어적 복사(defensive copy)를 고려하는 것이 더 안전한 방법이다.

 

방어적 복사

public Parent(Parent parent) {
  this.name = parent.name;
}

 

Parent clone = new Parent(parent);

방어적 복사에는 여러 방법이 있는데 그 중 복사 생성자(Copy Constructor)가 있다. 위 코드처럼 객체를 복제하는 데에 사용할 수 있는 생성자를 만들어서 새로운 객체를 초기화할 수 있다.

 

 

 

출처

자바 Clone에 대해서

Java 깊은 복사(Deep Copy)와 얕은 복사(Shallow Copy)

[Java] 마커인터페이스