콘텐츠로 이동

2021 03 10

2021-03-10

깊은 복사 vs 얕은 복사

  • 고민의 시작 - new ArrayList<>(); 의 인자로 List를 넘겨주면, 새로운 ArrayList가 생성된단 말이지? - 두 개의 주소값을 출력해보면 확실히 다르단 말이지? - 따라서 그 두 List에서 각각 add()/remove() 등의 연산을 수행해주면 각자 처리가 된단 말이지? - 하지만 두 List 안에 있는 객체의 주소는 동일하네? - 어라? List 내의 하나의 객체를 조작하면 다른 List의 객체도 조작이 되네? - "뇌절" - 뇌절의 이유는 Class 안에 Class를 저장해놨기 때문! - 첫 클래스(List)는 "깊은 복사"로써 새로운 참조값을 할당 받았지만, - 그 안에 Class들은 "얕은 복사"를 통해 복사되며, 이는 참조값을 공유하게 된다.
  • 얕은 복사 - 객체 복사시, 주소값을 복사한다 - "=" 연산자는 얕은 복사를 수행한다. 즉 주소를 이어준다
    transient Object[] elementData; // non-private to simplify nested class access
    
    public ArrayList(Collection<? extends E> c) {
        Object[] a = c.toArray();
        if ((size = a.length) != 0) {
            if (c.getClass() == ArrayList.class) {
                elementData = a;
            } else {
                elementData = Arrays.copyOf(a, size, Object[].class);
            }
        } else {
            // replace with empty array.
            elementData = EMPTY_ELEMENTDATA;
        }
    }
    

    - 결국 ArrayList는 배열에 요소를 저장하는 것 - 넘겨 받는 인자가 ArrayList라면, "elementData = a" 라며 "얕은 복사!"를 하는 것을 볼 수 있다 - 아하 내가 여기서 뇌절을 했구만

  • 깊은 복사 - 주소값 참조가 아닌, 새로운 메모리 공간에 값을 복사하는 것 - 해당 객체와 인스턴스 변수까지 복사하는 방식 - 참조를 공유하지 않음 - Java에서는 객체 자체를 복사할 때에는 Cloneable 인터페이스를 기반으로 구현할 것을 권고한다 - clone()을 구현할 때에는 어떤 인스턴스 필드를 복사해줄지 정의해줘야 깊은 복사가 가능하다 - primitive 타입 같은 경우는 상관이 없겠지만, 클래스라면 복사 하나하나 지정 필수 - 기본 자료형, Enum, Immutable과 같이 clone을 지원하는 객체가 아니라면 clone 별도 지정 필수
  • 배열을 복사하는 방법 - Arrays.copyOf() - Arrays.copyOfRange() - System.arraycopy() - Object.clone() - 등이 배열의 깊은 복사를 (즉 새로운 배열을 생성하고, 안의 내용물을 "="을 통해 얕은 복사) 지원
  • Object.clone() - 객체 구조를 복사하지만, 객체 레퍼런스의 경우 레퍼런스만 복사 - 즉, 실제 객체는 복사하지 않음 (shallow cloning) - 이런 클로닝은 shallow cloning으로 필드 멤버가 전부 primitive 타입인 경우에 유용 - clone() 기본 동작 과정
    - Cloneable 인터페이스가 구현되어 있다면 원본과 같은 객체를 생성, 모든 필드 멤버들을 원본의 필드와 동일하게 초기화 - 내용을 새로 생성하지 않고, 단지 대입에 의해 복사됨 (shallow cloning) - 기본적으로 배열은 Cloneable 인터페이스를 구현했음 - 따라서, 새로운 배열을 만들면 주소값은 달라지지만, - 보다시피 안에 원소들은 "="를 통해 얕은 복사로 할당되어, - 참조를 공유하게 된다 - 그래서 우리가 정의한 객체에서 복사를 위해 clone()을 오버라이딩해서 써야하는 것은 알겠어. - 왜냐하면, 실제 객체에 깊은 복사를 하기 위해서는 clone()이라는 메서드를 재정의해야하기 때문이야 - 여기서 주의해주어야 할 점이 무엇이냐면! - 클래스 안에 다른 배열/클래스가 인스턴스 변수로 있는 경우인데, - 기본적으로 super.clone()을 통해 Object.clone()을 호출하게 되면, - primitive 값 같은 경우에는 어차피 값의 복사이고, 메모리에 해당 자료형을 나타내는 1010이 저장되기에 상관이 없지만, - 객체 레퍼런스가 인스턴스 변수로 저장되어 있는 경우, 해당 레퍼런스만 복사해서 새로운 객체에 할당해줘 - 이는 곧, shallow cloning이 일어났다는 뜻이며, - 복사하기 전 객체에서 해당 인스턴스 변수를 조작하면, - 복사해놨다고 생각했던 친구도 똑같이 달라질 거야 - 따라서 다음과 같이 해결하도록 해요
    //참고: https://javacan.tistory.com/entry/31
    public class MyClass implements Cloneable {
       private MyData data = null;
    
       public MyClass() {
          data = new MyData();
       }
    
       public Object clone() throws CloneNotSupportedException {
          MyClass myObj = super.clone();
          myObj.data = (MyData) data.clone(); // 직접 객체에 접근하여 복사를 진행해줌
          return myObj;
       }
    }      
    
      - 따라서... 어떤 클래스를 완전히 deep cloning하기 위해서는 그 클래스로 부터 참조되는 모든 클래스를 추적/복사 하여야 함