티스토리 뷰
728x90
반응형
clone 재정의는 주의해서 진행하라
- Cloneable 인터페이스는 복제해도 되는 클래스임을 명시하는 용도의 마커 인터페이스입니다.
- 객체를 복사하고 싶다면 Cloneable 인터페이스를 구현하여 clone 메서드를 재정의하는 방법이 일반적입니다.
- 그러나 clone 메서드가 선언된 곳은 Cloneable 인터페이스가 아닌 Object 클래스에 선언되어 있고 접근 제한자가 protected 입니다. 그래서 Cloneable 인터페이스를 구현한 것만으로는 외부 객체에서 clone 메서드를 호출할 수 없고 리플렉션을 사용하면 가능하지만 100% 성공하는것도 아닙니다. 접근할려는 객체가 접근이 허용된 clone 메서드를 제공하지 않는다면 성공을 할 수 없습니다.
public class Object {
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
...생략
}
Cloneable 인터페이스의 용도
- Object 클래스에 선언된 clone 메서드의 동작 방식을 결정할 수 있습니다.
- Cloneable 인터페이스를 구현한 클래스의 인스턴스에서 clone 메서드를 호출하면 그 객체의 필드를 하나하나 복사한 객체를 반환하며, 그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 발생시키게 됩니다.
- 일반적으로 인터페이스를 구현한다는 것은 구현 클래스가 그 인터페이스에 정의한 기능을 제공한다고 선언하는 행위 입니다. 그런데 Cloneable의 경우에는 상위 클래스(Object)에 정의된 clone 메서드의 동작 방식을 변경한 것입니다.
@ToString
public class Product {
private String name;
private int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class EffectiveJavaApplication {
public static void main(String[] args) throws CloneNotSupportedException {
Product macbook = new Product("맥북", 1000000);
Product copyMacbook = (Product) macbook.clone();
System.out.println(copyMacbook.toString());
}
}
clone 메서드의 규약
- x.clone() != x 은 참이어야 합니다.
- 복사된 객체가 원본객체랑 같은 주소를 가지면 안됩니다.
- x.clone().getClass() == x.getClass() 식도 참이어야 합니다.
- 복사된 객체가 같은 클래스여야 합니다.
- (optional) x.clone().equals(x) 는 참이어야 하지만, 필수는 아닙니다.
- 복사된 객체가 논리적 동치는 일치해야하지만 필수는 아닙니다.
- 위 규약을 보면 clone 메서드의 반환값이 복사될 객체를 가르키기에 생성자 연쇄와 유사한데 이 말인즉슨 clone 내부로직이 생성자를 호출해 얻은 인스턴스를 반환해도 문제가 없다는 것입니다. 하지만 이렇게 되면 해당 클래스의 하위 클래스에서 super.clone() 메서드로 호출할 때 상위 객체에서 잘못된 클래스가 생성될 수 있기에 위험합니다.
문제점
- 클래스의 모든 필드가 기본 타입이거나 불변 객체를 참조한다면 super.clone() 메서드만으로도 문제없이 작동합니다.
- 아래 예제 코드와 같이 객체의 필드가 String, int로 기본 타입인 경우 clone 메서드에서 super.clone 메서드 호출만으로 정상적인
동작을 할 수 있습니다. 그리고 자바는 공변 반환 타이핑을 지원하므로 반환 타입을 상위 클래스의 하위 타입으로 형변환해 줄 수 있습니다. 그리고 try-catch 블록으로 예외를 대응했지만 해당 예외는 발생하지 않을 것입니다. 그렇기에 Unchecked Exception이였어야 한다는 신호가 됩니다.
public class Product implements Cloneable {
private String name;
private int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public Product clone() throws CloneNotSupportedException {
try {
return (Product) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
🧨 하지만 가변 객체를 참조하는 클래스의 clone을 재정의 하는경우에는 위의 예제는 문제가 발생할 수 있습니다.
- clone 메서드를 통해 복사된 객체는 Products 클래스의 products 필드의 주소를 참조하게 됩니다. 그렇기 때문에 원본과 복제본의
products 필드가 서로에게 영향을 주게 되면서 불변식을 해치고 데이터 오염이 발생할 수 있습니다.
public class Product {
private String name;
private int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
}
public class Products implements Cloneable {
private static final int BUFFER_SIZE = 16;
private Product[] products = new Product[BUFFER_SIZE];
private int size;
public void push(Product product) {
ensureCapacity();
products[size++] = product;
}
public Object pop(int index) {
if (size == 0) {
throw new IllegalArgumentException();
}
Object result = products[--size];
products[size] = null;
return result;
}
private void ensureCapacity() {
if (products.length == size) {
products = Arrays.copyOf(products, 2 * size + 1);
}
}
@Override
public Products clone() {
try {
return (Products) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class EffectiveJavaApplication {
public static void main(String[] args) throws CloneNotSupportedException {
Products products = new Products();
products.push(new Product("그램", 100000));
products.push(new Product("맥북", 100000));
Products copyProducts = products.clone();
System.out.println("product: " + products);
System.out.println("copyProducts: " + copyProducts);
copyProducts.push(new Product("삼성", 500000));
System.out.println("product: " + products);
System.out.println("copyProducts: " + copyProducts);
}
}
대안책
- clone 메서드는 생성자와 같은 효과를 내는데 원본과 동일한 내용을 원본에 영향을 주지 않으며 복제된 객체의 불변식을 보장해야 합니다.
💡 재귀적 호출
- 가장 쉬운 방법은 객체의 참조 변수나 배열의 clone을 재귀적으로 호출해주는 것입니다.
- 하지만 final 필드는 새로운 값을 할당할 수 없기 때문에 코드가 동작안할 가능성이 있습니다. 복제 가능한 클래스를 만들기 위해서는 final을 해제해야 하는 필드들도 있습니다.
- 내부적으로 배열을 재귀적 호출로 clone 메서드를 호출해서 연결할 수 있지만, 이 배열이 객체 배열이고 연결 리스트라면 원본과 복사본은 같은 열결 리스트를 참조하여 의도치 않게 동작할 수 있습니다.
💡 Deep Copy
- 깊은 복사를 이용하여 위의 문제를 해결할 수 있습니다.
public class Product {
private String name;
private int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
// 복사 생성자
public Product(Product product) {
this.name = product.name;
this.price = product.price;
}
// 복사 팩토리
public static Product copyFactory (Product product) {
return new Product(product.name, product.price);
}
}
public class Products implements Cloneable {
private static final int BUFFER_SIZE = 16;
private Product[] products = new Product[BUFFER_SIZE];
private int size;
public void push(Product product) {
ensureCapacity();
products[size++] = product;
}
public Object pop() {
if (size == 0) {
throw new IllegalArgumentException();
}
Object result = products[--size];
products[size] = null;
return result;
}
private void ensureCapacity() {
if (products.length == size) {
products = Arrays.copyOf(products, 2 * size + 1);
}
}
@Override
public Products clone() {
try {
Products result = (Products) super.clone();
result.products = new Product[this.products.length];
int count = 0;
for (int i = 0; i < this.size; i++) {
Product copy = Product.copyFactory(this.products[i]);
result.products[count++] = copy;
}
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
}
public class EffectiveJavaApplication {
public static void main(String[] args) throws CloneNotSupportedException {
SpringApplication.run(EffectiveJavaApplication.class, args);
Products products = new Products();
products.push(new Product("그램", 100000));
products.push(new Product("맥북", 200000));
Products copyProducts = products.clone(); // 복시
copyProducts.push(new Product("삼성", 500000));
copyProducts.push(new Product("아이폰 미니 12", 100000));
products.push(new Product("아이폰 맥스 12", 150000));
products.pop();
products.pop();
copyProducts.pop();
}
}
주의점
- 생성자에서는 재정의될 수 있는 메서드를 호출하지 않아야 하며, 이는 clone 메서드도 동일합니다.
- 만약 clone이 하위 클래스에서 재정의한 메서드를 호출하면 하위 클래스는 복제 과정에서 자신의 상태를 바꿀 기회가 사라지며 복제본과 원본의 상태가 달라질 수 있습니다.
- 재정의한 clone 메서드는 throws 절을 없애야 합니다.
- Cloneable 인터페이스를 구현한 모든 클래스는 clone 메서드를 재정의해야 합니다.
- 상속용 클래스는 Cloneable 클래스를 구현해서는 안됩니다. 아니면 구현을 할 수 없도록 선언해야 합니다.
@Override
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
복사 생성자와 복사 팩토리
- 이미 Cloneable 인터페이스를 구현한 클래스는 어쩔 수 없지만 그게 아니라면 복사 생성자나 복사 팩토리라는 객체 복사 방식을 고려할만 합니다.
- 이 두 방식을 사용하면 Cloneable/clone와 비교해서 더 나은점은 아래와 같습니다.
- 언어 모순적이고 생성자를 쓰지 않는 객체 생성 매커니즘을 사용하지 않습니다.
- 정상적인 final 필드 용법과도 충돌하지 않습니다.
- 불필요한 예외가 발생하지 않습니다.
- 형변환도 필요하지 않습니다.
public class Product {
private String name;
private int price;
public Product(String name, int price) {
this.name = name;
this.price = price;
}
// 복사 생성자
public Product(Product product) {
this.name = product.name;
this.price = product.price;
}
// 복사 팩토리
public static Product copyFactory (Product product) {
return new Product(product.name, product.price);
}
}
💡 정리
- 복사가 필요하면 복제 생성자 및 팩토리를 사용하는게 좋습니다.
- 오직 배열만이 clone 메서드 방식의 사용이 권장됩니다.
- 클래스를 새로 만들때는 Cloneable 인터페이스를 확장하지 않는것이 좋습니다.
참고자료)
https://catsbi.oopy.io/313829e4-e869-48fe-9fb4-adcca06de1c5
https://daldalhanstory.tistory.com/282
https://jake-seo-dev.tistory.com/31
728x90
반응형
'스터디 > 이펙티브 자바' 카테고리의 다른 글
이펙티브 자바 - Item15. 클래스와 멤버의 접근 권한을 최소화하라. (0) | 2022.07.11 |
---|---|
이펙티브 자바 - Item14. Comparable을 구현할지 고려하라. (0) | 2022.07.09 |
이펙티브 자바 - Item12. toString을 항상 재정의하라. (0) | 2022.07.08 |
이펙티브 자바 - Item11. equals를 재정의하려거든 hashCode도 재정의하라. (0) | 2022.07.07 |
이펙티브 자바 - Item10. equals는 일반 규약을 지켜 재정의하라. (0) | 2022.07.06 |
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
TAG
- spring boot excel download oom
- java userThread와 DaemonThread
- 공간 기반 아키텍처
- pipe and filter architecture
- 서비스 기반 아키텍처
- spring boot redis 대기열 구현
- transactional outbox pattern
- polling publisher spring boot
- space based architecture
- spring boot poi excel download
- 람다 표현식
- transactional outbox pattern spring boot
- spring boot redisson sorted set
- redis sorted set
- spring boot excel download paging
- service based architecture
- 트랜잭셔널 아웃박스 패턴 스프링 부트 예제
- microkernel architecture
- spring boot 엑셀 다운로드
- spring boot redisson 분산락 구현
- JDK Dynamic Proxy와 CGLIB의 차이
- redis sorted set으로 대기열 구현
- java ThreadLocal
- 트랜잭셔널 아웃박스 패턴 스프링부트
- spring boot redisson destributed lock
- pipeline architecture
- 자바 백엔드 개발자 추천 도서
- @ControllerAdvice
- 레이어드 아키텍처란
- redis 대기열 구현
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
글 보관함