티스토리 뷰

이번 포스트에서는 객체의 클론과 깊은 복사, 얕은 복사에 대해 살펴본다. 클론은 같은 객체를 값을 복사하여 그대로 한 개 더 만드는 것이다. ArrayList 같은 객체는 두 변수가 같은 객체를 참조하는 경우(매개변수로 전달한 경우) 한 변수를 통해 객체 내부의 값을 바꾸면 다른 변수의 객체(같은 것이므로)도 바뀌게 된다. 이런 경우 우리는 바꾸기 이전 값을 저장하기 위해 클론을 통해 같은 객체를 한 개 더 만들어두는 경우가 많이 있다.

먼저 클론(clone)이 무엇인지 알아보자. 클론은 필드의 값이 같은 새로운 객체를 만드는 것이다. 그러면 객체가 하나 더 생기고 모든 필드가 원래 객체와 같은 값을 가지게 될 것이다. 이 때 값변수라면 값을 저장할 새로운 메모리가 객체 안에 생기고 새로운 객체에는 새로운 칸이 생기므로 값이 복사되어야 한다. 참조 변수의 경우에도 새로운 참조 필드가 생기는데, 이 참조는 원래 객체의 참조 값을 복사한다. 이것을 얕은 복사(shallow copy)라고 한다. 즉 클론해서 생긴 객체의 필드는 원래 객체가 가리키던 필드의 객체를 같이 가리킨다.

클론의 얕은 복사가 필드의 값을 어떻게 복사하는지 알기 위해서는 메모리의 할당에 대해 알아야 한다.

1) 우선 int나 float 같은 단순값은 객체의 필드가 참조가 아니고 값을 직접 가지고 있다. 그러므로 객체 안에 새로 생긴 필드에 원래 객체의 값이 그대로 복사된다.

2) ArrayList 같은 참조 타입의 필드라면 새로 생긴 객체에는 참조를 가질 공간만 생기고 원래 객체의 참조 값을 복사한다.  

이 때 참조를 두 가지 경우로 나누어서 생각해 보자. 참조가 스트링 객체라면 앞의 포스트에서 살펴본 바와 같이 어차피 같은 값이면 같은 객체를 참조하면 된다. 나중에 cart2에서 userName 값을 바꾸면 cart2.userName은 다른 스트링을 가리키게 될 것이다. 불변값은 원래 객체의 값을 고칠 수 없으므로 새로운 객체를 만들어 바뀐 이름을 저장하게 된다. 이전 객체의 cart1.username 필드는 원래 값 "user1"을 가질 것이다.

그러나 참조가 배열이나 ArrayList인 경우는 어떨까? 우리가 이전 주문에서 클론해서 새로운 주문을 만들었다고 가정해 보자. 원래 주문과는 독립적으로 물건을 추가하거나 삭제할 수 있어야 한다. 그러나 필드를 그냥 복사해서 새로운 객체를 만들었다면? 위 그림처럼 이전 주문과 새로운 주문은 같은 ArrayList를 가리킬 것이다.

그러면 cart2에서 새로운 주문이 물건을 추가하거나 삭제할 때 cart1 주문의 리스트도 바뀌게 된다.  즉 위의 그림처럼 얕은 복사를 이용하여 cart1 을 클론하여 cart2 를 만들었다면, cart2 주문이 여기에 과일을 추가한다면? cart1 주문도 과일이 추가된다. 이것은 두 주문의 cartItems가 같은 ArrayList를 가리키고 있기 때문이다. 

이러한 문제를 해결하기 위해 깊은 복사는 변경가능한 참조 객체에 대해 가리키는 객체를 또 클론을 수행하여 같은 값을 가진 객체를 새로 만든다. 불변 객체의 참조는 그대로 복사하면 된다.    

새로운 주문을 클론해서 만들 때 오른쪽 그림처럼 새로운 ArrayList를 새로 생성하고 거기에 cart1 주문의 ArrayList에 있는 내용을 하나씩 다 복사해야 한다. [물론 하나씩 복사하지 않아도 되고 ArrayList.addAll 이라는 메소드를 이용하여 한꺼번에 넣을 수 있다.) 이것을 깊은 복사(deep copy)라고 한다. 즉 참조하는 객체를 재귀적으로 복제하는 것이다. 여기서 재귀적이라는 의미는 이렇게 새로 생성해서 복사하는 객체 안에 또 참조가 있다면? 그것도 또 새로 생성해서 복사해야 한다. 그러므로 깊은 복사란 트리와 같은 구조를 만들어내는 재귀적인 복사라고 할 수 있다.

여기까지는 개념이고 그럼 이제 자바에서의 clone 실제에 들어가 보자. 자바의 클론 구현은 인터페이스와 예외를 사용해야 하므로 어려운 개념에 속한다. 그러나 Cloneable에서 정의된 clone을 이용하는 것으로 compareTo와 유사하다.

클래스 X에 clone을 추가하는 방법은 다음과 같다. 일단 Cloneable을 implement하고 다음과 같은 시그너처로 clone을 구현해야 한다. https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html#clone 참조

public class X implements Cloneable {
        public X clone() throws CloneNotSupportedException {
                return (X) super.clone();
        }
}

이 네 줄의 코드를 이해하는 것이 쉬운 일은 아니다.

  • 먼저 Cloneable을 implements해서 Object가 가지고 있지만 protected로 정의되어 있고 호출이 금지되어 있는 clone을 public으로 바꾸어 재정의하고 호출할 수 있게 해야 한다. (이 부분이 없으면 예외가 발생한다.)
  • 모든 clone 메소드는 super.clone()을 불러 Object가 생성해주는 복제 객체의 참조를 받아야 한다.
  • CloneNotSupportedException 예외는 clone을 호출한 클래스가 Cloneable을 구현하지 않았거나 clone이 금지된 클래스인 경우(예를 들면 String)에 발생한다. 해당 클래스가 clone을 갖지 않거나 그 메소드 내부에서 사용한 clone이 해당 클래스에 구현되어 있지 않은 경우 이 예외가 발생하게 된다. 이 예외는 checked 예외여서 반드시 함수 선언부에 throws 절을 붙이거나 try { ... } catch 로 잡아 주어야 한다.

여기까지는 clone을 호출할 수 있게 해주는 일만 한다. 그러면 Object.clone()에 의해 같은 객체를 복제(모든 필드 값을 그대로 복사)하여 그 참조를 돌려주는 얕은 복사가 가능해 진다. 다음은 이러한 clone 기능을 사용하는 부분이다. Cloneable은 상속한 클래스에도 적용되므로 아래에서는 X를 상속한 Y나 Z 객체에도 clone 호출이 가능함을 보여준다.

public class Y extends X { }

public class Z extends Y { }

public class test1 {
        public void function() throws CloneNotSupportedException {
                Y varY1 = new Z();
                Y varY2 = (Y) varY1.clone();
        }
}

clone의 진짜 목적은 깊은 복사를 하는 데 있다. 깊은 복사를 위해서는 필요한 필드를 복제하여 새로운 객체로 만들어야 한다.  위의 ShoppingCart 예제를 생각해 보자.

cart2 = cart1.clone();
cart2.add(item);

위와 같이 새로 만든 카드에 아이템을 추가해도 원래 카트에는 변동이 없게 하기 위해 이 클래스에 deep copy를 구현한 clone 메소드를 오버라이드해야 한다. 그러면 기존의 카트에 있던 아이템을 모두 가지되 별도의 ArrayList를 가지게 되어 cart1과 상관없는 독립된 카트가 만들어진다.

class ShoppingCart implements Cloneable {
    String userName;
    ArrayList cartItems = new ArrayList<>();
    @Override
    public Object clone() throws CloneNotSupportedException {
        ShoppingCart cloned = (ShoppingCart)super.clone();
        cloned.cartItems = new ArrayList();
        cloned.cartItem.addAll(this.cartItems);
        return cloned;
    }
}

여기서 한가지 더 생각해 볼 수 있는 것은 Item이 제품 정보를 가진다면 같은 것을 참조하면 되지만(얕은 복사) 만약 주문 개수나 옵션 등 주문에 따라 달라질 수 있는 정보를 포함한 클래스라면 그것도 클론해서 cart2에서 바뀔 수 있게 해 주어야 한다. 이 경우 다시 Item의 clone을 불러서 복제된 아이템을 cart2의 ArrayList에 넣어야 한다.

이 주제에 관해서 한 가지 더 추가할 것은 clone이 꼭 필요한 기능은 아니라는 점이다. 대신에 생성할 때 복사해서 그대로 생성하는 "복사생성자"를 이용하는 것이 더 바람직하다. 그러면 위의 clone 대신 다음과 같이 생성자를 추가하면 된다.[각주:1]

class ShoppingCart implements Cloneable {
    String userName;
    ArrayList cartItems = new ArrayList<>();
    ShoppingCart(ShoppingCart cart) {
        cartItems = new ArrayList();
        cartItem.addAll(cart.cartItems);
    }
}

 

  1. 생성자는 하나도 없을 때는 묵시적으로 아무일도 안하는 디폴트 생성자(매개변수 없는 생성자)가 생기지만 다른 생성자가 있을 때는 디폴트 생성자가 생기지 않는다. 그러므로 디폴트 생성자가 필요하면 추가로 정의해 주어야 한다. [본문으로]
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함