조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)
1. 개요
clone 메서드의 개념과 사용법, 주의사항에 대해 알아보자.
2. clone 메서드가 뭔가요?
2.1. 정의
clone 메서드는 인스턴스를 복제하는 메서드이다. 이 메서드를 사용하기 위해서는 반드시 Cloneable 인터페이스를 implements 해야한다. 이를 어길 경우 clone 메서드 호출 시 CloneNotSupportedException이 발생한다.
public class Human implements Cloneable{
...
}
2.2. 구현할게 없는 Cloneable 인터페이스
clone 메서드를 사용하기 위해 Cloneable 인터페이스를 implements 한 후 구현 메서드를 작성하기 위해 Cloneable 인터페이스를 들어가봤더니 구현할 메서드가 없다. clone 이라는 메서드조차 없다. Clonable은 JVM에게 '이 클래스는 복제 가능한 클래스다.' 라는 것을 알려주는 구분자 역할을 할 뿐이기 때문이다.
2.3. 그럼 clone 메서드는 어디에?
실제 복제 기능을 수행하는 clone 메서드는 Object 클래스 존재한다. Object 클래스에 있는 clone 메서드의 사용법을 알아보자.
2.4. clone 메서드 사용법
Object의 clone() 메서드는 접근 제어자가 protected 이기 때문에 자식클래스 또는 같은 패키지 내에서만 호출이 가능하다. Object의 패키지 경로는 java.lang.Object이니 결국 자식 클래스에서만 호출이 가능하다. 즉, 외부에서는 호출이 불가능한 것이다.
이에따라 클래스 내부에서 super.clone() 메서드를 호출하도록 clone 메서드를 재정의 해야한다.
public class Human implements Cloneable{
private String name;
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Human(String name){
this.name = name;
}
public String getName() {
return name;
}
}
실제로 테스트를 하면 똑같은 필드를 가진 인스턴스가 생성됨을 알 수 있다.
public static void main(String[] args) throws CloneNotSupportedException {
Human human = new Human("테스터");
Human cloneHuman = (Human) human.clone();
System.out.println(human.getName());
System.out.println(cloneHuman.getName());
}
3. 주의사항
3.1. CloneNotSupportedException 예외 전환하기
CloneNotSupportedException 예외가 발생하는 경우는 Cloneable 인터페이스를 implements 하지 않은 상태에서 clone 메서드를 호출할때 뿐이다.
사실 개발자 입장에서 clone 메서드를 사용한다면 Cloneable 인터페이스를 implements 해야한다는 사실을 알고있기에 CloneNotSupportedException 예외 상황은 발생 불가능한 예외라고 할 수 있다.
그런데 이 예외는 Checked Exception이며 예외처리를 필수로 해야한다. 발생 불가능한 예외에 대해 예외처리를 하는거 자체가 웃긴 상황이다.
예외 처리를 강제하지 않도록 예상치 못한 상황이 발생했음을 나타내는 에러인 AssertionError를 사용하여 예외를 변환해주자.
@Override
public Object clone() {
try{
return super.clone();
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
3.2. 형변환은 clone 메서드 내에서
현재 clone 메서드에서는 Object 타입으로 복제 후 클라이언트에서 이를 형변환하고 있다. 형변환 작업은 clone 메서드 내에서 처리한 후 해당 타입에 맞게 반환해주자.
@Override
public Human clone() {
try{
return (Human) super.clone();
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
...
//Main.class
public static void main(String[] args){
Human human = new Human("테스터");
Human cloneHuman = human.clone();
}
3.3. 가변 객체를 참조하는 클래스에 대해서는 clone을 사용하면 안된다.
아래 Stack 클래스의 경우 elements라는 가변객체를 참조하고 있다. Stack에 대해 clone을 하게 된다면, 복제된 인스턴스 내의 elements는 기존 elements와 동일한 주소를 갖는 elements를 참조할 것이다. 둘 중 하나의 인스턴스를 수정하면 다른 하나도 수정되어 프로그램이 이상하게 동작할 수 있다.
public class Stack implements Cloneable{
private Object[] elements;
private int size = 0;
private static final int DEFAULT_INITIAL_CAPACITY = 16;
public Stack(){
this.elements = new Object[DEFAULT_INITIAL_CAPACITY];
}
public void push(Object e){
ensureCapacity();
elements[size++] = e;
}
public Object pop(){
if(size == 0){
throw new EmptyStackException();
}
Object result = elements[--size];
elements[size] = null;
return result;
}
private void ensureCapacity(){
if(elements.length == size){
elements = Arrays.copyOf(elements, 2* size + 1);
}
}
@Override
protected Stack clone() {
try{
return (Stack) super.clone();
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
}
테스트를 통해 알아보면, 다음과 같이 1과 2를 push한 stack 인스턴스를 생성한 후 cloneStack 인스턴스로 복제한다. 그리고 cloneStack 인스턴스에 대해 3을 push 한다면 elements 값에 3이라는 값이 추가될것이고, stack과 cloneStack 모두 동일한 elements를 갖고 있으므로 두 인스턴스 모두에게 영향을 끼치게 된다. 엎친데 덥친격으로 stack의 경우 elements의 실제 사이즈는 3이나, size 변수의 값은 2 가 된다. 버그를 유발할 가능성이 매우 크다.
class Main{
public static void main(String[] args) {
Stack stack = new Stack();
stack.push(1);
stack.push(2);
Stack cloneStack = stack.clone();
cloneStack.push(3);
}
}
clone 메서드는 본 객체에 아무런 해를 끼치지 않는 동시에 복제된 객체의 불변식을 보장해야한다. Stack의 clone 메서드를 제대로 동작하기 위해서는 elements 배열도 clone을 통해 복제해야 한다.
참고로 배열의 clone은 원본 배열과 똑같은 타입의 배열을 반환하기 때문에 형변환이 필요 없다. 이러한 이유로 배열 복제 시 clone 메서드를 사용을 권장하고 있다.
@Override
protected Stack clone() {
try{
Stack stack = (Stack)super.clone();
stack.elements = elements.clone();
return stack;
}catch (CloneNotSupportedException e){
throw new AssertionError();
}
}
3.4. 주의사항이 많은 clone
clone을 올바르게 사용하기 위해 Clonable 인터페이스를 구현, clone 메서드 재정의, 예외전환, 형변환, 가변객체는 따로 clone 하는 등의 많은 주의사항을 지켜야 한다. 그런데 객체를 복사하는 방법이 clone 메서드를 사용하는 방법만 있는 건 아니다. 앞서 언급한 주의사항을 고려하지 않아도 되는 복사 생성자와 복사 팩터리라는 더 좋은 방식이 있다.
4. 복사 생성자와 복사 팩터리
복사 생성자는 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자이고, 복사 팩터리는 복사 생성자를 통해 인스턴스를 제공하는 정적 팩터리 메서드이다.
이 방법을 사용하면 Clonable 인터페이스를 구현하지 않아도 되고, clone 메서드를 재정의하지 않아도 되고, 불필요한 예외처리를 할 필요가 없고, 형변환 할 필요도 없다. 배열 타입의 가변객체의 경우 똑같이 clone을 사용하곤 있지만, 앞서 언급한 여러 방면을 비교해봤을 때 clone보다 복사 생성자와 팩터리 메서드를 사용하는 방식이 이점이 많다.
public Stack(Stack stack){
this.elements = stack.elements.clone();
this.size = stack.size;
}
// 복사 팩터리 메서드
public static Stack newInstance(Stack stack){
return new Stack(stack);
}
5. 정리
복제 기능은 생성자와 팩터리를 이용하는게 최고다. 단, 배열 복제 시에는 clone 사용을 권장한다.
'공부 > Effective Java' 카테고리의 다른 글
[Effective Java] Item 15. 클래스와 멤버의 접근 권한을 최소화하라 / 정보은닉 (0) | 2023.09.20 |
---|---|
[Effective Java] Item 14. Comparable을 구현할지 고려하라 (0) | 2023.09.15 |
[Effective Java] Item 12. toString을 항상 재정의하라 (0) | 2023.09.14 |
[Effective Java] Item 6. 불필요한 객체 생성을 피하라 (2) | 2023.09.07 |
[Effective Java] Item 5. 자원을 직접 명시하지 말고 의존 객체 주입을 사용하라 (0) | 2023.09.06 |