Effective Java - 모든 객체의 공통 메서드(3)

SeungHyuk Shin·2021년 9월 14일
0

Effective Java

목록 보기
6/26
post-thumbnail

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


Cloneable은 복제해도 되는 클래스임을 명시하는 용도인 믹스인 인터페이스지만, 아쉽게도 의도한 목적을 제대로 이루지 못했다. 가장 큰 문제는 clone 메서드가 선언된 곳이 Clonealbe이 아닌 Obejct이고, 그마저도 protected라는데 있다. 하지만 이를 포함한 여러 문제점에도 불구하고 Cloneable 방식은 널리 쓰이고 있어서 잘 알아두는 것이 좋다.

그렇다면 메서드도 없는 Cloneable 인터페이스는 무슨일을 할까? 이 인터페이스는 놀랍게도 Object의 protected 메서드인 clone의 동작 방식을 결정한다. 이는 인터페이스를 상당히 이례적으로 사용한 예이니 따라하지는 말자. 인터페이스를 구현한다는 것은 일반적으로 해당 클래스가 그 인터페이스에서 정읳나 기능을 제공한다고 선언하는 행위다. 근데 Cloneable의 경우에는 상위 클래스에 정의된 메서드의 동작 방식을 변경한 것이다.

clone 메서드의 일반 규약을 허술하다. 다음을 봐 보자.

이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음 식은 참이다.
x.clone() != x
또한, 다음 식도 참이다.
x.clone().getClass() == x.getClass()
하지만 이상의 요구를 반드시 만족해야 하는 것은 아니다.
하나편 다음 식도 일반적으로 참이지만, 역시 필수는 아니다.
x.clone().equals(x)
관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와(Object를 제외한) 모든 상위 클래스가 이 관례를 따른다면 다음 식은 참이다.
x.clone().getClass() == x.getClass()
관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중하나 이상을 반환 전에 수정해야 할 수도 있다.

쓸데없는 복사를 지양한다는 관점에서 불변 클래스는 굳이 clone 메서드를 제공하지 않는게 좋다. 간단했던 앞서의 구현이 클래스가 가변 객체를 참조하는 순간 재앙으로 돌ㅇ변한다. 아이템 7에서 소개한 Stack 클래스를 예로 들어보자.

clone 메서드가 단순히 super.clone의 결과를 그대로 반환한다면 어떻게 될까? 반환된 Stack 인스턴스의 size 필드는 올바른 값을 갖겠지만, elements 필드는 원본 Stack 인스턴스와 똑같은 배열을 참조할 것이다.

clone 메서드는 사실상 생성자와 같은 효과를 낸다. 즉 clone은 원본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다.

배열의 clone은 런타임 타입과 컴파일타임 타입 모두가 원본 배열과 똑같은 배열을 반환한다. 따라서 배열을 복제할 때는 배열의 clone 메서를 사용하라고 권장한다. 사실 배열 clone 기능은 제대로 작동하는 유일한 예이다.

하지만 elements 필드가 final이라면 앞서의 방식은 작동하지 않는다.

복잡한 가변객체를 복제하는 마지막 방법은 super.clone을 호출하여 얻은 객체의 모든 필드를 초기상태로 설정한 다음, 원복 개체의 상태를 다시 생성하는 고수준 메서드를 호출한다. 즉 hashtable을 예시로 들자면, buckets 필드를 새로운 버킷 배열로 초기화한 다음 원본 테이블에 담긴 모든 키-값 쌍 각각에 대해 복제본 테이블의 put 메서드를 호출해 똑같이 하면된다.

하지만 고수준 API는 간단하고 우아한 코드를 가지고 있지만, 아무래도 저수준에서 바로 처리할 떄보다는 느리다.

기억해야 할 게 하나더 남았다. Cloneable을 구현한 스레드 안전 클래스를 작성할 때는 clone 메서드 역시 적절히 동기화 해야한다는 점이다. Obejct의 clone 메서드는 동기화를 신경쓰지 않았다. 그러니 super.clone 호출외에 다른 할일이 없더라도 clone을 재정의하고 동기화 해야한다.

그런데 이 모든 작업이 필요한 걸까? 다행히도 이처럼 복잡한 경우는 드물다. Cloenable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 하지만 그렇지 않은 상황에서는 복자 생성자와 복사 팩터리라는 더 나은 객체 복사 방식을 제공할 수 있다.

복사 생성자와 복사 팩터리는 해당 클래스가 구현한 '인터페이스' 타입의 인스턴스를 인수로 받을 수 있다.

0개의 댓글