ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 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으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지리라 기대한다.
    • 이 기대를 만족시키려면 그 클래스와 모든 상위 클래스는 복잡하고, 강제할 수 없고, 허술하게 기술된 프로토콜을 지켜야만 한다. 이는 깨지기 쉽고, 위험하고, 모순적인 메커니즘이 탄생하게 된다.

     

    Object clone() 명세

    • clone() 메서드의 일반 규약은 허술하다. Object 명세에서 가져온 다음 설명을 보자.

     

    • 이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다.
    • 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음식은 참이다.

     

     

    배열은 clone기능을 제대로 사용하는 유일한 예다

    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 메서드 방식이 가지는 가장 깔끔한, 이 규칙의 합당한 예외다.

     

     

     

    참고 자료 


    이펙티브 자바

     

Designed by Tistory.