본문 바로가기
개인 공부/Java (이펙티브 자바)

[이펙티브 자바] 아이템 13 : clone 재정의는 주의해서 진행하라

by 희조당 2023. 2. 1.
728x90

🎯 학습 목표

  • Cloneable 인터페이스란?
  • 재정의 시 문제점
  • 변환 생성자, 변환 팩토리

📌 clone 재정의 시 주의하기!

이번 아이템은 clone()을 사용할 때 주의점을 다룬다. 그러기에 앞서 Cloneable이 뭔지 알아보자!

💡 Cloneable 인터페이스

Cloneable Interface란? 일종의 maker interface'cloen에 의해 복제할 수 있다'를 표시하는 인터페이스이다.

Java에서는 인스턴스의 복제를 위해 clone() 메서드가 구현되어 있다.

신기하게도 이 메서드는 Cloneable 내부에 구현되어 있을 거란 예상을 깨고 java.lang.Object 클래스에 protected 접근 지정자로 구현되어 있다. 내부에는 구현해야 할 메서드가 하나도 없다!

 

사용법과 주의사항은 예시를 보면서 이해해 보자.

😉 사용법

public class Pokemon implements Cloneable {
    private List<String> skills;
    private int attack;
    private int defense;
    private int hp;

    public Pokemon(List<String> skills, int attack, int defense, int hp) {
        this.skills = skills;
        this.attack = attack;
        this.defense = defense;
        this.hp = hp;
    }

    @Override // clone() 구현..!!
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }

    // getter, seterr ...
}

복사할 객체에 Cloneable을 상속시켜 준 뒤 오버라이딩해 주면 된다.

clone()을 호출하면 인스턴스와 같은 크기의 메모리를 할당하고, 인스턴스의 필드를 그대로 복사한 복사본을 리턴한다.

😮 주의사항

주의할 점은 두 가지가 존재한다. 예시와 함께 이해해 보자!

  • clone() 메서드는 피상적 복사를 지원한다.
  • Cloneable상속하지 않으면 예외를 던진다.
public static void main(String[] args) throws CloneNotSupportedException {
    List<String> skills = Arrays.asList("파괴광선", "누르기");
    Pokemon pokemon = new Pokemon(skills, 10, 10, 10);
    Pokemon clonePokemon = (Pokemon) pokemon.clone();

    System.out.println("포켓몬 공격력 : " + pokemon.getAttack());
    System.out.println("복제몬 공격력 : " + clonePokemon.getAttack());

    System.out.println("포켓몬 기술 : " + pokemon.getSkills());
    System.out.println("복제몬 기술 : " + clonePokemon.getSkills());

    // 똑같은 참조를 가진다..!
    System.out.println(System.identityHashCode(pokemon.getAttack()));
    System.out.println(System.identityHashCode(clonePokemon.getAttack()));

    System.out.println(System.identityHashCode(pokemon.getSkills()));
    System.out.println(System.identityHashCode(clonePokemon.getSkills()));
}

피상적 복사란, 단순하게 참조만 복사한다고 이해하면 쉽다. 파이썬의 얕은 복사와 같다!

배열을 예로 들면, 내부 요소 하나하나 복사되는 것이 아니라 배열의 참조를 복사한다.

피상적 복사

두 번째로, java.lang.Object에 구현되어 있지만 Cloneable을 상속하지 않으면 다음과 같이 예외를 던진다.

정말 단순하게 복사만 하기 때문에 생성자를 호출하는 것이 아니라는 점도 꼭 기억해서 사용해야 한다!


📌 재정의 시 문제점

언뜻 보면 좋은 기능일 수 있지만 너무 특이한 메커니즘을 가지고 있다. (좋은 메커니즘은 아니다.)

바로 생성자를 호출하지 않고도 객체를 생성한다는 점이다.

 

이러한 문제 때문에 다음과 같은 경우에는 사용해도 무방하다.

  • 복사할 객체가 final 클래스이다. 
  • 모든 필드가 기본 타입이고 불변 객체를 참조한다.

위와 같은 경우에는 형변환을 이용해서 클라이언트에서 사용하기 편하게끔 작성해 줄 수 있다.

@Override
public Pokemon clone() {
    try {
        return (Pokemon) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

1️⃣ 가변 객체를 참조

가변 객체를 참조하는 객체를 복사하는 경우는 문제가 발생한다.

앞서 공부했듯이 clone()을 사용해서 복사한 인스턴스의 모든 필드는 같은 참조값을 가진다.

이는 다시 말해, 원본과 복사본이 있을 때 복사본의 가변 객체를 수정하면 원본 값도 수정된다는 의미이다.

 

생성자를 사용하면 이런 문제가 발생하지 않지만 유사 생성자인 clone()객체의 불변성을 보장할 수 없다,

따라서, 다음과 같이 재정의해서 불변성을 보장해야 한다.

@Override
public Pokemon clone() {
    try {
        Pokemon result = (Pokemon) super.clone();
        result.skills = new ArrayList<>(skills);
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}

달라진 참조..!!

2️⃣ 복잡한 가변 객체를 참조

복잡한 케이스는 책에서 제공한 예시를 보면서 이해해 보자.

public class HashTable implements Cloneable {
    private Entry[] buckets;

    private static class Entry {
        final Object key;
        Object value;
        Entry next;

        Entry(Object key, Object value, Entry next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
	
    // getter, setter ...
}

이렇게 내부 엔트리를 통해서 특정 값에 접근할 수 있도록 구현한 해시테이블이 있다.

단순하게 clone()을 사용하면 복사본이 원본의 엔트리를 통해서 값을 찾기 때문에 잘못된 값을 찾게 된다.

 

다음과 같이 재귀적으로 구현해서 해결할 수도 있다. 

이 경우는 재귀의 특성상 오버플로우와 같은 문제를 일으킬 수 있기 때문에 반복문을 쓰는 방법을 추천한다.

Entry deepCopy() {
    return new Entry(key, value,
    	next == null ? null : next.deepCopy());
}

@Override
public HashTable clone() {
    try {
        HashTable result = (HashTable) super.clone();
        result.buckets = new Entry[buckets.length];
        for (int i = 0; i < buckets.length; i++) {
            if (buckets[i] != null) {
                result.buckets[i] = buckets[i].deepCopy();
            }
        }
        return result;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}
// 개선된 deepCopy()
Entry deepCopy() {
    Entry result = new Entry(key, value, next);
    for (Entry p = result; p.next != null; p = p.next) {
        p.next = new Entry(p.next.key, p.next.value, p.next.next);
    }
    return result;
}

다른 방법으로 고수준 메서드를 만들어서 복제하는 방법도 있다.

하지만 좋은 성능을 기대할 수 없고, Cloneable 아키텍처와 어울리지 않는 방법이다.

3️⃣ 상속 가능한 객체

상속 가능한 객체는 기존 Object 방식(protected, throwable)을 따라서 구현할 수도 있고 동작을 못하게 막을 수도 있다.

하지만 상속 가능한 객체, 즉 상속 클래스는 Cloneable을 구현해서는 안 된다.


📌 변환 생성자, 변환 팩토리

지금까지 공부한 내용을 정리하면 다음과 같다.

  • Cloneable을 구현하는 모든 클래스는 clone()을 재정의해야 한다.
  • 재정의된 clone()은 public 접근 지정자, 자신의 타입을 리턴해야 한다.
  • 객체 내부에 깊은 구조를 가진다면 복사본이 원본을 가리키지 않게 해야 한다.

사실 clone()을 사용할 경우가 많지도 않고 썩 좋은 방법이 아니다.

생성자를 사용해서 새로운 복사본을 넘겨주는 변환 생성자, 변환 팩토리를 사용하는 것을 추천한다.

// 변환 생성자
public Pokemon(Pokemon pokemon) { ... }

// 변환 팩토리
public Pokemon newInstance(Pokemon pokemon) { ... }

 

😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋

댓글