-
clone 재정의는 주의해서 진행하라JAVA/Effective java 2021. 1. 20. 18:09
- Cloneable은 복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스지만, 아쉽게도 의도한 목적을 제대로 이루지 못했다.
- 가장 큰 문제는 clone 메서드가 선언된 곳이 Cloneable이 아닌 Object이고 그마저도 protected라는 데 있다.
- 따라서 Cloneable을 구현하는 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없다.
그렇다면 메서드 하나 없는 Cloneable 인터페이스는 무슨 일을 할까?
- 이 인터페이스는 Object의 protected 메서드인 clone의 동작 방식을 결정한다.
- Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
- 인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 행위다.
- 그런데 Cloneable의 경우 상위 클래스에 정의되니 protected 메서드의 동작 방식을 변경한 것이다.
- 실무에서 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대한다.
- 이 기대를 만족시키려면 그 클래스와 모든 상위 클래스는 복잡하고, 강제할 수 없고, 허술하게 기술된 프로토콜을 지켜야만 한다. 이는 깨지기 쉽고, 위험하고, 모순적인 메커니즘이 탄생하게 된다.
- clone() 메서드의 일반 규약은 허술하다. Object 명세에서 가져온 다음 설명을 보자.
- 이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다.
- 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음식은 참이다.
x.clone() != x
또한 다음 식도 참이다.
x.clone().getClass() == x.getClass()
하지만 이상의 요구를 반드시 만족해야 하는 것은 아니다.
한편 다음 식도 일반적으로 참이지만, 역시 필수는 아니다.
x.clone().equals(x)
관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와 (Object를 제외한)
모든ㄷ 상위 클래스가 이 관례를 따른다면 다음식은 참이다.
x.clone().getClass() == x.getClass()
관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.- 만약 클래스의 하위 클래스에서 super.clone을 호출한다면 잘못된 클래스의 객체가 만들어져, 결국 하위 클래스의 clone 메서드가 제대로 동작하지 않게 된다.
- 이 클래스의 clone 메서드가 단순히 super.clone의 결과를 그대로 반환한다면 elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다.
- 원본이나 복제본 중 하나를 수정하면 다른 하나도 수정되어 불변식을 해치게 된다.
- clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야 한다.
- 가장 쉬운 방법은 elements 배열의 clone을 재귀적으로 호출해주는 것이다.
- 한편 elements 필드가 final이었다면 앞선 방식은 동작하지 않는다.
- final에는 새로운 값을 할당하지 못하기 때문인데, 이는 근본적인 문제로,
- 직렬화와 마찬가지로 Cloneable 아키텍처는 '가변 객체를 참조하는 필드는 final로 선언하라'는 일반 용법과 충돌한다.
주의할 점
1. 만약 상속해서 쓰기 위한 클래스 설계 방식 두 가지 중 어느 쪽에서든 상속용 클래스는 Cloneable을 구현해서는 안 된다.
2. Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 동기화해줘야 한다. Object의 clone 메서드는 동기화를 신경 쓰지 않았다. super.clone 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화 해주자.
요약
- Cloneable을 구현한 모든 클래스는 clone을 재정의 해야 한다. 이때 접근 제한자는 public으로, 반환 타입은 클래스 자신으로 변경한다.
- 그렇게 하면 이 메서드는 가장 먼저 super.clone을 호출한 후 필요한 필드를 전부 적절히 수정한다. 즉 그 객체의 내부 '깊은 구조'에 숨어 있는 모든 가변 객체를 복사하고, 복제본이 가진 객체 참조 모두가 복사된 객체들을 가리키게 함을 뜻한다.
- 이 모든 작업은 꼭 필요하지는 않으며 이처럼 복잡한 경우들은 드물다.
- Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다.
- 그렇지 않은 상황에서는 복사 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.
-
(복사 생성자란 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다)
핵심 정리
- 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안된다.
- final 클래스라면 Cloneable을 구현해도 위험은 크지 않지만, 성능 최적화 관점에서 검토한 후 별 문제가 없을 경우만 드물게 허용하자.
- 기본 원칙은 '복제 기능은 생성자와 팩터리를 이용하는 게 최고'라는 것이다.
- 단 배열만은 clone 메서드 방식이 가지는 가장 깔끔한, 이 규칙의 합당한 예외다.
참고 자료
'JAVA > Effective java' 카테고리의 다른 글
클래스와 멤버의 접근 권한을 최소화하라 (0) 2021.01.23 Comparable을 구현할지 고려하라 (0) 2021.01.21 toString을 항상 재정의하라 (0) 2021.01.19 equals를 재정의하려거든 hashCode도 재정의하라 (0) 2021.01.14 equals는 일반 규약을 지켜 재정의 하라 (0) 2021.01.11