[Effective Java] Item13. clone 재정의는 주의해서 진행하라

2022년 03월 15일

TOC

이 글은 Effective Java 3/E의 내용을 요약한 글입니다. 자세한 내용은 책을 참고하시기 바랍니다.

자바에서는 클래스를 복제해도 되는 것을 명시하는 용도인 Cloneable 인터페이스를 제공하고 있습니다. Cloneable 인터페이스를 구현하여 clone()메서드를 재정의한다면 객체의 필드들을 하나씩 복사하여 객체를 반환할 수 있습니다.

하지만 이러한 clone메서드는 Cloneable이 아닌 Object에 선언되어 있어 해당 객체가 clone()을 제공한다는 보장이 없다는 단점이 있습니다.

clone() 메서드의 일반 규약

객체의 복사본을 만들어서 반환한다. 그리고 다음을 따른다. x.clone() != x 의 조건은 참이어야 한다,

x.clone().getClass() == x.getClass() 위의 조건은 참이겠지만 반드시 그래야 하는 것은 아니다.

x.clone().equals(x) 위의 코드를 실행한 결과도 true가 되겠지만 반드시 그래야하는것은 아니다.

위의 코드를 실행한 결과도 true가 되겠지만 반드시 그래야 하는 것은 아니다. 객체를 복사하면 보통 같은 클래스의 새로운 객체가 만들어지는데, 내부 자료 구조까지 복사해야 될 수도 있다. 어떤 생성자도 호출되지 않는다.

clone의 특징

  • clone은 메서드가 super.clone이 아닌, 생성자를 호출해 얻은 인스턴스를 반환해도 컴파일러는 불평하지 않는 점에서 생성자 연쇄와 비슷하다.
  • clone을 재정의한 클래스가 final이면 걱정할 하위 클래스가 없습니다. 만약 그렇지 않는다면 클래스의 하위 클래스에서 super.clone을 호출하면 잘못된 객체가 만들어져 하위 클래스의 clone이 잘 동작하지 않습니다.
  • clone 메서드는 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장하는 생성자와 같은 효과를 낸다.

재정의 방법

모든 필드가 기본 타임이거나 불변 객체를 참조하는 경우

  1. super.clone을 호출한다.
  2. 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 복사 완료

필드가 가변 타입일 경우 방법1

  1. super.clone을 호출한다.
  2. 필드에 가변타입이 있다면 내부적으로 clone을 재귀적으로 호출하여 복사를 해줘야한다. (ex. 리스트)

    1. 필드의 가변타입을 그대로 복사할 경우 문제가 생기는 경우도 존재한다.(링크드 리스트의 node) 그런 경우는 깊은 복사를 지원하도록 한다. 이러한 방법은 재귀를 사용하면 리스트의 원소 수만큼 스택 프레임을 소비하여 스택 오버플로우를 일으킬 위험이 있어 반복자를 써서 순회하는 것이 좋다.

필드가 가변 타입일 경우 방법2

  1. super.clone을 호출한다.
  2. 객체의 모든 필드를 초기 상태로 설정한다.
  3. 원본 객체의 상태를 다시 생성하는 고수준 메서드를 호출한다. (HashTable의 예로 보면, Table(map)을 초기화 한 후 put메서드를 통해 값 추가)

이와 같이 고수준의 api를 활용하면 간단하고 이쁜 코드를 얻을 수 있지만 저수준에서 바로 처리를 할 떄보다는 느리다. 또한 Cloneable의 기초가 되는 필드 단위의 복사를 하지 않아 Cloneable 아키텍쳐와는 어울리지 않는 방식이다.

clone 주의사항

  • clone에서도 생성자와 같이 재정의 될 수 있는 메서드를 호출하지 말아야한다.

    • 하위 클래스에서 재정의한 메서드를 호출하게 된다면 복사본의 상태가 달라질 수 있다.
  • Object의 clone메서드는 CloneNotSupportedException을 던지지만 clone을 재정의할 때는 throws절을 없애야 한다. (public일 경우)

    • 검사 예외를 던지지 않아야 그 메서드를 사용하기 편리하기 때문이다.
  • 상속용 클래스는 Cloneable을 구현해서는 안된다. 만약 사용한다면 아래와 같이 사용한다.

    • 제대로 작동하는 clone()을 구현해 protected로 두고 CloneNotSupportedException을 던질 수 있다고 선언하게 한다.
    • clone을 동작하지 않게 구현하고 하위 클래스에서 재정의하지 못하게 한다.
  • Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone메서드 역시 적절히 동기화해줘야 한다.

    • Object의 clone 메서드는 동기화에 신경을 쓰지 않아 super.clone 호출 외에 다른 할 일이 없더라도 clone을 재정의하고 동기화해줘야 한다.

반전

Cloneable을 이미 구현한 클래스를 확장할 때는 어쩔 수 없지만 그렇지 않은 경우 복사 생성자복사 팩터리를 사용하는 것이 더 낫다.

이들을 이용하면 클라이언트는 원본의 구현 타입에 얽매이지 않고 복제본의 타입을 직접 지킬 수 있으며 장점은 아래와 같다.

  • 언어 모순적이고 위험한 객체 생성 메커니즘을 사용하지 않는다
  • 모순적이고 엉성하게 문서화된 규약에 기대지 않는다.
  • 정상적인 final 필드 용법과도 충돌하지 않는다
  • 불필요한 검사 예외를 던지지 않는다
  • 형변환이 필요하지 않다.
  • 해당 클래스가 구현한 인터페이스 타입의 인스턴스를 인수로 받을 수 있다.

핵심 정리

Cloneable이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대로 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다. final 클래스라면 Cloneable을 구현해도 위험이 크지 않지만, 성능 최적화 관점에서 검토한 후 별다른 문제가 없을 때만 드물게 허용해야 한다. 기본 원칙은 "복제 기능은 생성자와 팩터리를 이용하는 게 최고"라는 것이다. 단, 배욜만은 clone 메서드 방식이 가장 깔끔한, 이 규칙의 합당한 예외라 할 수 있다.

Buy me a coffeeBuy me a coffee
Written by

@Seongwon

기술공유를 통해 새로운 가치 창조을 추구하는 백엔드 개발자 오성원입니다.
©SeongwonOh