JAVA/Effective Java

[아이템 13] clone 재정의는 주의해서 진행하라

꾸준함. 2024. 1. 29. 18:39

Cloneable 인터페이스

Cloneable은 Object 클래스에 정의된 protected 메서드인 clone의 동작 방식을 결정하는 인터페이스입니다.

Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출할 경우 해당 객체의 필드들을 하나하나 복사한 객체를 반환하고 Clonable을 구현하지 않은 클래스에서 clone()을 호출할 경우 CloneNotSupportedException을 던집니다.

일반적으로 인터페이스를 구현한다는 것은 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위인데 Cloneable의 경우 상위 클래스인 Object에 정의된 protected 메서드의 동작 방식을 변경한 것이기 때문에 인터페이스를 잘 못 사용한 예시라고 볼 수 있습니다.

정리하자면 Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도로 설계되었지만 정작 clone 메서드는 Object 객체에 protected로 정의되어 있고 Cloneable 인터페이스에는 아무것도 정의되어 있지 않기 때문에 의도한 목적을 제대로 이루지 못했다고 볼 수 있습니다.

그럼에도 불구하고 Cloneable이 널리 사용되고 있다고 하니 clone 메서드의 규약, 불변 객체와 가변 객체에서 clone 구현하는 방법, 그리고 clone 대신 권장하는 방법에 대해 정리해 보겠습니다. 

 

Clonable 인터페이스
Object 객체에 정의된 clone 메서드

 

clone 메서드의 규약

clone 메서드의 규약은 아래와 같습니다.

  • x.clone() != x는 반드시 참
  • x.clone().getClass() == x.getClass() 반드시 참
  • x.clone().equals(x)는 참일 수도 있고 거짓일 수도 있음

 

1. x.clone() != x는 반드시 참

 

객체를 복사를 했다는 것은 원본과는 다른 인스턴스임을 의미합니다.

따라서 두 인스턴스의 hashCode는 다를 것입니다.

 

2. x.clone().getClass() == x.getClass() 반드시 참

 

객체를 복사했으므로 두 인스턴스는 반드시 동일 클래스입니다.

 

3. x.clone().equals(x)는 참일 수도 있고 아닐 수도 있음

 

객체를 복사하더라도 두 인스턴스는 다른 인스턴스이기 때문에 식별자는 다르게 가져갈 수 있습니다.

이럴 경우 필드가 전부 동일한 것은 아니므로 equals() 결과가 거짓일 것입니다.

 

불변 객체 clone 구현하는 방법

불변 객체의 경우 아래 조건만 충족하면 됩니다.

  • Cloneable 인터페이스를 구현하고
  • clone() 메서드를 재정의하되 반드시 super.clone()을 사용
    • 어떤 인터페이스를 상속받아서 오버라이딩할 때 접근지시자는 항상 상위 클래스의 접근 지시자와 같거나 넓은 지시자로 정의
    • Object 클래스처럼 protected로 정의할 경우 하위 클래스에서만 쓸 수 있는 메서드이기 때문에 실질적으로 쓸모없음
    • 해당 클래스를 복사하려는 클라이언트 코드는 대부분 클래스의 하위 클래스가 아닐 확률이 높기 때문에 public 접근 지시자로 구현 필요
    • 위와 같은 이유 때문에 Cloneable 인터페이스에 clone() 메서드가 정의되어 있지 않은 것이 이상함

 

앞선 아이템에서 다루었던 PhoneNumber 클래스를 토대로 작성한 예시 코드는 아래와 같습니다.

 

 

 

이처럼 불변 객체의 경우 단순히 super.clone() 메서드를 호출하는 것만으로 규약을 전부 지킬 수 있는 것을 확인할 수 있습니다.

번외로 CloneNotSupportedException에 대해 첨언하자면 자바에서 해당 예외를 CheckedException이 아닌 UnchekcedException으로 구현했어야 한다고 생각합니다.

그 이유는 CloneNotSupportedException 예외가 발생한다 하더라도 개발자가 어떻게 대응을 할 수 있는 방도가 없기 때문입니다.

 

가변 객체의 clone 구현하는 방법

불변 객체와 달리 가변 객체의 clone 구현은 여러 까다로운 조건을 충족해야 합니다.

  • super.clone() 메서드를 호출하여 객체를 생성한 뒤 필드의 성격에 따라 깊은 복사를 실행할 필요 있음
    • 이 때문에 경우에 따라 배열 필드는 final로 선언하지 못할 수도 있음

 

  • 오버라이딩할 수 있는 메서드는 하위 클래스가 어떻게 정의할지 예측이 불가하므로 참조하지 않도록 조심
    • 이 때문에 상속용 클래스는 Cloneable을 구현하지 않는 것을 권장

 

  • Cloneable을 구현한 thread-safe 클래스를 작성할 때는 동기화 필요

 

1. super.clone() 메서드를 호출하여 객체를 생성한 뒤 필드의 성격에 따라 깊은 복사를 실행할 필요 있음

 

HashTable 클래스가 직접 정의한 Entry 객체 배열 필드를 가진다고 가정하고 clone() 메서드를 호출할 경우 아래 조건을 충족해야 합니다.

  • 원복 인스턴스와 복사한 인스턴스는 다른 인스턴스
  • 원본 인스턴스의 Entry 객체 배열과 복사한 인스턴스가 바라보는 Entry 배열은 다른 배열
    • original.buckets[0] != copy.buckets[0]

 

따라서 clone() 메서드의 Pseudo 코드는 아래와 같습니다.

  • 원본 인스턴스 super.clone()
  • 복제 인스턴스의 buckets 배열 새로 할당 (이 과정 때문에 buckets 필드에 final 키워드 못 붙임)
  • buckets 내 각각의 Entry 깊은 복사

 

위 내용을 정리한 코드는 아래와 같습니다.

 

 

 

위 코드는 앞서 언급한 조건을 모두 충족시키는 것을 확인할 수 있습니다.

만약 buckets 필드를 단순히 clone() 메서드로 얕은 복사를 했을 경우 두 인스턴스 모두 같은 entry를 바라보는 오류가 발생하는 것을 확인할 수 있습니다.

 

 

2. 상속용 클래스는 Cloneable을 구현하지 않는 것을 권장

 

제목 그대로 오버라이딩할 수 있는 메서드의 경우 하위 클래스가 어떻게 정의할지 예측이 불가하기 때문에 clone() 메서드에 포함시키지 않는 것을 권장합니다.

또한 상속용 클래스에서 Cloneable을 구현할 경우 하위 클래스를 작성하는 개발자에게 clone 메서드 규약을 모두 따르며 구현하는 것을 강요하게 됩니다.

따라서 개발자의 부담을 덜기 위해 clone() 메서드에 final 키워드를 붙여 Cloneable 구현을 막거나 개발자의 부담을 덜기 위해 기본 clone() 구현체를 제공해 Cloneable 구현 여부를 서브 클래스를 구현하는 개발자가 선택할 수 있도록 하는 것을 권장합니다.

 

 

 

3. Cloneable을 구현한 thread-safe 클래스를 작성할 때는 동기화 필요

 

멀티 쓰레드 환경에서는 clone 메서드를 thread-safe 하게 구현하기 위해 synchronized 키워드를 붙이거나 ReentrantLock을 사용하는 것을 권장합니다.

 

clone 구현 시 주의사항

clone 구현 시 반드시 super.clone()을 호출해야 합니다.

만약 clone() 메서드 내에서 super.clone()을 호출하지 않고 단순히 생성자를 통해 복사를 시도할 경우 상위 타입 클래스를 하위 타입 클래스로 형변환하지 못해 런타임 내 캐스팅 에러가 발생할 수 있습니다.

 

 

아래처럼 super.clone()을 호출하는 방식으로 수정하면 정상 작동하는 것을 확인할 수 있습니다.

 

 

 

 

clone 대신 권장하는 방법

clone 대신 복사를 위한 생성자 혹은 복사를 위한 정적 팩토리 메서드를 정의하는 경우 취득할 수 있는 장점은 아래와 같습니다.

  • 기능이 명확하여 파악하기 쉬움
  • final 기변 필드 사용 가능
  • 파라미터로 상위 타입을 받아 하위 타입으로 변환 가능

 

1. 기능이 명확하여 파악하기 쉬움

 

복사 생성자와 복사 팩토리 메서드는 객체의 복제를 담당하는 명확한 메커니즘을 제공하는 반면 Cloneable 인터페이스의 clone 메서드는 복제의 세부 동작을 명시적으로 정의하지 않기 때문에 이해하기 쉽지 않습니다.

실제로 Object 클래스의 clone() 메서드의 내부 동작을 파악하려고 해도 native 메서드이기 때문에 정확히 어떻게 동작하는지는 파악하기 어렵습니다.

 

2. final 기변 필드 사용 가능

 

복사 생성자 혹은 복사 정적 팩토리 메서드는 새로운 객체를 생성하고 필요에 따라 final 필드를 초기화할 수 있습니다.

반면 clone 메서드의 경우 복제 대상 객체의 필드를 직접 접근하기 때문에 경우에 따라 final 기변 필드 사용이 제한될 수 있습니다.

 

3. 파라미터로 상위 타입을 받아 하위 타입으로 변환 가능

 

Cloneable 인터페이스의 clone 메서드는 동일 클래스에 대한 복제만 허용합니다.

반면 복사 생성자 혹은 복사 정적 팩토리 메서드의 경우 상위 타입을 받아 하위 타입으로 변환이 가능합니다.

실제로 TreeSet의 생성자를 보면 Collection을 매개변수로 받는 것을 확인할 수 있는데 이 때문에 HashSet을 TreeSet으로 형 변환하면서 복제가 가능합니다.

 

 

정리

불변 객체의 경우 Cloneable 인터페이스를 구현해도 위험이 적지만, 가변 객체에 대해서는 Cloneable 인터페이스를 구현할 때 지켜야 하는 규약과 주의사항이 많아지기 때문에 복사 생성자나 복사 정적 팩토리 메서드를 사용하여 복사하는 것이 권장됩니다.

 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 1부 - 백기선 강사님

 

 

반응형