반응형
shallow cloning과 deep cloning의 비교
간단한 객체 클로닝 구현
모든 클래스의 부모인 Object 클래스는 객체의 클로닝을 위한 Object.clone() 메소드를 제공하고 있다. 객체의 복사는 객체 생성의 또 다른 방법으로 new 연산자를 통한 객체 생성 이외의 다른 방법을 제공한다. 클로닝을 통한 객체 복사를 이용해 객체를 생성할 경우 객체의 생성자 메소드를 실행하지 않으며, 객체가 유지하고 있는 상태값들을 그대로 복사하게 된다. 이때 복사되는 원래 객체에는 아무런 변화를 주지 않는다. Object.clone() 메소드는 객체 복사의 좋은 시작점으로 객체 구조의 완벽한 복사를 제공한다.
그러나 여기에는 몇가지 주의할 문제점이 있는데 바로 객체 레퍼런스의 복사에 관한 문제이다. Object.clone() 메소드는 객체 구조를 복사하지만 객체 레퍼런스의 경우 레퍼런스만 복사한다. 즉, 객체 레퍼런스가 가리키고 있는 실제 객체는 복사하지 않는다. 이것을 shallow cloning이라고 하며, 레퍼런스로 가리키는 객체까지 모두 복사하는 deep cloning과 구별된다.
첫번째 간단한 클로닝 구현
단순한 객체의 클로닝은 간단하다. Cloneable 인터페이스를 구현(implement, 실제로는 상속)한 다음, clone() 메소드를 오버라이드한다. 새로운 객체의 생성과 객체 구조의 복사를 완전히 지원하는 Object.clone() 메소드를 호출하기 위하여 super.clone() 메소드를 호출한다. 다은은 간단한 클로닝의 구현을 보여준다.
이런 클로닝은 shallow cloning으로 필드 멤버가 전부 primitive 타입인 경우에 유용하다. (primitive 타입이란 객체의 레퍼런스가 아닌 int, char, double 등의 단일 값을 저장하는 데이터 타입을 말한다)
clone() 메소드를 작성하는데 필요한 세가지 기본 요소
클로닝을 위해 사용되는 필수적인 세가지 기본 요소는 다음과 같다.
Cloneable 인터페이스
자신이 만들 클래스에 clone() 메소드를 제공하기 위해서는 Cloneable 인터페이스를 반드시 상속 구현해야 한다. (구현할 메소드는 하나도 없는 마커 인터페이스다.)
Object 클래스에서 구현된 clone 메소드
객체의 구조를 복사하는 기본적인 클로닝 메커니즘을 수행하는 메소드이다. 객체의 모든 필드를 복사하며, 객체 클로닝을 위한 첫 출발점이 된다.
CloneNotSupportedException 클래스
클래스의 clone() 메소드가 지원되지 않을 경우를 나타내기 위한 예외 클래스.
Object.clone() 메소드
Object 클래스에서 제공하는 clone() 메소드는 다음과 같이 정의된다.
protected Object clone() throws CloneNotSupportedException
Object.clone() 메소드는 바로 이 객체의 복사본을 생성하여 반환한다. 복사의 의미는 클래스에 따라 다르다. 보통 x.clone() != x 즉, 복사된 객체와 원래 객체는 다른 객체이며, x.clone().getClass() == x.getClass() 즉, 원본과 복사본 객체의 클래스는 같다. 하지만 반드시 지켜야 할 요구조건은 아니다. 대체로 x.clone().equals(x) 즉, 원본 객체와 복사본 객체는 equals() 메소드에 참값을 나타내지만 이것 역시 꼭 지켜야 할 요구조건은 아니다.
클로닝을 통한 객체의 복사는 새로운 객체를 생성하고 내부의 데이터 구조들을 복사하지만 생성자 메소드는 실행하지 않는다.
Object.clone() 메소드는 다음과 같은 작업을 수행한다. 제일 먼저 Cloneable 인터페이스를 구현(implement, 실제 의미는 상속)했는지 확인하고, 구현하지 않았다면 CloneNotSupportedException 예외 객체를 던진다(throw). Cloneable 인터페이스가 구현되어 있으면 원본과 같은 객체를 새로 생성하고, 모든 필드 멤버들을 원본의 필드와 완전히 같은 값으로 초기화한다. 이때 필드 멤버의 내용은 새로 생성되지 않고 단지 대입에 의해 복사된다. 즉, 원본 객체를 deep cloning하지 않고 shallow cloning 한다.
참고로 모든 배열은 기본적으로 Cloneable 인터페이스를 구현한 것으로 가정된다.
Object 클래스는 그 자체로는 Cloneable 인터페이스를 구현하지 않았으므로, Object에서 직접 clone() 메소드를 호출하는 것은 CloneNotSupportedException 예외를 발생시킨다.
CloneNotSupportedException 예외는 실행 시 발생하는 예외로 컴파일 시에는 발견되지 않는다.
Object.clone() 메소드는 Object 클래스에서 구현되어 있으므로 상속받은 클래스가 손쉽게 Cloneable 인터페이스를 구현함으로써 clone() 메소드를 호출할 수 있게 되며, 대부분의 경우 clone() 메소드를 오버라이딩하여 사용하게 된다. 이 경우 오버라이딩한 메소드는 Object.clone() 메소드의 기능을 호출하기 위하여 super.clone() 메소드를 호출하면 된다.
clone() 메소드가 발생할 수 있는 예외는 두 종류다. 하나는 CloneNotSupportedException이며 Cloneable 인터페이스를 구현하지 않았거나, 상속된 클래스가 클로닝될 수 없음을 나타내기 위하여 일부러 발생시키는 경우이다. 다른 하나는 OutOfMemoryError이며 객체를 생성할 수 있는 메모리가 부족할 때 발생되는 에러이다.
실제 클로닝의 예
실제 클로닝의 예 #1 : 배열을 포함하는 객체의 클로닝
배열을 포함하는 클래스의 클로닝은 배열의 복사를 따로 제어해주어야 한다. Object.clone() 메소드는 배열의 레퍼런스만을 복사하므로 배열 자체는 복사되지 않는다. 배열의 내용이 절대로 변경되지 않는다면 문제가 없지만 배열의 내용이 바뀔 경우 원본과 복사본이 같이 영향을 받는다. 따라서 좀더 깊게 deep cloning을 시도해야 한다.
이 객체의 clone() 메소드는 MyNumbers 객체의 복사본을 만들지만 배열에 대해서는 새로운 배열을 생성하지 않고 원본에서 생성된 배열을 공유하게 된다. 원본의 배열을 가리키는 numbers 레퍼런스의 레퍼런스 값만 복사하기 때문이다. 이제 shallow cloning을 deep cloning으로 바꾸어보자.
새로운 clone() 메소드는 먼저 super.clone() 메소드로 Object.clone() 메소드를 호출하여 완전한 객체 구조를 복사하고, deep cloning을 위하여 배열을 클로닝하여 새로 생성된 클론 객체에 대입해 준다. 이로서 완전한 deep cloning을 구현할 수 있다. 앞에도 설명했듯이 배열은 기본적으로 Cloneable 인터페이스를 구현했으므로 클로닝이 가능하다. 따라서 이 경우에는 CloneNotSupportedException 예외가 발생할 원인이 없다.
실제 클로닝의 예 #2 : 객체 레퍼런스를 포함하는 객체의 클로닝
객체 레퍼런스를 포함하는 경우에는 배열보다 조금 더 주의하여야 한다. 기본적으로는 객체의 레퍼런스만 복사되므로, deep cloning을 위하여 배열의 경우와 같은 방법을 사용하면 된다.
배열의 경우와 마찬가지로 완전한 클로닝을 제공하는 것 같지만 한가지 문제가 있다. 배열은 기본적으로 클로닝을 제공하지만 MyData 클래스는 클로닝을 제공한다는 보장이 없다. 따라서 MyData 클래스가 클로닝을 보장하지 않는다면 즉, Cloneable 인터페이스를 구현하지 않았다면 MyClass의 clone() 메소드는 항상 CloneNotSupportedException 예외를 발생할 것이다. 따라서 deep cloning을 위해서는 MyData 클래스의 클로닝을 보장할 필요가 있다.
실제 클로닝의 예 #3 : Collection 계열의 객체를 포함하는 객체의 클로닝
콜렉션 계열의 객체를 포함하는 클래스는 클로닝에 조금 더 세심한 주의를 필요로 한다. 왜냐하면 콜렉션 내에 어떠한 타입의 객체도 저장될 수 있으므로 저장된 객체 중 하나라도 Cloneable 인터페이스를 구현하지 않은 객체가 있다면 클로닝에서 CloneNotSupportedException 예외가 발생할 수 있기 때문이다. 이런 이유로 콜렉션 계열의 클래스는 기본적으로 Cloneable 인터페이스를 구현하지만 반드시 클로닝에 성공하는 것은 아니다. 콜렉션 계열의 Vector 클래스를 예로 들면 다음과 같다.
객체의 레퍼런스의 클로닝과 마찬가지로 콜렉션 계열의 Vector 클래스도 클로닝을 지원하므로 (다시 말해서 Cloneable 인터페이스를 구현하였으므로) clone() 메소드를 사용하여 deep 클론을 구현할 수가 있다. 그러나 Vector 객체에 저장된 객체들이 모두 클로닝을 지원하지 않고 하나라도 CloneNotSupportedException 예외를 발생하면 deep cloning에 실패하게 된다.
shallow cloning과 deep cloning 중 어느 것을 택할 것인가?
어떤 클래스를 완전히 deep cloning하기 위해서는 그 클래스로부터 참조되는 모든 클래스를 추적해서 복사하여야 한다. 위의 예에서는 한단계 깊이의 클로닝이지만 필드 멤버가 가리키는 객체의 내부에 다시 객체 레퍼런스인 필드 멤버가 있고 다시 그 객체에 객체 레퍼런스가 있는 식의 구조를 지닌 경우 어느 깊이까지 클로닝을 해야 하는가는 개발자가 선택해야 한다. shallow cloning을 채택하여 내부에 클로닝의 범위가 미치지 않는다면 클로닝에 실패하고 CloneNotSupportedException 예외 발생을 허용할 것인지, 아니면 내부 객체나 배열을 공유할 것인지 미리 결정하여야 한다. 공유의 경우에는 값의 변경이 치명적인 영향을 미칠 수 있다는 것을 고려하여야 한다.
참고로 객체 직렬화(serialization)는 deep cloning으로 객체의 참조 그래프를 모두 복제하여 스트림으로 전송하게 된다.
결론
객체를 생성하는 또 다른 메커니즘인 클로닝에 대하여 알아보았다. 그리고, 이러한 객체의 복사를 위하여 제공되는 기본 요소로서, 객체 구조의 완전한 복사를 제공하는 Object.clone() 메소드, Cloneable 인터페이스, CloneNotSupportedException 예외 클래스에 대하여 알아보았다. 또한, 객체 클로닝에서 객체의 참조 그래프에 대한 복사의 깊이 정도를 나타내는 shallow cloning과 deep cloning에 대하여 알아보고, 그 조건에 대하여 알아보았다.
본 글의 저작권은 이동훈에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.
간단한 객체 클로닝 구현
모든 클래스의 부모인 Object 클래스는 객체의 클로닝을 위한 Object.clone() 메소드를 제공하고 있다. 객체의 복사는 객체 생성의 또 다른 방법으로 new 연산자를 통한 객체 생성 이외의 다른 방법을 제공한다. 클로닝을 통한 객체 복사를 이용해 객체를 생성할 경우 객체의 생성자 메소드를 실행하지 않으며, 객체가 유지하고 있는 상태값들을 그대로 복사하게 된다. 이때 복사되는 원래 객체에는 아무런 변화를 주지 않는다. Object.clone() 메소드는 객체 복사의 좋은 시작점으로 객체 구조의 완벽한 복사를 제공한다.
그러나 여기에는 몇가지 주의할 문제점이 있는데 바로 객체 레퍼런스의 복사에 관한 문제이다. Object.clone() 메소드는 객체 구조를 복사하지만 객체 레퍼런스의 경우 레퍼런스만 복사한다. 즉, 객체 레퍼런스가 가리키고 있는 실제 객체는 복사하지 않는다. 이것을 shallow cloning이라고 하며, 레퍼런스로 가리키는 객체까지 모두 복사하는 deep cloning과 구별된다.
첫번째 간단한 클로닝 구현
단순한 객체의 클로닝은 간단하다. Cloneable 인터페이스를 구현(implement, 실제로는 상속)한 다음, clone() 메소드를 오버라이드한다. 새로운 객체의 생성과 객체 구조의 복사를 완전히 지원하는 Object.clone() 메소드를 호출하기 위하여 super.clone() 메소드를 호출한다. 다은은 간단한 클로닝의 구현을 보여준다.
public class Hello implements Cloneable {
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
.
.
.
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
.
.
.
}
이런 클로닝은 shallow cloning으로 필드 멤버가 전부 primitive 타입인 경우에 유용하다. (primitive 타입이란 객체의 레퍼런스가 아닌 int, char, double 등의 단일 값을 저장하는 데이터 타입을 말한다)
clone() 메소드를 작성하는데 필요한 세가지 기본 요소
클로닝을 위해 사용되는 필수적인 세가지 기본 요소는 다음과 같다.
Cloneable 인터페이스
자신이 만들 클래스에 clone() 메소드를 제공하기 위해서는 Cloneable 인터페이스를 반드시 상속 구현해야 한다. (구현할 메소드는 하나도 없는 마커 인터페이스다.)
Object 클래스에서 구현된 clone 메소드
객체의 구조를 복사하는 기본적인 클로닝 메커니즘을 수행하는 메소드이다. 객체의 모든 필드를 복사하며, 객체 클로닝을 위한 첫 출발점이 된다.
CloneNotSupportedException 클래스
클래스의 clone() 메소드가 지원되지 않을 경우를 나타내기 위한 예외 클래스.
Object.clone() 메소드
Object 클래스에서 제공하는 clone() 메소드는 다음과 같이 정의된다.
protected Object clone() throws CloneNotSupportedException
Object.clone() 메소드는 바로 이 객체의 복사본을 생성하여 반환한다. 복사의 의미는 클래스에 따라 다르다. 보통 x.clone() != x 즉, 복사된 객체와 원래 객체는 다른 객체이며, x.clone().getClass() == x.getClass() 즉, 원본과 복사본 객체의 클래스는 같다. 하지만 반드시 지켜야 할 요구조건은 아니다. 대체로 x.clone().equals(x) 즉, 원본 객체와 복사본 객체는 equals() 메소드에 참값을 나타내지만 이것 역시 꼭 지켜야 할 요구조건은 아니다.
클로닝을 통한 객체의 복사는 새로운 객체를 생성하고 내부의 데이터 구조들을 복사하지만 생성자 메소드는 실행하지 않는다.
Object.clone() 메소드는 다음과 같은 작업을 수행한다. 제일 먼저 Cloneable 인터페이스를 구현(implement, 실제 의미는 상속)했는지 확인하고, 구현하지 않았다면 CloneNotSupportedException 예외 객체를 던진다(throw). Cloneable 인터페이스가 구현되어 있으면 원본과 같은 객체를 새로 생성하고, 모든 필드 멤버들을 원본의 필드와 완전히 같은 값으로 초기화한다. 이때 필드 멤버의 내용은 새로 생성되지 않고 단지 대입에 의해 복사된다. 즉, 원본 객체를 deep cloning하지 않고 shallow cloning 한다.
참고로 모든 배열은 기본적으로 Cloneable 인터페이스를 구현한 것으로 가정된다.
Object 클래스는 그 자체로는 Cloneable 인터페이스를 구현하지 않았으므로, Object에서 직접 clone() 메소드를 호출하는 것은 CloneNotSupportedException 예외를 발생시킨다.
CloneNotSupportedException 예외는 실행 시 발생하는 예외로 컴파일 시에는 발견되지 않는다.
Object.clone() 메소드는 Object 클래스에서 구현되어 있으므로 상속받은 클래스가 손쉽게 Cloneable 인터페이스를 구현함으로써 clone() 메소드를 호출할 수 있게 되며, 대부분의 경우 clone() 메소드를 오버라이딩하여 사용하게 된다. 이 경우 오버라이딩한 메소드는 Object.clone() 메소드의 기능을 호출하기 위하여 super.clone() 메소드를 호출하면 된다.
clone() 메소드가 발생할 수 있는 예외는 두 종류다. 하나는 CloneNotSupportedException이며 Cloneable 인터페이스를 구현하지 않았거나, 상속된 클래스가 클로닝될 수 없음을 나타내기 위하여 일부러 발생시키는 경우이다. 다른 하나는 OutOfMemoryError이며 객체를 생성할 수 있는 메모리가 부족할 때 발생되는 에러이다.
실제 클로닝의 예
실제 클로닝의 예 #1 : 배열을 포함하는 객체의 클로닝
배열을 포함하는 클래스의 클로닝은 배열의 복사를 따로 제어해주어야 한다. Object.clone() 메소드는 배열의 레퍼런스만을 복사하므로 배열 자체는 복사되지 않는다. 배열의 내용이 절대로 변경되지 않는다면 문제가 없지만 배열의 내용이 바뀔 경우 원본과 복사본이 같이 영향을 받는다. 따라서 좀더 깊게 deep cloning을 시도해야 한다.
public class MyNumbers implements Cloneable {
private int[] numbers = null;
public MyNumbers() {
numbers = new int[10];
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
private int[] numbers = null;
public MyNumbers() {
numbers = new int[10];
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
이 객체의 clone() 메소드는 MyNumbers 객체의 복사본을 만들지만 배열에 대해서는 새로운 배열을 생성하지 않고 원본에서 생성된 배열을 공유하게 된다. 원본의 배열을 가리키는 numbers 레퍼런스의 레퍼런스 값만 복사하기 때문이다. 이제 shallow cloning을 deep cloning으로 바꾸어보자.
public Object clone() throws CloneNotSupportedException {
MyNumber myObj = super.clone();
myObj.numbers = (int[]) numbers.clone();
return myObj;
}
MyNumber myObj = super.clone();
myObj.numbers = (int[]) numbers.clone();
return myObj;
}
새로운 clone() 메소드는 먼저 super.clone() 메소드로 Object.clone() 메소드를 호출하여 완전한 객체 구조를 복사하고, deep cloning을 위하여 배열을 클로닝하여 새로 생성된 클론 객체에 대입해 준다. 이로서 완전한 deep cloning을 구현할 수 있다. 앞에도 설명했듯이 배열은 기본적으로 Cloneable 인터페이스를 구현했으므로 클로닝이 가능하다. 따라서 이 경우에는 CloneNotSupportedException 예외가 발생할 원인이 없다.
실제 클로닝의 예 #2 : 객체 레퍼런스를 포함하는 객체의 클로닝
객체 레퍼런스를 포함하는 경우에는 배열보다 조금 더 주의하여야 한다. 기본적으로는 객체의 레퍼런스만 복사되므로, deep cloning을 위하여 배열의 경우와 같은 방법을 사용하면 된다.
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;
}
}
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;
}
}
배열의 경우와 마찬가지로 완전한 클로닝을 제공하는 것 같지만 한가지 문제가 있다. 배열은 기본적으로 클로닝을 제공하지만 MyData 클래스는 클로닝을 제공한다는 보장이 없다. 따라서 MyData 클래스가 클로닝을 보장하지 않는다면 즉, Cloneable 인터페이스를 구현하지 않았다면 MyClass의 clone() 메소드는 항상 CloneNotSupportedException 예외를 발생할 것이다. 따라서 deep cloning을 위해서는 MyData 클래스의 클로닝을 보장할 필요가 있다.
실제 클로닝의 예 #3 : Collection 계열의 객체를 포함하는 객체의 클로닝
콜렉션 계열의 객체를 포함하는 클래스는 클로닝에 조금 더 세심한 주의를 필요로 한다. 왜냐하면 콜렉션 내에 어떠한 타입의 객체도 저장될 수 있으므로 저장된 객체 중 하나라도 Cloneable 인터페이스를 구현하지 않은 객체가 있다면 클로닝에서 CloneNotSupportedException 예외가 발생할 수 있기 때문이다. 이런 이유로 콜렉션 계열의 클래스는 기본적으로 Cloneable 인터페이스를 구현하지만 반드시 클로닝에 성공하는 것은 아니다. 콜렉션 계열의 Vector 클래스를 예로 들면 다음과 같다.
public class MyClass implements Cloneable {
private Vector vector = null;
public MyClass() {
vector = new Vector();
}
public Object clone() throws CloneNotSupportedException {
MyClass myObj = super.clone();
myObj.vector = (Vector) vector.clone();
return myObj;
}
}
private Vector vector = null;
public MyClass() {
vector = new Vector();
}
public Object clone() throws CloneNotSupportedException {
MyClass myObj = super.clone();
myObj.vector = (Vector) vector.clone();
return myObj;
}
}
객체의 레퍼런스의 클로닝과 마찬가지로 콜렉션 계열의 Vector 클래스도 클로닝을 지원하므로 (다시 말해서 Cloneable 인터페이스를 구현하였으므로) clone() 메소드를 사용하여 deep 클론을 구현할 수가 있다. 그러나 Vector 객체에 저장된 객체들이 모두 클로닝을 지원하지 않고 하나라도 CloneNotSupportedException 예외를 발생하면 deep cloning에 실패하게 된다.
shallow cloning과 deep cloning 중 어느 것을 택할 것인가?
어떤 클래스를 완전히 deep cloning하기 위해서는 그 클래스로부터 참조되는 모든 클래스를 추적해서 복사하여야 한다. 위의 예에서는 한단계 깊이의 클로닝이지만 필드 멤버가 가리키는 객체의 내부에 다시 객체 레퍼런스인 필드 멤버가 있고 다시 그 객체에 객체 레퍼런스가 있는 식의 구조를 지닌 경우 어느 깊이까지 클로닝을 해야 하는가는 개발자가 선택해야 한다. shallow cloning을 채택하여 내부에 클로닝의 범위가 미치지 않는다면 클로닝에 실패하고 CloneNotSupportedException 예외 발생을 허용할 것인지, 아니면 내부 객체나 배열을 공유할 것인지 미리 결정하여야 한다. 공유의 경우에는 값의 변경이 치명적인 영향을 미칠 수 있다는 것을 고려하여야 한다.
참고로 객체 직렬화(serialization)는 deep cloning으로 객체의 참조 그래프를 모두 복제하여 스트림으로 전송하게 된다.
결론
객체를 생성하는 또 다른 메커니즘인 클로닝에 대하여 알아보았다. 그리고, 이러한 객체의 복사를 위하여 제공되는 기본 요소로서, 객체 구조의 완전한 복사를 제공하는 Object.clone() 메소드, Cloneable 인터페이스, CloneNotSupportedException 예외 클래스에 대하여 알아보았다. 또한, 객체 클로닝에서 객체의 참조 그래프에 대한 복사의 깊이 정도를 나타내는 shallow cloning과 deep cloning에 대하여 알아보고, 그 조건에 대하여 알아보았다.
본 글의 저작권은 이동훈에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.