결말부터 보고 가자
✍ Cloneable/clone의 단점
- 언어 모순적이다.
- 생성자를 쓰지 않는 방식을 사용한다.
- 엉성하게 문서화된 규약을 가진다.
- 정상적인 final 필드 용법과 충돌한다.
- 불필요한 검사 예외를 던진다.
- 형변환이 필요하다.
단점이 상당하기 때문에 clone 대신에 대부분 복사 생성자와 복사 팩토리를 사용한다.
단, 배열을 복제하는 경우, final 클래스이며 별다른 문제가 없을 경우는 제외한다.
자바의 금쪽이 Cloneable/clone이 무엇인지 알아보자
Cloneable 인터페이스는 Object의 protected 메소드인 clone의 동작 방식을 결정한다. Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException(언체크 예외)을 던진다.
*Cloneable
Java의 Cloneable 인터페이스는 객체 복제를 지원하기 위한 인터페이스이다. Cloneable 인터페이스를 구현한 클래스는 자신을 복제할 수 있게 된다. 이를 위해서는 clone() 메소드를 오버라이드하여 구현해야 한다.
Cloneable 인터페이스는 복제가 가능한 클래스임을 표시하는 용도로만 사용되며, 복제를 위한 메소드를 정의하거나 구현하지 않는다. 따라서 Cloneable 인터페이스를 구현하더라도 clone() 메소드를 구현하지 않으면 CloneNotSupportedException이 발생한다.
clone() 메소드는 객체를 복제할 때 사용되며, 해당 객체와 같은 상태를 갖는 새로운 객체를 생성한다. 이때 객체의 복제는 얕은 복제가 일어난다. 이는 참조 타입 필드가 복제된 객체와 원조 객체가 같은 객체를 참조하게 되는 것을 의미한다. 따라서 깊은 복제를 수행하려면 clone() 메소드를 오버라이드하여 구현해야 한다. 깊은 복제는 참조 타입 필드에 대해서도 새로운 객체를 생성하여 복사하는 방식이다.
Cloneable 인터페이스는 자바의 객체 지향적인 특성을 활용하여 객체의 복제를 쉽게 구현할 수 있게 한다. 그러나 복제를 위해 clone() 메소드를 사용하는 것은 권장되지 않는다. clone() 메소드가 구현하기 어렵고 예측하기 어려운 경우가 많기 때문이다. 대신 복제를 위해 복사 생성자나 팩토리 메소드를 사용하는 것이 좋다.
*Mixin interface(item 20)
Java에서 인터페이스를 이용하여 여러 개의 클래스에서 공통으로 사용되는 기능을 추상화하여 구현하는 방식이다.
믹스인 인터페이스를 이용하면 기능을 중복해서 구현하는 것을 방지하고, 코드의 재사용성과 유지보수성을 높일 수 있다. 또한, 다중 상속을 지원하지 않는 자바에서 다중 상속과 유사한 효과를 얻을 수 있다.
대표적인 예로 Comparable 인터페이스의 compareTo() 메소드가 있다.
이쯤되면 인터페이스와 믹스인 인터페이스의 명확한 차이가 궁금해질 것이다. (나만 그런가?) 차이점은 목적과 사용 방법이다. 인터페이스는 클래스가 반드시 구현해야 하는 메소드의 시그니처를 정의하는 데에 목적이 있고, 믹스인 인터페이스는 클래스에서 상속할 수 있는 메소드의 구현을 제공하는 데에 목적이 있다.
*리플렉션(item 65)
자바에서 프로그램 실행 중에 클래스 정보를 가져오고, 클래스의 필드나 메소드 등을 조작할 수 있도록 하는 기능이다. 일반적으로 프레임워크나 라이브러리에서 많이 사용되며, 프로그램의 동적인 기능을 구현하는 데에 유용하게 사용된다. 그러나 리플렉션은 실행 시간에 객체의 정보를 동적으로 가져오기 때문에 성능 이슈가 있을 수 있고, 런타임 오류 발생 가능성도 높아지기 때문에 신중하게 사용해야 한다.
clone 메소드의 허술한 일반 규약
일반적으로는 아래 규약이 참이여야 한다. 그러나 '규약'인데 예외가 많아서 허술하다고 표현한 듯하다. (허술하긴 하네..)
x.clone() != x
불변 객체라면 복제한 객체와 원본 객체가 서로 다른 객체이기 때문에 참이다. 가변 객체이지만 clone() 메소드를 오버라이딩하여 복제 시에도 같은 참조를 공유하도록 구현한 경우에는 거짓이 된다.
x.clone().getClass() == x.getClass()
이 경우는 어떨 때 거짓인지 생각이 안 난다..chatGPT한테 물어봤는데 "final 클래스에서 clone() 메소드를 오버라이딩하지 않은 경우" 거짓이라는데 오버라이딩하지 않으면 CloneNotSupportedException 에러 발생하지 않나 ???(스터디에서 공격할 예정)
x.clone().equals(x)
복제된 객체와 원본 객체가 같은 내용을 가지고 있어도 참조가 다를 수 있기 때문에 거짓이 될 수 있다.
super.clone 대신 생성자를 호출할 때 문제점
super.clone 대신 생성자를 호출하여 객체를 생성하면 하위 클래스에서 clone() 메소드가 제대로 동작하지 않는다. 아래 예시를 보자.
public class Person implements Cloneable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
// clone() 메소드 오버라이딩
@Override
public Object clone() throws CloneNotSupportedException {
return new Person(this.name, this.age); // 생성자를 호출하여 객체를 생성
}
// getter, setter 생략
}
public class Student extends Person {
private String school;
public Student(String name, int age, String school) {
super(name, age);
this.school = school;
}
// clone() 메소드 오버라이딩
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // 오류 발생!
}
// getter, setter 생략
}
해당 예시는 Student 클래스에서 super.clone() 메소드를 호출하면, Person 클래스에서 생성자를 호출하여 객체를 생성하도록 구현하였다. 이 경우 복제된 객체는 Person 클래스의 객체이며, Student 클래스의 객체가 아니므로 Student 클래스에서 추가한 필드나 메소드는 복제되지 않는다.
단, 클래스가 final인 경우 하위 클래스가 없으니 상관없다.
*생성자 연쇄
자바에서 객체 생성 시 다른 생성자를 호출하여 코드의 중복을 방지하는 방법이다. 같은 클래스 내에서 다른 생성자를 호출하는 방식과 부모 클래스의 생성자를 호출하는 방식으로 나뉜다.
불변 클래스는 clone 메소드를 제공하지 않는 것이 좋다.
복제를 통한 객체의 변경이 필요하지 않고, 불변 클래스의 특성에 어긋나는 결과를 가져올 수 있기 때문에 제공하지 않는 것이 좋다.
거의 유일해보이는(내 생각) clone 메소드의 규약 :
Cloneable을 구현한 클래스에서 clone() 메소드를 public으로 오버라이딩해야 하는 이유
clone() 메소드가 Object 클래스의 protected 메소드이기 때문이다. 다시 말해 public으로 오버라이딩하지 않으면, 외부에서 복제 작업을 수행할 수 없다.
만약 clone() 메소드를 protected로 오버라이딩한다면,
public class MyClass implements Cloneable {
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
외부에서 복제하려면 캐스팅이 필요하다.
MyClass obj = new MyClass();
MyClass copy = (MyClass) obj.clone(); // 캐스팅 필요
이런 방식으로 캐스팅 하는 것을 권장하지 않기 때문에(가독성, 오류 발생 가능성 문제), 꼭 public으로 오버라이딩해야 한다.
clone 메소드의 유일한 장점
clone 메소드는 원본 객체와 독립적이어야 한다. super.clone한다면 Object[] elements 필드는 원본 인스턴스와 같은 배열을 참조하게 된다(얕은 복사). 즉, 독립적이지 못하다. 드디어 이때 clone 메소드의 능력을 볼 수 있다. 아래 처럼 elements 배열의 clone을 재귀적으로 호출하면 간단히 해결된다.
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
심지어 elements.clone의 결과를 Object[]로 형변환할 필요없다. 배열의 clone은 원본 배열과 동일한 배열을 반환하기 때문이다.
그러나 final 필드라면 힘을 못 쓰는 clone ..
elements 배열이 final로 선언되면 elements.clone()에서 복제된 배열은 동일한 객체를 참조하게 된다.
Cloneable 아키텍쳐는 "가변 객체를 참조하는 필드는 final로 선언하라"와 어긋나게 된다.(이쯤되면 안타깝다. 동일한 참조를 공유해도 안전하다면 상관없지만 ..)
복잡한 가변 상태를 갖는 클래스
심지어 clone 메소드를 재귀적으로 호출해도 해결되지 않는 경우가 있다.
1. clone 메소드를 재귀적으로 호출
아래의 경우에는 원본과 같은 연결 리스트를 참조하게 된다. 이 배열의 각 원소는 Entry 객체에 대한 참조다. 따라서 buckets 배열을 복제하더라도 원본과 동일한 연결 리스트를 참조하게 된다.
public class HashTable implements Cloneable {
private Entry[] buckets = ...;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
}
... // 나머지 코드는 생략
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = buckets.clone();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
2. 재귀 호출으로 깊은 복사
아래 코드는 깊은 복사를 하여 다른 연결 리스트를 참조하도록 한 예시이다. 그러나 아래의 경우 리스트가 길어지면 재귀 호출로 인한 스택 오버플로가 발생한다.
public class HashTable implements Cloneable {
...
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++)
if (buckets[i] != null)
result.buckets[i] = buckets[i].deepCopy();
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
3. 반복자로 깊은 복사
2번 문제를 해결하기 위해 반복자를 사용하는 방법도 있다.
Entry deepCopy() {
Entry result = new Entry(key, value, next);
for (Entry p = result; p.next != null; p = p.next)
p.next = new Entry(p.next.key, p.next.value, p.next.next);
return result;
}
4. 고수준 API를 활용해 복제
아래 코드는 clone() 메소드에서 super.clone()을 호출하여 HashTable 객체의 필드를 복사한 후, buckets 배열을 새로운 배열로 초기화한다. 그리고 원본 객체의 buckets 배열에 담긴 모든 키-값 쌍을 복제하여 복제본 객체의 buckets 배열에 추가한다. 복제 과정에서 각 엔트리 객체를 복제하기 위해 Entry 클래스에 deepCopy() 메소드를 추가하였다. 이 메소드는 엔트리 객체를 깊은 복사하여 새로운 엔트리 객체를 생성한다. 이렇게 생성된 엔트리 객체를 복제본 객체의 buckets 배열에 추가한다.
이는 저수준에서 바로 처리할 때보다 느리며, Cloneable 아키텍처의 기초가 되는 필드 단위 객체 복사를 우회하기 때문에 적합하지 않다.
public class HashTable implements Cloneable {
private Entry[] buckets;
private static class Entry {
final Object key;
Object value;
Entry next;
Entry(Object key, Object value, Entry next) {
this.key = key;
this.value = value;
this.next = next;
}
Entry deepCopy() {
return new Entry(key, value, next == null ? null : next.deepCopy());
}
}
public void put(Object key, Object value) {
int hash = key.hashCode();
int index = hash % buckets.length;
for (Entry e = buckets[index]; e != null; e = e.next) {
if (e.key.equals(key)) {
e.value = value;
return;
}
}
buckets[index] = new Entry(key, value, buckets[index]);
}
@Override
public HashTable clone() {
try {
HashTable result = (HashTable) super.clone();
result.buckets = new Entry[buckets.length];
for (int i = 0; i < buckets.length; i++) {
if (buckets[i] != null) {
result.buckets[i] = buckets[i].deepCopy();
}
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
clone 메소드 주의해야 할 점
1. 생성자에서는 재정의될 수 있는 메소드(ex. clone 메소드)를 호출하지 않아야 한다.(item 19)
*아이템 19 맛보기
서브 클래스가 부모 클래스의 메소드를 재정의할 수 있기 때문에, 생성자에서는 재정의될 수 있는 메소드를 호출하지 않아야 한다. 만약 생성자가 부모 클래스의 재정의 가능한 메소드를 호출하면, 이를 재정의한 서브 클래스에서 원하지 않는 동작이 발생할 수 있다.
이해가 부족하다면 아래 예시를 보자.
class Parent { public Parent() { someMethod(); } public void someMethod() { System.out.println("부모가 부름"); } } class Child extends Parent { public void someMethod() { System.out.println("자식이 부름"); } } public class Main { public static void main(String[] args) { Child c = new Child(); // 자식이 부름 } }
만약 앞서 다룬 put(key, value) 메소드는 final이거나 private일 경우, 하위 클래스에서 재정의될 수 없으므로로 해당 문제가 발생하지 않는다. 그러나 final이나 private로 선언된 메소드를 호출하는 것은 public으로 선언된 메소드보다 일반적으로 성능이 좋지 않다. 따라서 생성자에서 재정의될 수 있는 메소드를 호출하는 것은 좋지 않다.
2. 상속용 클래스는 Cloneable을 구현하면 안 된다.
상속해서 쓰기 위한 클래스 설계 방식 두 가지(item 19)(미래의 나는 알겠지..?) 모두 상속용 클래스는 Clonealble을 구현해서는 안 된다.
✍ 방법 1
clone 메소드를 protected로 하고 CloneNotSupportedException도 던지도록 구현한다. 하위 클래스에서 clone() 메소드를 사용하지 않는 경우에도 별도의 예외 처리를 할 필요가 없어져 코드가 간결해진다. 즉, 이렇게 할 경우 Cloneable 구현 여부는 하위 클래스에서 선택할 수 있다.
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
✍ 방법 2
clone을 동작하지 않게 구현한 후 하위 클래스에서 재정의하지 못하게 한다.
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
3. Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메소드 또한 적절히 동기화해줘야 한다.(item 78)
Cloneable을 구현한 스레드 안전 클래스에서 clone 메소드를 적절히 동기화하지 않으면, 여러 스레드가 동시에 객체를 복제할 때 의도치 않은 결과가 발생할 수 있다. clone 메소드가 호출되는 동안 다른 스레드에서 해당 상태를 변경할 수 있다. 이를 방지하기 위해 clone 메소드를 호출하는 동안 다른 스레드에서 해당 객체를 변경하지 못하도록 동기화해주는 것이 필요하다.
복사 생성자와 복사 팩토리
: clone의 문제점을 대부분 해결해주는 ..
*복사 생성자
자신과 같은 클래스의 인스턴스를 인수로 받는 생성자
*복사 팩토리
복사 생성자를 모방한 정적 팩토리
public Person(Person person) {}; // 복사 생성자
public static Person newInstance(Person person) {}; // 복사 팩토리
복사 생성자와 복사 팩토리는 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다. 따라서 클라이언트가 복제본의 타입을 직접 선택할 수 있다는 장점이 있다.
✍ 예시 1
자바에서 제공하는 범용 컬렉션 구현체는 모두 Collection이나 Map 인터페이스를 구현한다. 이들 구현체는 일반적으로 생성자를 제공하여 인자로 받은 Collection이나 Map 객체를 복제할 수 있도록 구현한다. 이러한 기능을 인터페이스 기반 변환 생성자라고 한다.
✍ 예시 2
HashSet과 TreeSet은 Set 인터페이스를 구현한다. HashSet 객체 s를 TreeSet 타입으로 복제하려면 new TreeSet<>(s)처럼 TreeSet의 생성자에 인자로 넘기기만 하면 된다.
'Language > Java' 카테고리의 다른 글
[effective java] 아이템 15. 클래스 멤버의 접근 권한을 최소화하라. (0) | 2023.03.25 |
---|---|
[effective java] 아이템 14. Comparable을 구현할지 고려하라. (0) | 2023.03.25 |
[effective java] 아이템 12. toString을 항상 재정의하라. (0) | 2023.03.09 |
[effective java] 아이템 11. equals를 재정의하려거든 hashCode도 재정의하라. (0) | 2023.03.09 |
[effective java] 아이템 10. equals는 일반 규약을 지켜 재정의하라. (0) | 2023.03.09 |