🎯 학습 목표
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) { ... }
😋 지극히 개인적인 블로그지만 훈수와 조언은 제 성장에 도움이 됩니다 😋
'개인 공부 > Java (이펙티브 자바)' 카테고리의 다른 글
[이펙티브 자바] 아이템 18 : 상속보다 컴포지션을 사용하라 (0) | 2023.02.14 |
---|---|
[이펙티브 자바] 아이템 15 : 클래스와 멤버의 접근 권한을 최소화하라 (0) | 2023.02.08 |
[이펙티브 자바] 아이템 10 : equals는 일반 규약을 지켜 재정의하라 (0) | 2023.01.31 |
[이펙티브 자바] 아이템 7 : 다 쓴 객체 참조를 해제하라 (0) | 2023.01.31 |
[이펙티브 자바] 아이템 6 : 불필요한 객체 생성을 피하라 (0) | 2023.01.15 |
댓글