728x90
clone 재정의는 주의해서 진행하라
Cloneable
public interface Cloneable {
}
Cloneable
: 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스(기능 추가)이다.- 구현할 메서드가 존재하지 않으며,
Object.clone()
의 동작 방식만을 결정한다.
Object.clone()
public class Object {
// ...
@IntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
- Object는
clone()
메서드를 가지고 있지만,Cloneable
인터페이스를 구현하고 있지 않다. - 동작 방식
Cloneable
을 구현하지 않은 클래스에서clone
을 호출하면CloneNotSupportedException
을 던진다.Cloneable
을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 모든 필드들을 복사한 객체를 반환한다.
Cloneable 테스트
public class CloneTest {
public static class Clone implements Cloneable { // Cloneable 구현한 클래스
@Override
public Clone clone() {
try {
return (Clone) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
public static class NoClone { // Cloneable 구현 안한 클래스
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
@Test
@DisplayName("Cloneable 구현시 객체를 정상적으로 복제한다.")
void Cloneable_구현시_객체를_정상적으로_복제한다() {
// Given
Clone clone = new Clone();
// When
Clone anotherClone = clone.clone();
// Then
assertThat(anotherClone).isInstanceOf(Clone.class)
.isNotSameAs(clone);
}
@Test
@DisplayName("Cloneable을 구현하지 않고 clone 호출시 예외가 발생한다.")
void Cloneable을_구현하지_않고_clone_호출시_예외가_발생한다() {
// Given
NoClone noClone = new NoClone();
// When & Then
assertThatThrownBy(noClone::clone)
.isInstanceOf(CloneNotSupportedException.class);
}
}
protected
의Object.clone()
을public
메서드로 재정의해야 한다.- 재정의 하지 않은 경우
- 다른 패키지에 있다면
protected
메서드라 접근이 불가능하다. - 같은 패키지라면
CloneNotSupportedException
예외가 발생한다.
- 다른 패키지에 있다면
- 리플렉션을 사용하면 가능하지만 객체가 접근이 허용된
clone()
을 제공하지 않을 경우 실패한다.
- 재정의 하지 않은 경우
Cloneable가 인터페이스의 정의를 잘 살리지 못한 이유
- 일반적으로 인터페이스를 구현한다는 것은 해당 클래스가 그 인터페이스에 정의한 기능을 제공한다고 선언하는 것이다.
Cloneable
은 상위 클래스(Object
)에 정의된protected
메서드의 동작 방식을 변경한다.Cloneable
은 일반적인 인터페이스 정의를 살리지 못한 예제이다.
clone 메서드의 일반 규약
clone()
은 객체의 복사본을 생성해 반환한다.- 어떤 x에 대해 다음 식은 참이다.
x.clone() != x
x.clone().getClass() == x.getClass()
- 어떤 x에 대해 다음 식은 일반적으로 참이지만 반드시 만족해야 하는 것은 아니다.
x.clone.equals(x)
clone()
이 반환하는 객체가super.clone()
을 호출해 얻는다면,x.clone().getClass() == x.getClass()
- 관례상 반환된 객체와 원본 객체는 독립적이어야 한다.
clone()에서 super.clone()이 아닌 생성자
를 사용하여 인스턴스를 반환한다면?
super.clone()
는Object
타입의 인스턴스를 반환한다.- 일반적으로 캐스팅하여 사용한다.
- 생성자를 사용하여 인스턴스를 반환할 경우 Object가 아닌 해당 클래스의 인스턴스를 반환하게 된다.
- 만약 해당 클래스의 하위 클래스에서
super.clone
을 호출한다면 Object 타입이 아닌 인스턴스가 반환되어 캐스팅 예외가 발생한다.
class Parent implements Cloneable {
@Override
public Parent clone() {
return new Parent(); // super.clone()이 아닌 생성자를 호출하도록 구현함
}
}
class Child extends Parent {
private int childField;
public Child(int childField) {
this.childField = childField;
}
@Override
public Child clone() {
// ClassCastException 발생! Parent를 하위 클래스로 캐스팅 불가
Child cloned = (Child) super.clone();
cloned.childField = this.childField;
return cloned;
}
}
Child original = new Child(5);
Child cloned = original.clone(); // ClassCastException 발생
- 만약
clone
을 재정의한 클래스가final
이라면?- 하위 클래스가 없으니 캐스팅시 예외는 발생하지 않는다.
- 하지만
Cloneable
을 구현하여object.clone()
을 사용하는 것을 기대하는데, 생성자를 호출하여 객체를 생성하기 때문에 의도에 맞지 않다. - 생성자로 객체를 생성한다면,
clone()
을 구현하지 말고 의도에 맞게 복사 생성자로 사용하자.
생성자 vs clone()
- 공통점 : 둘 다 새로운 객체를 생성한다.
- 차이점
생성자
: 객체를 생성할 때 생성자에서 정의한 초기화 로직이 수행된다.clone()
: 메모리 내용을 그대로 복사하고 생성자를 따로 호출하지 않으므로 성능이 빠르다.
- 추천
- clone()보다는 생성자를 사용하자.
clone()
은 객체의 특성에 상관없이 각 필드를 그대로 복사하기 때문에 문제가 발생할 수 있다. (위험하다)
- clone()보다는 생성자를 사용하자.
불변 클래스 복제
불변 클래스는 굳이 clone 메서드를 제공하지 않는게 좋다.
- 불변 객체는 복제해도 원본과 똑같기 때문에 새로운 객체를 만들 필요가 없다.
- 단순히 기존 객체를 반환하면 된다.
공변 반환 타이핑
// 코드 13-1 가변 상태를 참조하지 않는 클래스용 clone 메서드 (79쪽)
@Override
public PhoneNumber clone() {
try {
return (PhoneNumber) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError(); // 일어날 수 없는 일이다.
}
}
- 재정의한 메서드의 반환 타입은 상위 클래스의 메서드가 반환하는 타입의 하위 타입일 수 있다.
Object가
아닌PhoneNumber
로 캐스팅한 객체를 반환할 수 있다.
CloneNotSupportedException
CloneNotSupportedException
은 검사 예외지만 사실은비검사 예외
였어야했다.Cloneable
을 구현했다면 발생하지 않을 예외이기 때문이다.
가변 클래스 복제
가변식을 해친다
// Stack의 복제 가능 버전 (80-81쪽)
public class Stack implements Cloneable {
private static final int DEFAULT_INITIAL_CAPACITY = 16;
// 가변 필드
private Object[] elements;
private int size = 0;
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;
}
public boolean isEmpty() {
return size ==0;
}
// 원소를 위한 공간을 적어도 하나 이상 확보한다.
private void ensureCapacity() {
if (elements.length == size)
elements = Arrays.copyOf(elements, 2 * size + 1);
}
}
elements
필드는 원본Stack
인스턴스와 똑같은 배열을 참조하므로 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해친다.
clone 메서드는 사실상 생성자와 같은 효과를 낸다
- . 즉,
clone
은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다.super.clone()
후 추가 작업을 수행할 수도 있다.- ex)
elements
배열의clone
을 재귀적으로 호출한다.
// 코드 13-2 가변 상태를 참조하는 클래스용 clone 메서드
@Override
public Stack clone() {
try {
Stack result = (Stack) super.clone();
result.elements = elements.clone(); // 배열 복사 필요 (원본과 다른 참조를 가지기 위함)
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public static void main(String[] args) {
Stack stack = new Stack();
for (String arg : args)
stack.push(arg);
Stack copy = stack.clone();
while (!stack.isEmpty())
System.out.print(stack.pop() + " ");
System.out.println();
while (!copy.isEmpty())
System.out.print(copy.pop() + " ");
}
elements
필드가final
이라면 새로운 값을 할당할 수 없으므로 불변식을 보장할 수 없다.Cloneable
아키텍처는 가변 객체를 참조하는 필드는final
로 선언하라는 일반 용법과 충돌한다. (참조 변경 방지)
배열의 clone()
- 배열을 복제할 때는 배열의
clone
메서드를 사용하라고 권장한다. (clone 기능을 사용하는 유일한 예시) - 원본 배열의 내용을 정확히 복사하는데 간단하기 때문이다.
깊은 복사를 지원해야 할 때도 있다.
- 참조 타입의 배열의 경우 복제본은 자신만의 버킷 배열을 가져야 한다.
- 먼저
super.clone
을 호출하여 얻은 객체의 모든 필드를 초기 상태로 설정한다. - 원본 객체의 상태를 다시 생성하는 고수준 메서드들을 호출한다.
HashTable
예시라면buckets
필드를 새로운 버킷 배열로 초기화한다음 원본 테이블에 담긴 모든 키-값 쌍 각각에 대해 복제본 테이블의put
메서드를 호출해 내용을 같게 한다.- 고수준 메서드는 저수준에서 바로 처리할때보다는 느리다.
- 필드 단위 객체 복사를 우회하므로
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());
}
}
@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();
}
}
}
- 깊은 복사 - 반복 방식 예시
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() {
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;
}
}
기타 주의사항
하위 클래스에서 재정의될 수 있는 메서드는 호출해서는 안된다.
clone
이 하위 클래스에서 재정의한 메서드를 호출하면 원본과 복제본의 상태가 달라질 가능성이 크다. (기대했던 방식대로clone
이 안될수도 있다.)HashTable
의 경우put
메서드는final
이거나private
(public
의 도우미 메서드)여야 한다.
public인 clone 메서드에서는 throws 절을 없애야 한다.
- 검사 예외(
Checked Exception
) 던지지 않아야 메서드를 사용하기 편하다.
상속용 클래스는 Cloneable을 구현해서는 안된다.
- 하위 클래스에서 잘못 사용하거나 상속 계층에서 일관성을 유지하면서
clone()
을 사용하는 것이 어렵기 때문이다. - 하위 클래스에서 재정의하지 못하도록 막자.
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
clone()
메서드를 오버라이드하지 않는다면, 구현 여부를 하위 클래스에서 선택할 수 있다.
Cloneable을 구현한 스레드 안전 클래스
Cloneable
을 구현한 스레드 안전 클래스를 작성할 때는clone
메서드를 동기화해줘야 한다.
public class ThreadSafeClass implements Cloneable{
private List<Integer> numbers = new ArrayList<>();
public synchronized void add(Integer number) {
numbers.add(number);
}
@Override
protected synchronized Object clone() {
try {
ThreadSafeClass clone = (ThreadSafeClass) super.clone();
clone.numbers = new ArrayList<>(numbers);
return clone;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
}
Cloneable 요약
Cloneable
을 구현하는 모든 클래스는clone
을 재정의해야 한다.- 접근 제어자는
public
으로, 반환 타입은 클래스 자신으로 변경한다.- 반환 타입을 클래스 자신으로 하면 호출하는 곳에서 캐스팅할 필요가 없어 편리하다.
super.clone
을 호출한 후 필요한 필드를 적절히 수정한다.- 즉, 모든 가변 필드를 복사하고, 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 한다.
- 불변 객체의 경우 일련번호와 고유 id 등을 수정해야한다.
- 접근 제어자는
객체 복사 방식 : 복사 생성자와 복사 팩터리 (권장) ⭐️
Cloneable
을 구현하는 것보다 더 나은 방식이다.
복사 생성자
- 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다.
public class Cat {
private final int age;
private final String name;
public Cat(final int age, final String name) {
this.age = age;
this.name = name;
}
// 복사 생성자
public Cat(final Cat cat) {
this.age = cat.age;
this.name = cat.name;
}
}
복사 팩터리
- 복사 팩터리는 복사 생성자를 모방한 정적 팩터리다.
- 생성자를 사용하며
final
필드 용법과도 충돌하지 않는다. - 불필요한 검사 예외를 던지지 않고 형변환도 필요하지 않다.
public class Cat {
private final int age;
private final String name;
public Cat(final int age, final String name) {
this.age = age;
this.name = name;
}
// 복사 팩터리
public static Cat newInstance(final Cat cat) {
return new Cat(cat.age, cat.name);
}
}
- 복사 생성자와 복사 팩터리는 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.
- 모든 범용 컬렉션 구현체는
Collection
이나Map
타입을 받는 생성자를 제공한다. - 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 선택할 수 있다.
- 모든 범용 컬렉션 구현체는
interface Animal {
String makeSound();
}
class Dog implements Animal {
@Override
public String makeSound() {
return "Woof!";
}
}
class Cat implements Animal {
@Override
public String makeSound() {
return "Meow!";
}
}
class AnimalShelter {
private List<Animal> animals;
// 기본 생성자
public AnimalShelter() {
this.animals = new ArrayList<>();
}
// 복사 생성자
public AnimalShelter(Collection<? extends Animal> animals) { // Animal의 하위 타입인 Dog, Cat 인자로 받을 수 있음
this.animals = new ArrayList<>(animals);
}
// 복사 팩터리 메서드
public static AnimalShelter newInstance(Collection<? extends Animal> animals) { // Animal의 하위 타입인 Dog, Cat 인자로 받을 수 있음
return new AnimalShelter(animals);
}
public void addAnimal(Animal animal) {
animals.add(animal);
}
public void makeAllSounds() {
for (Animal animal : animals) {
System.out.println(animal.makeSound());
}
}
}
public class Main {
public static void main(String[] args) {
AnimalShelter originalShelter = new AnimalShelter();
originalShelter.addAnimal(new Dog());
originalShelter.addAnimal(new Cat());
// 복사 생성자를 사용한 복제
AnimalShelter copiedShelter1 = new AnimalShelter(originalShelter.getAnimals());
// 복사 팩터리를 사용한 복제
AnimalShelter copiedShelter2 = AnimalShelter.newInstance(originalShelter.getAnimals());
List<Animal> animalList = new LinkedList<>();
animalList.add(new Dog());
animalList.add(new Cat());
AnimalShelter linkedListShelter = new AnimalShelter(animalList);
System.out.println("Original Shelter:");
originalShelter.makeAllSounds(); // Woof! Meow!
System.out.println("\nCopied Shelter 1:");
copiedShelter1.makeAllSounds(); // Woof! Meow!
}
}
결론
- 새로운 인터페이스를 만들 때는 절대
Cloneable
을 확장해서는 안되며, 새로운 클래스도 이를 구현해서는 안된다.Cloneable
은 설계에 문제가 있는 인터페이스이고,clone()
메서드는 복잡하고 위험하다.final
클래스라면Cloneable
을 구현해도 위험이 크지는 않지만 성능 최적화 관점에서 검토한 후 드물게 허용하자.
- 기본 원칙은 "복제 기능은 생성자와 팩터리를 이용하는 것이 최고"라는 것이다.
- 배열은
clone
메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외이다.
728x90
'Java > effective java' 카테고리의 다른 글
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2025.02.05 |
---|---|
[Effective Java] Item 14. Comparable을 구현할지 고려하자. (1) | 2025.01.20 |
[Effective Java] Item 12. toString을 항상 재정의하라. (0) | 2025.01.20 |
[Effective Java] Item 11. equals를 재정의하려거든 hashCode도 재정의하라. (0) | 2025.01.20 |
[Effective Java] Item 10. equals는 일반 규약을 지켜 재정의하라 (1) | 2025.01.13 |