본문 바로가기
프로그래밍 놀이터/디자인 패턴, 리펙토링

[Design Pattern/Java] clone 메소드는 신중하게 오버라이드 하자.

by 돼지왕 왕돼지 2012. 3. 12.
반응형


안녕하세요 돼지왕 왕돼지입니다.

오늘은 "clone 메소드는 신중하게 오버라이드 하자" 라는 주제로 이야기하고자 합니다.

이 글은 "Effective Java" 를 정리한 내용입니다.



- Cloneable 인터페이스는 복제를 허용하는 객체라는 것을 알리는 목적으로 사용하는 믹스인 인터페이스( Mixin interface ) 이다.


Cloneable 이놈은 뭐하는 놈이야?

- Clonable interface 는 아무런 추상 메소드도 가지고 있지 않다.

- Object 클래스의 protected 메소드인 clone을 사용할 것인지의 여부를 결정한다. 어떤 클래스에서 Cloneable 인터페이스 implement 하고, clone 메소드를 호출하면, 그 클래스 객체의 복제본을 만들어 반환한다. 이 때 복제 객체는 원본 객체와 같은 필드를 가지며, 필드의 값도 복사된다.

- Cloneable interface 를 implement 하지 않고, clone 을 호출하면 CloneNotSupportedException 이 발생한다. ( 정상적인 인터페이스 사용 방법과 다르다. ), 즉 이 녀석은 슈퍼 클래스의 protected clone 메소드 동작 여부를 결정한다.

- clone 은 생성자를 호출하지 않고 객체를 생성, 복제한다.






clone 도 그럼 보편적 계약이 있어?

- clone 의 보편적 계약은 내용이 빈약하고, 강제성도 낮아. ( 강제되어야 할 것들도 강제되지 않은 느낌? )

1. x.clone() != x

2. x.clone().getClass() == x.getClass()

3. x.clone().equals( x )

4. 복제를 할 때는 어떤 생성자도 호출되지 않는다.

1.2.3 요것들이 보편적 계약 내용인데 "필수 요건이 아님" 이라는 게 웃기지.

게다가 4 는 너무나도 엄격한 계약이지.






clone method 는 그럼 어떻게 override 해?

- final 이 아닌 슈퍼 클래스의 clone 메소드를 오버라이드 할 경우, 서브 클래스의 clone에서는 반드시 super.clone을 호출하여 얻은 객체를 반환해야 한다. ( 이 때 슈퍼 클래스들도 clone 을 override 했다면, 마찬가지로 super.clone 을 return 하는 형식 )

- return type 을 override 하고 있는 class 명으로 바꾸어서 return 한다. - Client 에서의 형 변환이 필요가 없다.
   ( 자바 1.4 이후부터 covariant return type ( 공변 반환 타입 ) 이 가능해졌다. )
   ( "라이브러리에서 할 수 있는 것을 클라이언트가 하도록 하지 말자!!" )

// example code
@Override
public PhoneNumber clone(){
   try{
      return (PhoneNumber) super.clone();
   }
   catch( CloneNotSupportedException e ){
      throw new AssertionError(); 
   }
}

 
[Covariant Return Type이란?]

 - 기본 object.clone() 에서는 참조하는 필드에 대해서, 그 참조값 ( 주소값 ) 을 그대로 전달하기 때문에, 이럴 경우 "복제"가 필요하다.  참조필드가 있는데 기본 clone() 으로 퉁치려 한다면, 원본 또는 복제본 중 어느 것에 변화를 주었을 때, 엉뚱한 결과를 초래하거나 NullPointException 을 초래하기 쉽다. 사실상 clone 메소드는 또 다른 생성자인 셈이다. 따라서 clone 메소드가 원본 객체에 손상을 주지 않으면서 원본과 복제 객체 간의 상호 영향도 없도록 해야 한다. 가장 쉬운 방법은 재귀적으로 clone 메소드를 호출하는 것이다. ( 참조객체에 대해 clone 을 수행해준다. ) 

// example code
@Override
public Stack clone(){
   try{
      Stack result = (Stack) super.clone();
      result.elements = elements.clone();
      return result;
   }
   catch( CloneNotSupportedException e ){
      throw new AssertionError();
   }
}

 
- 위와 같은 참조값 복제를 수행 할 때, 참조 변수가 final 로 지정되어 있을때는 추가적인 문제가 발생할 수 있다.
   가변 객체를 참조하는 final 상수 필드의 일반적인 사용법과 달라진다는 것!!.
   따라서 경우에 따라 final 키워드를 제거해야할 필요가 생긴다.

- super 의 clone 이 제대로 구현되어 있지 않은 경우는, 참조값이 또 다시 참조하는 것들에 대한 재귀적인 복제도 필요하다.
   ( deepcopy() - deepcopy 를 할 때는 재귀적 호출보다는 loop 반복처리가 권장된다. 
                          재귀적 호출은 stack overflow 를 가져올 수 있기 때문!)

- 생성자처럼, clone 메소드에서는 복제 중인 객체의 final 이 아닌 ( 오버라이드 가능한 ) 어떤 메소드도 호출하면 안된다.  clone 에서 해당 메소드를 호출하면, 자신이 정의된 서브 클래스의 상태가 복제 객체에 정착되기 전에 실행될 것이므로, 복제본과 원본 객체 모두 정상이 아닌 상태가 될 가능성이 많다.

- clone 을 public 으로 오버라이딩 할 때는 CloneNotSupportedException 을 던지지 않도록 하는 것이 좋다.

- clone()메소드는 동기화를 지원하지 않는다. 따라서 필요하다면 동기화되는 clone 메소드를 구현해주어야 한다.

- clone 을 통해 완벽히 모든 것을 다 복사하는 것이 아닌 경우도 있다.
  일련 번호나 고유 번호( ID ), 객체 생성시간 등을 나타내는 필드의 경우는 ( 의도적이지 않다면 ) 
  복제하기보다는 clone 메소드에서 따로 설정해주는 것이 좋다. 






clone 은 엄청 복잡하네?? clone 꼭 override 해줘야 해?

- 앞서 말했다시피, 객체 복사라던지, 특수 목적이 있을 때 외에는 안 하는 것이 좋고, 실제 현장(?) 에서도 하는 경우는 매우 드물다.

- 객체를 복제하는 다른 방법을 제공하거나, 또는 복제할 수 없도록 하는 것이 더 좋다.
  특히 불변 클래스의 객체 복제를 지원하는 것은 바람직하지 않다. ( 복제본 = 원본 이기 때문. )






그럼 객체를 복제하는 다른 방법은 뭔데?

- 복제 생성자나 복제 팩토리 메소드를 제공하는 거지.
  ( so called Conversion Constructor, Conversion Factory Method )
  복제 생성자는 그냥 생성자인데, 해당 클래스 instance 를 parameter 로 받는 생성자야.
  ex ) public Foo( Foo foo ){ ... }
 
  복제 팩토리 메소드는 복제 생성자와 유사한 static 팩토리 메소드지.
  ex ) public static Foo newInstance( Foo foo ){ ... }


- 복제 생성자나 복제 팩토리 메소드의 장점
 1. final 필드를 올바르게 사용하는지에 대한 신경 쓸 필요가 없다.
 2. checked exception 을 불필요하게 발생시키지 않음.
 3. 복제된 객체의 타입 변환이 필요 없음. 

- Cloneable 의 단점이 많기 때문에, 배열 복제 정도로 사용하면 모를까, 일부 숙련 프로그래머들은 clone 메소드를 전혀 오버라이드 하지 않고 호출하지도 않는다. 상속을 해주기 위한 클래스를 설계할 때, 잘 동작하는 protected clone 메소드를 그 클래스에 두지 않는다면 서브 클래스에서 Cloneable 인터페이스를 제대로 구현할 수 없다는 것에 명심.!  



도움이 되셨다면 손가락 꾸욱~

반응형

댓글