[ 이펙티브 자바 ] 아이템 13 clone 재정의는 주의해서 진행하라

Dayeon myeong·2022년 3월 6일
0

이펙티브자바

목록 보기
3/15

Cloneable 인터페이스의 역할

클래스에서 clone을 재정의할 때에는 해당 클래스에 Cloneable 인터페이스를 구현하여야 한다. 실제로 clone 메서드는 Cloneable 인터페이스가 아닌 Object에 선언되어있다. 즉, Cloneable 인터페이스는 아무런 메서드 하나 없는 빈 인터페이스다.

이러한 Cloneable 인터페이스의 역할은 Object의 clone 메서드의 동작 방식을 결정할 때 사용한다.
Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환한다.
만약 Cloneable 을 구현하지 않은 특정 인스턴스에서 clone 메서드를 호출하면 CloneNotSupportedException을 던진다.

Object clone 메서드의 일반 규약

  • x.clone() != x 는 참이다. 원본 객체와 복사 객체는 서로 다른 객체이다.
  • x.clone().getClass() == x.getClass() 는 참이다. 하지만 반드시 만족해야 하는 것은 아니다.
  • x.clone().equals(x) 는 참이지만 필수는 아니다.
  • x.clone().getClass() == x.getClass(), super.clone()을 호출해 얻은 객체를 clone 메소드가 반환한다면, 이 식은 참이다. 관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.

getClass는 로딩된 클래스를 가져온다. 인스턴스가 달라도 로딩된 클래스 타입을 가져옴.

Object.clone 메서드는 똑같은 클래스 타입으로 새로운 객체를 만드는 것과 같다고 보면 될 것같다. 마치 new 생성자로 새로운 객체를 만드는 것이라 heap 영역에 새로운 복사 객체가 생성된다. 이 때 각 멤버변수를 '=' 을 이용해서 복사한다고 생각하면 쉽게 이해할 수 있을 것 같다.

Cloneable 인터페이스 재정의 시 super.clone() 연쇄 호출

class A implements Cloneable {
	
    @Override
    public B clone() {
    	try {
        	return new A();
        } catch(CloneNotSupportedEception e) {
        	throw new AssertionError();
        }
    }
}

class B implements ㅁ {

	...
    
    @Override
    public B clone() {
    	try {
        	return super.clone();//A의 clone 호출하여 A 객체 생성하는 문제
        } catch(CloneNotSupportedEception e) {
        	throw new AssertionError();
        }
    }
}

만약,clone 메소드가 super.clone 이 아닌 생성자를 호출해 얻은 인스턴스를 반환 하더라도 컴파일시에 문제가 되지는않는다.

하지만, 해당 클래스의 하위 클래스에서 super.clone을 호출한다면 하위 클래스 타입 객체를 반환하지 않고 상위 클래스 타입 객체를 반환하여 문제가 생길 수 있다.

예를들어 위 코드에서 하위 클래스 B의 clone은 B타입 객체를 반환해야 한다. 그런데 A의 clone이 자신의 생성자, 즉 new A(...)로 생성한 객체를 반환한다면 B의 clone도 A의 clone을 반환할 수 밖에 없다.

따라서 super.clone()을 연쇄적으로 호출하도록 구현해두면
B.clone -> A.clone -> Object.clone 으로 clone이 처음 호출된 클래스의 객체가 만들어진다.

필드가 기본타입이거나 불변 객체인 경우

@Override
public PhoneNumber clone() {
	try {
    	return (PhoneNumber) super.clone();
    } catch (CloneNotSupportedException e) {
    	throw new AssertionError();//일어날 수 없는 일이다.
    }
}

clone 메서드를 통해 클래스에 정의된 모든 필드도 원본 필드와 똑같은 값을 갖는다.이 때 각 필드를 '=' 을 이용해서 복사한다고 생각하면 쉽게 이해할 수 있을 것 같다. 만약 모든 필드가 기본타입이거나 불변 객체인 경우라면 더이상 손볼 것도 없다.
하지만 필드가 가변 객체인 경우 문제가 된다.

필드가 가변 객체인 경우 : 재귀적인 clone 호출

public class Stack implements Cloneable {
    private Object[] elements; // 가변 객체
    private int size = 0;
    private static final int DEFAULT_INITIAL_CAPACITY = 16;

    public Stack() {
        System.out.println("---------------");
        System.out.println("Stack constructor");
        this.elements = new Foo[DEFAULT_INITIAL_CAPACITY];
    }

   ...

    @Override
    public Stack clone() {
        try {
            return (Stack) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

만약 위와 같은 Stack 클래스에서 가변 객체 elemets 배열이 있다.
단순히 clone 메서드가 super.clone()을 호출한다면 elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다. 결국 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정된다.shallow copy라고 본다.

copyElements = elements;//shallow copy

만약 아래와 같이 elemetns에 대한 clone을 사용하여 해결 가능하다. 배열의 clone은 원본 배열과 똑같은 배열을 반환한다. 따라서 배열을 복제할 때는 배열의 Clone 메서드를 사용하라고 권장한다.

    @Override
    public Stack clone() {
        try {
            Stack stack = (Stack) super.clone();
            stack.elements = this.elements.clone();
            return stack;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

하지만 이 코드도 단점이 존재한다.
기존 객체와 복사된 객체의 객체 배열은 서로 다른 주소를 가지고 있어도 배열이 갖고있는 원소 하나하나 즉 객체원소 하나하나는 같은 객체라는 것이다.

그렇기 때문에 int와 같은 기본형에 대한 배열의 경우에는 위와 같이 배열.clone()을 써도 되지만,
클래스 타입의 배열은 배열.clone()을 해도 위험하다. 결국 원본 객체 원소나 복제본 객체 원소 중 하나를 수정하면 다른 하나도 수정된다.따라서 deepCopy 방식으로 배열의 원소하나하나를 복사해야 한다.

또한 elements 필드가 final이었다면 위 코드는 작동하지 않는다. final 필드는 한번 생성, 초기화한 이후에 새로운 값을 할당 할 수 없기 때문이다.

필드가 가변 객체인 경우 : deepCopy를 할 때 반복적으로 복사하기

  @Override
    public Stack clone() {
        try {
            Stack stack = (Stack) super.clone();
            stack.elements = new Object[elements.length];
            for (int i = 0; i< elements.length; i++) {
            	stack.elements[i] = new Object();
            }
            return stack;
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }

주의사항

Cloneable를 구현한 thread safe 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화해줘야 한다. Object의 clone 메서드는 동기화를 신경 쓰지 않았다. 그러니 clone 을 재정의하고 동기화해줘야 한다.

정리

Cloneable을 구현하는 모든 클래스는 clone을 재정의해야한다. 접근 제어자는 public으로, 반환 타입은 클래스 자신으로 변경한다. 이 메소드는 super.clone()을 호출한하 필요한 필드를 전부 적절히 수정한다. 적절히 수정한다는 말은, 가변 객체 필드를 shallow copy 복사해버려 원본 클래스가 가진 가변 객체 필드와 복사된 클래스가 가진 가변 객체가 같으면 안된다는 것이다.

복사 생성자와 복사팩토리

public Yum(Yum yum) {...};

public static Yum newInstance(Yum yum) {...};

복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식이 있다.

  • clone 메소드 같은 생성자를 쓰지않는 객체 생성 메커니즘을 사용하지 않는다.
  • 엉성하게 문서화된 규약에 기대지 않는다.
  • final 필드 용법과도 충돌하지 않는다.
  • 불필요한 검사 예외나 형변환이 필요치않다.
  • 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.

참고

이펙티브자바

자바봄 블로그
https://javabom.tistory.com/15?category=833277

https://lannstark.tistory.com/100

profile
부족함을 당당히 마주하는 용기

0개의 댓글