들어가기 전에
멘토링 시간에 clone()과 Clonable 인터페이스에 대해 제대로 답변하지 못했던 것이 생각난다. 멘티님과 같이 리마인드하면서 얕은 복사와 깊은 복사까지 짚고 넘어갔었는데 어제 정렬 알고리즘 문제를 풀다가 배열을 복사할 때 간단하게 사용하면서 다시 생각이 났다.
인터넷에 이 키워드로 정리된 글이 많아서 여러 개를 읽다가 java.lang.Object.clone 메소드라는 글을 보게 됐다. Object.clone()이 native 메서드라서 구현 코드가 나와있지 않은데 이 글을 통해서 관련 코드를 살펴볼 수 있었고, 인용하신 Thinking in Java의 내용을 통해 새로 알게되는 내용도 꽤 많았다. 참고로 추가하신 사이트들에서도 다른 개념들을 파악하는 데 도움이 돼서 이 글을 바탕으로 더 알게된 내용들과 기본적으로 Object.clone()에 대해 알아야 하는 내용들을 정리해보려고 한다.
Object.clone()
Object 클래스에서 clone() 메서드를 찾아보면 @HotSpotIntrinsicCandidate 어노테이션을 사용하고 native 메서드로 구현되어있는 것을 알 수 있다. 그리고 CloneNotSupportedException을 throws 하고 있다.
@HotSpotIntrinsicCandidate
protected native Object clone() throws CloneNotSupportedException;
Object.hashCode()도 똑같은 어노테이션을 사용하고 native 메서드로 구현되었다. 그 이유는 JVM이 내부적으로 효율적인 구현 방식을 찾아서 활용할 수 있게 하고, 자바 코드가 아닌 다른 언어로 작성된 코드를 사용함으로써 성능을 더 높이기 위해서였는데 Object.clone()도 마찬가지로 같은 이유에서 이와 같이 작성되었다.
이 코드의 내부 동작을 알기 위해서는 jvm 내부 소스 코드를 살펴봐야 하는데 위에 언급한 블로그 글에서 소스 코드를 보여주시면서 한국어로 직접 주석을 달아주셔서 대략적으로 파악이 가능하다. 물론 모든 코드를 이해할 수는 없었지만 대략적으로 어떤 과정으로 동작하는지 이해하는 데 도움이 됐다.
1. Cloneable 인터페이스를 구현하지 않았다면 CloneNotSupportedException을 반환한다.
2. 원본 객체의 크기를 파악하여 새로운 저장 공간을 생성한다.
3. 값의 원자성을 보장하는 얕은 복사를 진행한다.
참고로 3번에 관해서는 따로 주석이 작성되어있는데 내용은 다음과 같다.
4846409: an oop-copy of objects with long or double fields or arrays of same won't copy the longs/doubles atomically in 32-bit vm's, so we copy jlongs instead of oops.
Java Language Specification 17.7에 따르면 long과 double은 64비트의 값을 가지기 때문에 32비트 vm에서 나눠서 처리할 경우 해당 값에 대한 원자성을 보장하지 못하는 경우가 생긴다고 한다. 그래서 jvm 내부 소스 코드에서는 복사해야 하는 값이 long이거나 double이면 jlong(JNI에서 사용하는 데이터 타입)을 이용해서 처리한다고 하는데 이를 통해서 long과 double의 원자성 문제에 대해 인지하고 처리하는 과정이 있다는 것을 파악하게 됐다.
내용은 조금 어려웠지만 위의 정리를 통해서 알게된 사실이 있다. Cloneable 인터페이스를 구현하지 않으면 예외가 발생한다는 것과 얕은 복사가 이루어진다는 것이다. 예제 코드를 보면서 구현해야 할 내용과 얕은 복사에 대해 알아보자.
clone() 메서드 구현하기
기본적으로 clone() 메서드는 객체를 복제하기 위해 사용한다. 아래와 같이 Cloneable 인터페이스를 구현하면서 CloneNotSupportedException을 처리하고 내부에는 super.clone()으로 Object의 clone() 메서드를 호출하고 있다.
// 출처 : Do it! 자바 프로그래밍 입문
class Circle implements Cloneable {
...
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
여기에서 구현하고 있는 Cloneable인터페이스에는 아무것도 정의되어있지 않다.
public interface Cloneable {
}
이렇게 빈 인터페이스를 Marker Interface 혹은 Tagging Interface 라고 하는데 이 인터페이스를 구현함으로써 구현 클래스에서 clone() 동작을 사용할 수 있음을 명시한다. 더 자세한 설명은 이펙티브 자바의 글을 인용하겠다.
이 인터페이스는 놀랍게도 Object의 protected 메서드인 clone의 동작 방식을 결정한다.
Cloneable을 구현한 클래스의 인스턴스에서 clone을 호출하면 그 객체의 필드들을 하나하나 복사한 객체를 반환하며,
그렇지 않은 클래스의 인스턴스에서 호출하면 CloneNotSupportedException을 던진다.
덧붙여서 Tagging Interface라는 명칭은 이 인터페이스 자체가 이를 구현한 클래스 타입을 나타내는 플래그 역할을 한다는 뜻에서 붙여졌다고 한다.
그렇다면 왜 이렇게 많은 절차를 거치면서 clone() 메서드를 사용하게 만들었을까? 인터페이스 구현, 예외 처리, 메서드 오버라이딩 그리고 호출까지 말이다. Thinking in Java의 설명에 따르면 어떤 객체를 쉽게 복제할 수 있을 때 그에 관련하여 보안 문제가 발생할 수 있는 위험이 있어서 원래의 쉬운 설계에 패치를 더한 결과라고 한다. 쉽게 비유하자면 방지턱을 만든 셈이다.
Example
직접 예제 코드를 작성해보자.
public class Student implements Cloneable {
private String name;
private int age;
...
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
우선 clone() 메서드를 사용하기 위한 구현을 위의 코드와 같이 마쳤다.
CloneNotSupportedException을 처리하기 위해 try-catch문을 사용해서 코드를 작성했다. 결과는 어떻게 될까?
public class Copy {
public static void main(String[] args) {
try {
Student std1 = new Student("김노력", 10);
Student std2 = (Student) std1.clone();
System.out.println(std1.hashCode());
System.out.println(std2.hashCode());
std2.setAge(20);
System.out.println(std1);
System.out.println(std2);
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
}
출력결과
818403870
1531333864
Student{name='김노력', age=10}
Student{name='김노력', age=20}
해시코드 값을 통해 서로 다른 두 객체가 생성된 것과 복사본의 값을 변경해도 원본의 값이 변경되지 않는 것을 확인할 수 있다. 여기서 의아함을 가져야 한다. 분명히 얕은 복사를 한다고 했는데 그렇다면 같은 참조 변수 값을 가지고 있으니까 해시코드 값도 같아야 하고 값이 같이 변경되어야 하는 게 아닌가? 이렇게 서로 다른 두 객체가 생성되어서 값만 복사되고 서로 영향을 미치지 않는 것은 깊은 복사인데 말이다.
Shallow copy(얕은 복사)와 Deep copy(깊은 복사)
clone() 메서드 위의 주석에서는 깊은 복사가 아닌 얕은 복사를 수행한다고 적혀져있다.
Thus, this method performs a "shallow copy" of this object, not a "deep copy" operation.
그런데 왜 위의 코드에서는 깊은 복사가 이루어졌던 걸까? 아래의 스택오버플로우 답변을 보면 기본 동작은 얕은 복사이지만 이 clone() 메서드를 사용하기 위해서 재정의하는 과정을 통해 깊은 복사를 하게 된다는 것을 알 수 있다.
얕은 복사의 예제를 작성해 보겠다.
public class Copy2 {
public static void main(String[] args) {
Student std1 = new Student("김노력", 10);
Student std2 = std1;
System.out.println(std1.hashCode());
System.out.println(std2.hashCode());
std2.setAge(20);
System.out.println(std1);
System.out.println(std2);
}
}
출력결과
818403870
818403870
Student{name='김노력', age=20}
Student{name='김노력', age=20}
두 객체의 해시코드 값이 같고 하나의 값이 변경되면 모든 값이 변경되는 것을 확인할 수 있다.
결론적으로 우리가 Object.clone()을 재정의해서 사용한다는 것은 깊은 복사를 한다는 것이다. 그리고 이렇게 재정의하기 위해서 clone() 메서드도 다른 equals()나 hashCode() 메서드와 같이 규약을 먼저 살펴봐야 한다.
clone() 재정의
"관례상" 이라는 말 뒤에 설명되는 말들은 결국 깊은 복사를 뜻하는데 위에서 clone() 메서드를 재정의함으로써 깊은 복사의 동작을 할 수 있다는 이야기와 일맥상통한 것을 알 수 있다.
이 객체의 복사본을 생성해 반환한다. '복사'의 정확한 뜻은 그 객체를 구현한 클래스에 따라 다를 수 있다. 일반적인 의도는 다음과 같다. 어떤 객체 x에 대해 다음 식은 참이다.
x.clone() != x
또한 다음 식도 참이다.
x.clone().getClass() == x.getClass()
하지만 이상의 요구를 반드시 만족해야 하는 것은 아니다.
한편 다음 식도 일반적으로 참이지만, 역시 필수는 아니다.
x.clone().equals(x)
관례상, 이 메서드가 반환하는 객체는 super.clone을 호출해 얻어야 한다. 이 클래스와(Object를 제외한) 모든 상위 클래스가 이 관례를 따른다면 다음 식은 참이다.
x.clone().getClass() == x.getClass()
관례상, 반환된 객체와 원본 객체는 독립적이어야 한다. 이를 만족하려면 super.clone으로 얻은 객체의 필드 중 하나 이상을 반환 전에 수정해야 할 수도 있다.
출처 : Effective Java
이펙티브 자바에서는 Clonable을 이미 구현한 클래스를 확장하는 경우에만 clone() 메서드를 재정의해서 사용하고 그렇지 않다면 복제를 위한 생성자나 팩토리 방법을 사용하기를 권장하고 있다. 사실상 clone() 메서드는 생성자와 같은 효과를 내기 때문이다. 하지만 배열의 경우에는 clone() 메서드를 사용하는 것이 가장 간결하기 때문에 생성자나 팩토리 보다는 이 메서드를 사용하는 것이 좋겠다.
Copy Constructor (복제 생성자) 예제 코드
// 출처 : https://www.baeldung.com/java-deep-copy
public Address(Address that) {
this(that.getStreet(), that.getCity(), that.getCountry());
}
public User(User that) {
this(that.getFirstName(), that.getLastName(), new Address(that.getAddress()));
}
Copy Factory (복제 팩토리) 예제 코드
// 출처 : 이펙티브 자바
public static User newInstance(User user) { ... }
장점 및 단점
- 장점
- clone() 메서드를 재정의하여 사용하면 길고 반복적인 코드를 줄이고 코드의 재사용성을 높일 수 있다.
- 배열을 복사할 때 가장 빠른 방법이다.
- 단점
- Object.clone()은 얕은 복사만 지원하기 때문에 깊은 복사가 필요하다면 재정의를 해야 한다.
- 깊은 복사를 하기 위해서는 여러 과정을 거쳐야 한다.
- Cloneable 인터페이스를 구현한다.
- clone() 메서드를 오버라이딩한다.
- CloneNotSupportedException을 처리한다.
- 메서드 내부에서 super.clone()을 호출해야 한다.
- 하위 클래스에서 clone() 메서드를 사용하기 위해서는 모든 상위 클래스에서 clone() 메서드를 정의하거나 다른 상위 클래스에서 상속받아야 한다.
면접 예상 질문
- clone() 메서드와 Clonable 인터페이스에 대해 설명해 주세요.
- 얕은 복사와 깊은 복사의 차이점은 무엇인가요?
참고
'Java' 카테고리의 다른 글
[Java] Enum이 철벽치는 방법 (0) | 2024.11.15 |
---|---|
[Java] MessageFormat (0) | 2023.05.27 |
[Java] Object.hashCode()와 Hashing, 그리고 Hash Collision (3) | 2023.02.20 |
[Java] Object.equals() (0) | 2023.02.14 |
[Java] Socket에 관하여 (0) | 2023.02.08 |