item 13 : clone 재정의는 주의해서 진행해라
이번 아이템은 clone()을 사용할 때 주의점을 다룬다.
1. Cloneable 인터페이스
Cloneaable 인터페이스란?
일종의
maker interface
로 'cloen에 의해 복제할 수 있다'를 표시하는 인터페이스이다.복제해도 되는 클래스임을 명시하는 용도의 믹스인 인터페이스
Java에서는 인스턴스의 복제를 위해 clone()
메서드가 구현되어 있다.
신기하게도 이 메서드는 Cloneable
내부에 구현되어 있을 거란 예상을 깨고 java.lang.Object
클래스에 protected 접근 지정자로 구현되어 있다. 내부에는 구현해야 할 메서드가 하나도 없다!
Cloneaable 인터페이스 하는 일
Object의 protected 메서드인 clone의 동작 방식을 결정한다.
Cloneable의 경우, 일반적으로 인터페이스를 구현한다는 것 즉, 해당 클래스가 그 인터페이스에서 정의한 기능을 제공한다고 선언하는 것과 달리 상위 클래스에 정의된 protected 메서드의 동작방식을 변경한 것
사용법
복사할 객체에 Cloneable
을 상속시켜 준 뒤 오버라이딩해 주면 된다.
clone()
을 호출하면 인스턴스와 같은 크기의 메모리를 할당하고, 인스턴스의 필드를 그대로 복사한 복사본을 리턴한다.
Clonable 인터페이스의 문제점
복잡성과 위험성
Cloneable
과clone()
메서드는 객체 복사 메커니즘이 직관적이지 않고, 잘못 구현하면 오류가 발생하기 쉽다.가변 객체의 복사,
final
필드와의 충돌, 예외 처리 등 여러 가지 문제가 있다.
2. 재 정의시 문제점
🤔 Cloneable을 구현한 클래스는 clone 메서드를 public으로 제공하며, 사용자는 당연히 복제가 제대로 이뤄지겠지? 라고 실무에서는 기대
이 기대를 만족시키려면 그 클래스와 모든 상위 클래스는 복잡 하고, 강제할 수 없고, 허술하게 기술된 프로토콜을 지켜야만 하는데, 그 결과 로 깨지기 쉽고, 위험하고, 모순적인 메커니즘이 탄생한다. 생성자를 호출하지 않고도 객체를 생성할 수 있게 되는 것이다.
3. Clone() 메서드
1) clone() 메서드의 일반 규약
약간 허술하다고 함
x.clone() ! =x 식은 참이어야 한다.
복사된 객체와 원본이랑 같은 주소를 가지면 안된다는 뜻
x.clone().getClass() == x.getClass() 식도 참이어야 한다.
복사된 객체가 같은 클래스여야 한다는 소리임
x.clone().equals(x) 참이어야 하지만 필수는 아니다.
복사된 객체가 논리적 동치는 일치해야 한다고 함
반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.
2) super.clone()
의 중요성
super.clone()
의 중요성super.clone()
을 호출해야 하는 이유
clone()
메서드를 재정의할 때 가장 먼저 해야 할 일은 super.clone()
을 호출하는 것이다. 이는 Object
클래스의 clone()
메서드를 통해 객체의 필드 값을 복사한 새로운 객체를 생성하기 위함이다.
하위 클래스에서의 문제점
만약 super.clone()
을 호출하지 않고 생성자를 사용하여 인스턴스를 반환한다면, 하위 클래스에서 super.clone()
을 호출할 때 올바른 클래스의 객체가 생성되지 않을 수 있다. 이는 하위 클래스의 clone()
메서드가 제대로 동작하지 않게 만들 수 있다.
예시: 상위 클래스에서
super.clone()
을 호출하지 않고 새로운 객체를 생성하면, 하위 클래스에서super.clone()
을 호출해도 상위 클래스의 인스턴스가 반환되어 하위 클래스의 필드가 초기화되지 않을 수 있다.
final
클래스의 경우
클래스가 final
이라면 하위 클래스가 없으므로 이 규칙을 무시해도 안전하다. 그러나 이 경우에도 Cloneable
을 구현할 필요가 없다. Object
의 clone()
메서드를 사용하지 않기 때문이다.
3. Clone() 메서드의 구현
1) 가변 상태를 참조하지 않는 클래스용 clone() 메서드
super.clone()
을 호출한다.
정의된 모든 필드는 원본 필드와 똑같은 값을 갖게 된다.
모든 필드가 primitive 타입이거나 final 이라면, 이 상태로 끝이다.
사실 final은 쓸데 없는 복사를 지양하기 때문에 clone()이 필요 없다.
PhoneNumber 타입으로 형변환하여 반환하도록 해서 편의성을 증대시켰다.
try-catch 블록으로 감싼 이유는 Object.clone() 메서드가
CloneNotSupportedException
을 던지기 때문이다.
우린 PhoneNumber 클래스가 Cloneable 인터페이스를 상속받는 것을 보고 clone()을 지원함을 알 수 있다.
CloneNotSupportedException 예외 처리
Object
의clone()
메서드는CloneNotSupportedException
을 다.Cloneable
을 구현한 클래스에서clone()
메서드를 재정의할 때는 이 예외를 던질 필요가 없습니다.대신
try-catch
블록으로 감싸고, 예외가 발생할 수 없음을 보장하기 위해 를 던진다.과도한 검사 예외는 API를 사용하기 불편하게 만든다.
2) 가변 객체를 참조하는 클래스의 clone()
메서드 구현
clone()
메서드 구현가변객체란, instance 생성 이후에도 내부 상태 변경이 가능한 객체를 말한다.
이 클래스를 그대로 복제하면, primitive 타입의 값은 올바르게 복제되지만, 복제된 Stack의 elements는 복제 전의 Stack과 같은 배열을 가리키게 될 것이다.
두 스택에 같은 elements가 들어있고, 하나를 바꾸면 다른 하나도 연동된다는 뜻이다. 이건 우리가 원한 clone()이 아니다.
예를 들어,
Stack
클래스의elements
배열이 이에 해당한다.Stack
클래스의 유일한 생성자를 이용하면 이런 문제는 없을 것이다. 그러나 값이 복사되지 않는 문제가 있다.
얕은 복사의 문제점
단순히
super.clone()
을 호출하면 얕은 복사가 이루어져, 객체의 필드 값만 복사되고 그 필드들이 참조하는 객체들은 복사되지 않는다.따라서 가변 객체를 참조하는 필드가 있을 경우, 원본 객체와 복제된 객체가 동일한 가변 객체를 공유하게 된다.
문제점: 원본이나 복제본 중 하나에서 가변 객체를 변경하면 다른 하나에도 영향을 미쳐 불변식이 깨질 수 있다.
깊은 복사의 필요성
원본과 복제본이 독립적인 객체 상태를 유지하려면 가변 객체를 복사해야 한다. 이를 위해 해당 필드를 깊은 복사하여 새로운 객체를 생성해야 한다.
3) 깊은 복사를 위한 방법
배열의 clone()
메서드 사용
배열은 clone()
메서드를 호출하면 깊은 복사가 이루어집니다. 즉, 배열 자체가 복사되고 배열의 각 요소들도 복사됩니다. 따라서 배열을 복제할 때는 배열의 clone()
메서드를 사용하는 것이 좋습니다.
예시:
🧐 왜 elements
배열을 복제하나요?
elements
배열은Stack
의 내부 상태를 저장하는 중요한 가변 객체이다.원본과 복제본이 같은 배열을 공유하면 한쪽에서 변경이 발생할 때 다른 쪽에도 영향을 미친다.
이를 방지하기 위해
elements
배열을 복제하여 복제본이 독립적인 배열을 가지도록 한다.
사용예제
사용 예시
설명:
originalStack
을 생성하고 "A", "B"를 추가originalStack
을 복제하여clonedStack
을 생성원본과 복제본이 서로 다른 객체이고, 내부의
elements
배열도 다른 객체임을 확인원본 스택에 "C"를 추가해도 복제본에는 영향이 없다.
테스트 결과:
stack1
과stack2
는 서로 다른elements
배열을 가지고 있으며, 독립적으로 동작한다.
요약
clone()
메서드를 재정의할 때는super.clone()
을 호출하여 객체의 얕은 복사를 수행한다.가변 객체를 참조하는 필드는 반드시 복제하여 원본과 복제본이 독립적인 객체 상태를 유지하도록 한다.
CloneNotSupportedException
은 발생하지 않지만 예외 처리를 위해try-catch
블록을 사용하고, 예외가 발생하면AssertionError
를 던진다.Cloneable
인터페이스를 구현해야super.clone()
을 호출할 수 있다.
😀 위의 내용을 요약하자면
clone()
메서드를 재정의할 때는super.clone()
을 호출하여 객체의 얕은 복사를 수행한다.가변 객체를 참조하는 필드는 반드시 복제하여 원본과 복제본이 독립적인 객체 상태를 유지하도록 힌다.
CloneNotSupportedException
은 발생하지 않지만 예외 처리를 위해try-catch
블록을 사용하고, 예외가 발생하면AssertionError
를 던진다.Cloneable
인터페이스를 구현해야super.clone()
을 호출할 수 있다.
연결 리스트의 복제 방법
연결 리스트를 복제할 때는 각 노드를 개별적으로 복사해야 합니다.
재귀적 복제의 문제점
재귀적으로 연결 리스트를 복제하면 리스트의 길이만큼 스택 프레임을 사용하므로, 리스트가 길면 스택 오버플로우가 발생할 수 있다.
재귀적 복제 코드 예시:
반복문을 사용한 복제
반복문을 사용하면 스택 오버플로우의 위험 없이 연결 리스트를 복제할 수 있다.
반복문을 사용한 복제 코드 예시:
코드 예시: HashTable
클래스의 clone()
메서드
각 버킷의 연결 리스트를 복제하여 원본과 복제본이 독립적인 상태를 유지합니다.
4) clone()
메서드에서 재정의 가능한 메서드 호출 금지
clone()
메서드에서 재정의 가능한 메서드 호출 금지재정의된 메서드 호출의 문제점
clone()
메서드에서 하위 클래스에서 재정의된 메서드를 호출하면, 하위 클래스는 복제 과정에서 자신의 상태를 적절히 초기화할 수 없게 된다. 이는 원본과 복제본의 상태가 달라질 수 있다는 것을 의미
해결책
clone()
메서드 내에서 호출하는 메서드는 final
또는 private
으로 선언하여 재정의되지 않도록 해야 한다.
예시:
5) final
필드와의 충돌 문제
final
필드와의 충돌 문제만약
elements
필드가final
로 선언되어 있다면,clone()
메서드에서 새로운 배열을 할당할 수 없어 문제가 발생한다.이는 가변 객체를 참조하는 필드는
final
로 선언하라는 일반적인 규칙과 충돌한다.이러한 경우,
clone()
메서드를 사용하는 대신 복사 생성자를 사용하는 것이 더 바람직하다.
복사 생성자 사용 예시
장점:
clone()
메서드보다 구현이 간단하고 명확하다.예외 처리가 필요 없다.
final
필드와의 충돌이 없다.
사용 예시
stack1
과stack2
는 독립적인 스택으로 동작한다.
6) HashTable
클래스의 예시와 문제점
HashTable
클래스의 예시와 문제점잘못된 clone()
메서드:
buckets
배열만 복제하고, 배열 내부의Entry
객체들은 복제되지 않았다.문제점: 복제된
HashTable
과 원본HashTable
이 같은Entry
객체들을 공유하게 된다.
7) HashTable
클래스의 올바른 clone()
메서드 구현
HashTable
클래스의 올바른 clone()
메서드 구현깊은 복사를 위한 clone()
메서드:
deepCopy()
메서드를 통해 각 버킷의 연결 리스트를 복제한다.결과: 복제된
HashTable
은 원본과 독립적인Entry
객체들을 가지게된다.
8) deepCopy()
메서드의 동작 원리
deepCopy()
메서드의 동작 원리반복문 사용: 재귀 호출 대신 반복문을 사용하여 스택 오버플로우를 방지
새로운
Entry
객체 생성: 연결된 각Entry
를 새로운 객체로 생성하여 복사한다.원본과 복제본의 독립성 보장: 복제된 객체들은 원본 객체들과 독립적으로 동작한다.
9) 생성자에서 재정의 가능한 메서드 호출 금지
생성자에서는 재정의될 수 있는 메서드를 호출하면 안 됩니다.
만약
put()
메서드를 이용하여 복제한다면,put()
메서드는final
또는private
으로 선언되어야 합니다.이는 하위 클래스에서 메서드를 재정의하여 예상치 못한 동작이 발생하는 것을 방지합니다.
4. Cloneable
의 문제점 요약
Cloneable
의 문제점 요약언어적인 모순과 위험성:
Cloneable
과clone()
메서드는 직관적이지 않고 구현하기 어렵다.얕은 복사의 기본 동작: 기본적으로 얕은 복사가 이루어져 가변 객체를 제대로 복제하지 못한다.
final
필드와의 충돌:final
필드는 복사 후 값을 변경할 수 없어 복제 시 문제가 발생한다.예외 처리의 복잡성:
CloneNotSupportedException
등 불필요한 예외 처리가 필요재정의된 메서드 호출 문제:
clone()
메서드에서 재정의된 메서드를 호출하면 문제가 발생한다.
5. Clone() 메서드 주의사항
Object.clone()
은 동기화를 신경쓰지 않은 메서드이다.동시성 문제가 발생할 수 있다.
만일
clone()
을 막고 싶다면clone()
메서드를 재정의하여,CloneNotSupportedException()
을 던지도록 하자.기본 타입이나 불변 객체 참조만 가지면 아무것도 수정할 필요 없으나
일련번호
혹은고유 ID
와 같은 값을 가지고 있다면, 비록 불변일지라도 새롭게 수정해줘야 함
6. 복사 생성자 및 복사 팩터리 메서드
복사 생성자란 단순히 자신과 같은 클래스의 인스턴스를 인수로 받는 생성자를 말한다.
Cloneable
인터페이스와 clone()
메서드 대신 복사 생성자와 복사 팩터리 메서드를 사용하는 것이 좋다.
복사 팩터리 메서드: 복사 생성자와 유사하지만 정적 팩터리 메서드로 구현된다.
Cloneable을 이미 구현한 클래스를 확장한다면 어쩔 수 없이 clone을 잘 작동하도록 구현해야 한다. 그렇지 않은 상황에서는
복사 생성자와 복사 팩터리
라는 더 나은 객체 복사 방식을 제공할 수 있다.
복사 생성자
복사 팩터리 메서드
복사 생성자와 복사 팩터리의 장점
직관적인 객체 생성: 생성자나 정적 메서드를 사용하므로 객체 생성 방식이 명확
안전성:
Cloneable
의 복잡한 규약에 의존하지 않으므로 구현 실수의 위험이 적다.유연성: 필요한 경우 복사 과정에서 필드를 변환하거나 수정할 수 있다.
예외 처리 간소화: 불필요한 예외를 던지지 않으므로 사용하기 편리합
형변환 불필요: 반환 타입이 명확하므로 형변환할 필요가 없다.
복사 생성자랑 복사 팩터리로 clone() 구현하기
위는
HashMap
에서 제공하는 복사 생성자로 볼 수 있다.
더 정확한 이름은
변환 생성자(conversion constructor)
와변환 팩터리(conversion factory)
이다.
ArrayList
변환 생성자 예시: ArrayList
ArrayList
이 방식은 clone()
메서드보다 여러 면에서 더 낫습니다. ArrayList
는 clone()
메서드를 사용하기보다, 다른 컬렉션의 요소를 받아들이는 생성자를 제공한다.
주석이 포함된 코드 예제:
설명:
목적: 이 생성자는 지정된 컬렉션
c
의 모든 요소를 포함하는 새로운ArrayList
인스턴스를 생성한다다. 요소들은 컬렉션의 이터레이터가 반환하는 순서를 따른다.자세한 장점:
클론 메커니즘의 문제 회피:
clone()
메서드는 얕은 복사로 인해 가변 객체를 올바르게 복제하지 못하는 등의 문제가 있다.생성자를 사용하면 복제 과정에서 완전한 통제를 할 수 있다.
인터페이스 타입 수용:
Collection<? extends E>
를 인수로 받으므로,List
,Set
등 다양한 컬렉션을 인수로 받을 수 있다.이를 통해 생성자의 활용 범위가 넓어짐
형변환 불필요:
내부적으로 타입 변환을 처리하므로, 호출 시에 별도의 형변환이 필요 없다.
런타임 시
ClassCastException
이 발생할 위험을 줄인다.
검사 예외 처리 간소화:
clone()
메서드는CloneNotSupportedException
을 처리해야 하지만, 이 생성자는 그런 예외를 던지지 않아 사용이 간편
final
필드의 올바른 초기화:생성자를 사용하면
final
필드를 적절히 초기화할 수 있지만,clone()
은 생성자를 호출하지 않으므로final
필드 초기화에 문제가 생길 수 있다.
사용 예시
✨ 결론
객체 복사를 필요로 할 때는
Cloneable
과clone()
메서드보다는 복사 생성자나 복사 팩터리 메서드를 사용하는 것이 더 안전하고 권장되는 방법
Cloneable
이 몰고 온 모든 문제를 되짚어봤을 때, 새로운 인터페이스를 만들 때는 절대 Cloneable을 확장해서는 안 되며, 새로운 클래스도 이를 구현해서는 안 된다.
새로운 클래스나 인터페이스를 만들 때
Cloneable
을 구현하거나 확장하지 말아라Cloneable
은 많은 문제점을 가지고 있어 사용을 권장하지 않는다.
복제 기능은 복사 생성자나 복사 팩터리 메서드를 사용해라
더 안전하고 유연하며, 이해하기 쉽다.
이들은 코드가 명확하고 이해하기 쉬우며,
final
필드와도 충돌하지 않고, 불필요한 예외 처리나 형변환이 필요하지 않는다.
배열은 예외
배열은
clone()
메서드를 사용하여 복제하는 것이 가장 깔끔하고 효율적
++ native 메서드
native: 메서드는 C나 C++ 같은 네이티브 프로그래밍 언어로 작성한 메서드를 의미한다. Java Native Interface(JNI)라고 부른다. 즉, C나 C++의 코드를 자바에서 불러 사용하려면 native 메서드를 정의해서 메서드 바디를 갖지 않는 메서드를 구현한다. (메서드 바디가 dll (Unix에선 so) 파일로 되어 있어 런타임 시에 dll 파일을 System.loadLibrary 메서드가 수행하여 classpath 경로에서 파라미터의 파일을 메모리에 로딩)
Last updated