clone은 객체를 복사한다.
Cloneable 구현 예시
public class TestCloneDto implements Cloneable {
private String name;
private String email;
public String toString () {
return " name : " + name + " email : " + email;
}
public TestCloneDto(String name, String email) {
this.name = name;
this.email = email;
}
@Override
protected TestCloneDto clone() throws CloneNotSupportedException {
return (TestCloneDto) super.clone();
}
public static void main(String[] args) {
TestCloneDto testCloneDto = new TestCloneDto("girinDev", "girin_dev@gmail.com");
try {
TestCloneDto testCloneDto1 = testCloneDto.clone();
System.out.println(testCloneDto.toString());
System.out.println("#####1");
System.out.println(testCloneDto1.toString());
System.out.println("#####2");
} catch (Exception e) {
e.printStackTrace();
}
}
}

🥕 Cloneable을 구현흔 클래스의 인스턴스에서 clone()을 호출하면 객체의 필드를 모두 복사한 객체를 반환한다.
하지만 저자가 이렇게 사용하지 말아야 한다고 못박았다.
1. x.clone() != x 는 참이다.
2. x.clone().getClass() == x.getClass() 는 참이다.
3. x.clone().equals(x) 는 참이다.
@0verride
public PhonNumber clone() {
try{
return (PhonNumber) super.clone();
}catch (CloneNotSupportedException) {
thrownewAssertionError(); //일어날수없는일이다.
}
}
위에 작성한 코드는 CloneNotSupportedException이 비검사 예외 였어야 한다.
하지만 위의 클래스가 가변 객체를 참조한다면
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object [DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 참조 해제
return result;
}
// 원소를 위한 공간을 적어도 하나 이상 확보한다.
private void ensureCapacity() {
if (elements.1ength==size)
elements=Arrays.copyOf(elements, 2 * size十1);
}
}
클론값과 원본이 동일한 상황에서 --> 원본이나 복제본 중 하나를 수정하면, 동일한 값을 보장 할 수가 없다.
clone 메서드는 생성자와 같은 효과를 낸다. 원본 객체에 아무런 해를 끼치지 않으며, 복제된 객체의 불변식을 보장해야 clone 메서드의 의의가 있다.
따라서 Stack과 자료 구조에서 clone 메서드는 스택의 내부 정보도 복사해야 한다.
clone을 재귀적으로 호출한다면 스택 내부 정보도 복사가 된다.
@0verride
public Stack clone() {
try{
Stackresult= (stack) super.clone();
result.elements=elements.clone();
returnresult;
}catch (CIoneNotsupportedExceptione) {
thrownewAssertionError();
}
}
🤖 저자가 말하길 배열을 복제할 때는 배열의 clone 메서드를 쓰는게 clone 기능을 제대로 사용하는 유일한 예라고 한다.
하지만 가변 객체를 참조하는 필드는 final로 선언하는 일반 용법과 충돌하게 되므로, 복제할 수 있는 클래스를 위해서는 일부 필드에서 final 한정자를 제거해야 할 수도 있다.
🥕 그 외에도 HashTable에서 clone 메서드를 사용 할 경우에도
해시테이블의 자료 구조 특성때문에, 원본과 같은 연결 리스트를 참조한 복제본이 생길 수 있다.
물론 복제본은 자신만의 버킷 배열을 가지지만, 깊은 복사가 아닐 경우 예기치 못한 동작의 가능성이 있다고 한다.
문제는 깊은 복사를 할 경우 재귀 호출로 인해 리스트의 원소 개수만큼 스택 프레임을 소비하므로, 리스트가 길면 스택 오버플로를 일으킬 수 있음에 유의해야 한다.
따라서. 깊은 복사를 재귀 호출 대신 반복자를 써서 순회하는 방향으로 수정해야 한다.
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next=newEntry(p.next.key, p.next.value, p.next.next);
return result;
}
결론적으로 저자는 복잡한 필드 및 가변 객체를 복제하려한다면,
HashTable의 경우에는
1. super.clone을 호출 해서 얻은 객체의 모든 필드를 초기화하고,
2. 원본 객체의 상태를 다시 생성하는 메서드를 호출하고,
3. buckets 필드를 모두 초기화 하고,
4. 원본 테이블의 모든 key-value 쌍들을 다시 복제한(클론한) 테이블의 버킷 배열 put(key, value)를 호출해 똑같이 만든다.
론 느리다고한다. 객체 복사를 위해 우회(초기화하고 다시 put(key, value)처리 하는 것을 말하는 듯 하다.) 하므로,
물론 위의 메서드를 통한 처리는 final 이거나 private이어야 한다.
위와 같은 처리와 더불어 저자가 원하는 방향은 상속해서 쓰기 위한 클래스 설계에서,
상속용 클래스는 Cloneable을 구현해서는 안 된다는 것이다.


결론적으로는 Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다고 한다.
물론 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 하되, super.clone을 호출하고,
필요한 필드를 전부 적절히 수정해야 한다. --> 깊은 복사 ( 가변 객체들 모두 ) 가 이루어지도록.
이렇게 복잡한 과정과 원칙을 따지면서 까지 Cloneable 인터페이스를 구현하는 상황이 필요 할 지도 의문이다.
하지만 쓰는 상황이 있었고,
[Java] Cloneable 상속 없이 깊은 복사(deep copy) 해주는 library 소개
최근 운영업무를 보던 중 dto 객체의 값을 수정할 일이 있어서 수정을 했는데, 배포 후 사이드 이펙트가 여럿 터지는 일이 있었다. 당연히 해서는 안되는 일이었지만 dto 의 값을 변경하게 되면서
velog.io
라이브러리 속도도 비교한 블로그가 있다. 감사합니다.
잘못된 점을 지적해주세요. 많이 배우겠습니다.
'Effective Java' 카테고리의 다른 글
[아이템 50] 적시에 방어적 복사본을 만들라 (0) | 2023.07.24 |
---|---|
[아이템 18] 상속보다는 컴포지션을 사용하라 (0) | 2023.04.06 |
[ 아이템 12 ] toString (0) | 2023.02.14 |
clone은 객체를 복사한다.
Cloneable 구현 예시
public class TestCloneDto implements Cloneable {
private String name;
private String email;
public String toString () {
return " name : " + name + " email : " + email;
}
public TestCloneDto(String name, String email) {
this.name = name;
this.email = email;
}
@Override
protected TestCloneDto clone() throws CloneNotSupportedException {
return (TestCloneDto) super.clone();
}
public static void main(String[] args) {
TestCloneDto testCloneDto = new TestCloneDto("girinDev", "girin_dev@gmail.com");
try {
TestCloneDto testCloneDto1 = testCloneDto.clone();
System.out.println(testCloneDto.toString());
System.out.println("#####1");
System.out.println(testCloneDto1.toString());
System.out.println("#####2");
} catch (Exception e) {
e.printStackTrace();
}
}
}

🥕 Cloneable을 구현흔 클래스의 인스턴스에서 clone()을 호출하면 객체의 필드를 모두 복사한 객체를 반환한다.
하지만 저자가 이렇게 사용하지 말아야 한다고 못박았다.
1. x.clone() != x 는 참이다.
2. x.clone().getClass() == x.getClass() 는 참이다.
3. x.clone().equals(x) 는 참이다.
@0verride
public PhonNumber clone() {
try{
return (PhonNumber) super.clone();
}catch (CloneNotSupportedException) {
thrownewAssertionError(); //일어날수없는일이다.
}
}
위에 작성한 코드는 CloneNotSupportedException이 비검사 예외 였어야 한다.
하지만 위의 클래스가 가변 객체를 참조한다면
public class Stack {
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack() {
this.elements = new Object [DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e) {
ensureCapacity();
elements[size++] = e;
}
public Object pop() {
if (size == 0)
throw new EmptyStackException();
Object result = elements[--size];
elements[size] = null; // 참조 해제
return result;
}
// 원소를 위한 공간을 적어도 하나 이상 확보한다.
private void ensureCapacity() {
if (elements.1ength==size)
elements=Arrays.copyOf(elements, 2 * size十1);
}
}
클론값과 원본이 동일한 상황에서 --> 원본이나 복제본 중 하나를 수정하면, 동일한 값을 보장 할 수가 없다.
clone 메서드는 생성자와 같은 효과를 낸다. 원본 객체에 아무런 해를 끼치지 않으며, 복제된 객체의 불변식을 보장해야 clone 메서드의 의의가 있다.
따라서 Stack과 자료 구조에서 clone 메서드는 스택의 내부 정보도 복사해야 한다.
clone을 재귀적으로 호출한다면 스택 내부 정보도 복사가 된다.
@0verride
public Stack clone() {
try{
Stackresult= (stack) super.clone();
result.elements=elements.clone();
returnresult;
}catch (CIoneNotsupportedExceptione) {
thrownewAssertionError();
}
}
🤖 저자가 말하길 배열을 복제할 때는 배열의 clone 메서드를 쓰는게 clone 기능을 제대로 사용하는 유일한 예라고 한다.
하지만 가변 객체를 참조하는 필드는 final로 선언하는 일반 용법과 충돌하게 되므로, 복제할 수 있는 클래스를 위해서는 일부 필드에서 final 한정자를 제거해야 할 수도 있다.
🥕 그 외에도 HashTable에서 clone 메서드를 사용 할 경우에도
해시테이블의 자료 구조 특성때문에, 원본과 같은 연결 리스트를 참조한 복제본이 생길 수 있다.
물론 복제본은 자신만의 버킷 배열을 가지지만, 깊은 복사가 아닐 경우 예기치 못한 동작의 가능성이 있다고 한다.
문제는 깊은 복사를 할 경우 재귀 호출로 인해 리스트의 원소 개수만큼 스택 프레임을 소비하므로, 리스트가 길면 스택 오버플로를 일으킬 수 있음에 유의해야 한다.
따라서. 깊은 복사를 재귀 호출 대신 반복자를 써서 순회하는 방향으로 수정해야 한다.
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next=newEntry(p.next.key, p.next.value, p.next.next);
return result;
}
결론적으로 저자는 복잡한 필드 및 가변 객체를 복제하려한다면,
HashTable의 경우에는
1. super.clone을 호출 해서 얻은 객체의 모든 필드를 초기화하고,
2. 원본 객체의 상태를 다시 생성하는 메서드를 호출하고,
3. buckets 필드를 모두 초기화 하고,
4. 원본 테이블의 모든 key-value 쌍들을 다시 복제한(클론한) 테이블의 버킷 배열 put(key, value)를 호출해 똑같이 만든다.
론 느리다고한다. 객체 복사를 위해 우회(초기화하고 다시 put(key, value)처리 하는 것을 말하는 듯 하다.) 하므로,
물론 위의 메서드를 통한 처리는 final 이거나 private이어야 한다.
위와 같은 처리와 더불어 저자가 원하는 방향은 상속해서 쓰기 위한 클래스 설계에서,
상속용 클래스는 Cloneable을 구현해서는 안 된다는 것이다.


결론적으로는 Cloneable을 구현하는 모든 클래스는 clone을 재정의해야 한다고 한다.
물론 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 하되, super.clone을 호출하고,
필요한 필드를 전부 적절히 수정해야 한다. --> 깊은 복사 ( 가변 객체들 모두 ) 가 이루어지도록.
이렇게 복잡한 과정과 원칙을 따지면서 까지 Cloneable 인터페이스를 구현하는 상황이 필요 할 지도 의문이다.
하지만 쓰는 상황이 있었고,
[Java] Cloneable 상속 없이 깊은 복사(deep copy) 해주는 library 소개
최근 운영업무를 보던 중 dto 객체의 값을 수정할 일이 있어서 수정을 했는데, 배포 후 사이드 이펙트가 여럿 터지는 일이 있었다. 당연히 해서는 안되는 일이었지만 dto 의 값을 변경하게 되면서
velog.io
라이브러리 속도도 비교한 블로그가 있다. 감사합니다.
잘못된 점을 지적해주세요. 많이 배우겠습니다.
'Effective Java' 카테고리의 다른 글
[아이템 50] 적시에 방어적 복사본을 만들라 (0) | 2023.07.24 |
---|---|
[아이템 18] 상속보다는 컴포지션을 사용하라 (0) | 2023.04.06 |
[ 아이템 12 ] toString (0) | 2023.02.14 |