깊은 복사와 얕은 복사
얕은 복사란?
원시타입이 아닌 객체와 같은 참조형(reference) 타입의 변수를 그대로 복제를 한다면 값이 복사되는 것이 아닌 주소값이 복사되어 결국 같은 힙의 데이터를 바라보는 꼴이 되어 버린다
public class Main {
public static void main(String[] args) {
Test t1 = new Test(10);
Test t2; // 복사할 변수
t2 = t1; // 얕은 복사
System.out.println(t1 == t2); // 두개의 참조값이 같으므로 true
t1.setValue(20);
System.out.println(t2.getValue() == t1.getValue());// t1의 값을 변경했으므로 t2의 값도 변경됨. true
}
}
@Getter
@Setter
class Test{
private int value;
public Test(int value){
this.value = value;
}
}
- 위처럼 얕은 복사는 최상위 레밸 속성만 복사한다.
- 얕은 복사는 복사본의 속성을 변경하면 원본도 수정된다.(당연히 같은 객체를 바라보기 때문)
깊은 복사란?
- 깊은 복사는 객체의 모든 필드를 재귀적으로 복사하여 새로운 객체를 생성하는 방식이다.
- 이 방식은 객체의 모든 데이터를 복사하기 때문에 원본 객체와 복사된 객체가 독립적으로 존재한다.
깊은 복사, 얕은 복사의 장단점
- 깊은 복사
- 얕은 복사
깊은 복사를 구현하는 다양한 방법
- Object 의 clone() 메서드를 구현 (문제있는 방법)
- 내부 네서드에 깊은 복사를 구현하는 메서드를 제공
Cloneable 인터페이스의 문제점
clone 메서드란?
- 상위 클래스인 Object에 정의된 clone 메서드가 있음
- 해당 메서드는 오버라이딩만 해서는 안됨 Cloneable 인터페이스를 implements 해야 오류없이 사용 가능
- 깊은 복사를 구현하는 메서드라는 오해가 있음
- 일반적인 경우에 사용하기에는 설계적인 결함이 있음
clone 메서드를 사용하는 방법과 복사
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Test t1 = new Test(10);
Test t2; // 복사할 변수
t2 = t1.clone(); // 얕은 복사
System.out.println(t2 != t1); // t2와 t1은 다른 객체
}
}
@Getter
@Setter
class Test implements Cloneable{
private int value;
public Test(int value){
this.value = value;
}
@Override
public Test clone() throws CloneNotSupportedException {
return (Test) super.clone();
}
}
- 위처럼 Cloneable 인터페이스를 implements 를 하고 Object 의 clone 메서드를 구현해야한다.
- Cloneable 인터페이스는 마커 인터페이스로 적용되며 만약 해당 인터페이스를 implements 를 안한다면 CloneNotSupportedException 예외를 반환한다.
- 위 실행 예시처럼 참조값을 복사했을 때와는 다르게 새로운 객체가 생성되어 원본과 복사본이 다른 것을 볼 수 있다.
clone 메서드의 문제
위 예시를 보면 깊은 복사가 된 것 처럼 보이지만 다음 예시처럼 문제점이 있다.
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
var inner = new InnerClass(10);
Test t1 = new Test(inner);
Test t2; // 복사할 변수
t2 = t1.clone();
System.out.println(t2.getValue() == t1.getValue()); // true
t1.getValue().setValue(20);
// clone 으로 복사한 복제 객체의 프로퍼티를 변경하면 원본도 변경됨
System.out.println(t2.getValue().getValue() == 20);// true
}
}
@Getter
@Setter
class Test implements Cloneable{
private InnerClass value;
public Test(InnerClass value){
this.value = value;
}
@Override
public Test clone() throws CloneNotSupportedException {
return (Test) super.clone();
}
}
@Getter
@Setter
class InnerClass{
private int value;
public InnerClass(int value){
this.value = value;
}
}
- 위처럼 clone 을 사용하여 복사했지만 프로퍼티는 값에 의한 복사를 하기 때문에 깊은 복사가 안되었다.
- 따라서 깊은 복사를 원한다면 clone 에 구현을 해야한다.
clone 을 사용하지말자
- 위 예시처럼 그냥 clone 을 사용해서는 깊은 복사가 안되어 따로 구현을 해줘야 한다.
- 하지만 따로 구현을 한다고 하더라도 Object 에 의한 상속 구조 때문에 문제가 될 수 있다. (부모 객체 중에 clone 을 깊은 복사를 구현했을 수도 안했을 수도 있기 때문)
- final 필드와 충돌 : final 필드는 생성자에서만 값을 생성할 수 있는데 clone 메서드는 생성자를 우회하기 때문에 final 필드와 충돌할 수 있다.
- 사용하더라도 원시타입만 사용한다면 크게 문제가 되지 않을 수 있음 하지만 참조 자료형이 들어간다면 깊은 복사를 기대하지 말자
깊은 복사 가이드
깊은 복사 가이드
- 깊은 복사를 구현할 때는 위의 clone 의 문제점을 인식하고 다른 방식으로 구현해야한다.
- clone 은 기본 구현이 얕은 복사이며 Object 의 상속 구조이기 때문에 모호함이 있어서 문제였다. 따라서 깊은 복사 메서드를 명시적으로 따로 구현하면 된다.
// 1. 복사 생성자
public User(User original) {
this.name = original.name;
// 깊은 복사 (Deep Copy)
this.hobbies = new ArrayList<>(original.hobbies);
}
// 2. 복사 팩토리 메서드
public static User newInstance(User original) {
return new User(original); // 내부에서 복사 생성자 호출 혹은 따로 구현해도됨
}
'JAVA' 카테고리의 다른 글
| String, StringBuffer, StringBuilder 알고 사용하자 (1) | 2025.12.16 |
|---|---|
| System.currentTimeMills(), System.nanoTime() 알고 사용하자 (0) | 2025.12.16 |
| 자바의 제네릭 기초부터 deep dive (0) | 2025.11.24 |
| java 에서 equals 와 hashcode 를 재정의 해야하는 이유 (0) | 2025.11.11 |
| [java의 병렬 프로그래밍] complete 할 수 있는 Future -CompletableFuture 의 원리와 사용법 (0) | 2025.07.30 |