얉은 복사 VS 깊은 복사 (Python, Java)
얕은 복사와 깊은 복사
얕은 복사(Shallow Copy)는 객체의 참조값(주소값)만을 복사하는 방식이고, 깊은 복사(Deep Copy)는 객체의 실제 값을 새로운 메모리 공간에 복사하는 방식이다.
얕은 복사 (Shallow Copy)
얕은 복사는 객체의 최상위 속성만 복사하고, 중첩된 객체나 배열은 원본과 같은 참조를 공유한다. 이는 메모리를 절약하고 빠른 복사가 가능하지만, 중첩된 객체를 수정하면 원본에도 영향을 미치는 특징이 있다.
얕은 복사의 특징
- 메모리 효율성: 주소값만 복사하므로 메모리 사용량이 적다.
- 빠른 속도: 참조만 복사하므로 복사 과정이 빠르다.
- 참조 공유: 중첩된 객체를 원본과 복사본이 공유한다.
깊은 복사 (Deep Copy)
깊은 복사는 객체의 모든 레벨의 중첩된 속성까지 완전히 새로운 메모리 공간에 복사하여 원본과 완전히 독립적인 객체를 생성한다. 이를 통해 원본 객체와 복사된 객체가 서로 영향을 주지 않는다.
깊은 복사의 특징
- 완전한 독립성: 원본과 복사본이 완전히 독립적이다.
- 메모리 사용량 증가: 모든 속성을 새로 생성하므로 메모리를 많이 사용한다.
- 복사 속도 저하: 재귀적으로 모든 속성을 복사하므로 상대적으로 느리다.
Python 예시
얉은 복사
# * 연산자는 내부 리스트의 참조를 복사한다 (얕은 복사)
matrix_shallow = [[]] * 3
print(f"초기 상태: {matrix_shallow}")
# id() 함수로 각 행의 메모리 주소를 확인
print(f"각 행의 메모리 주소: id(matrix_shallow[0])={id(matrix_shallow[0])}, id(matrix_shallow[1])={id(matrix_shallow[1])}, id(matrix_shallow[2])={id(matrix_shallow[2])}")
# 첫 번째 행의 내용을 변경하면, 공유된 리스트 자체가 변경된다.
print("\\n>> 첫 번째 행에 'A' 추가...")
matrix_shallow[0].append('A')
print("\\n--- 변경 후 상태 ---")
print(f"결과: {matrix_shallow}")
결과
초기 상태: [[], [], []]
각 행의 메모리 주소: id(matrix_shallow[0])=440...880, id(matrix_shallow[1])=440...880, id(matrix_shallow[2])=440...880
(모든 주소가 동일)
>> 첫 번째 행에 'A' 추가...
--- 변경 후 상태 ---
결과: [['A'], ['A'], ['A']]
* 연산자를 사용하여 [[]] * 3과 같이 리스트를 생성하면, 파이썬은 내부 리스트 []를 새로 3개 만드는 것이 아니라, 이미 존재하는 단 하나의 [] 리스트에 대한 참조(메모리 주소)를 3번 복사하여 새로운 리스트에 담는다.
따라서 겉으로는 3개의 행을 가진 리스트처럼 보이지만, 실제로는 모든 행이 메모리상에서 단 하나의 동일한 리스트 객체를 가리키고 있는 상태가 된다.
깊은 복사
# 리스트 컴프리헨션은 매번 새로운 객체를 생성한다.
matrix_deep = [[] for _ in range(3)]
print(f"초기 상태: {matrix_deep}")
print(f"각 행의 메모리 주소: id(matrix_deep[0])={id(matrix_deep[0])}, id(matrix_deep[1])={id(matrix_deep[1])}, id(matrix_deep[2])={id(matrix_deep[2])}")
# 첫 번째 행만 독립적으로 변경된다.
print("\\n>> 첫 번째 행에 'A' 추가...")
matrix_deep[0].append('A')
print("\\n--- 변경 후 상태 ---")
print(f"결과: {matrix_deep}")
결과
초기 상태: [[], [], []]
각 행의 메모리 주소: id(matrix_deep[0])=440...368, id(matrix_deep[1])=440...624, id(matrix_deep[2])=440...144
(모든 주소가 다름)
>> 첫 번째 행에 'A' 추가...
--- 변경 후 상태 ---
결과: [['A'], [], []]
2차원 리스트를 만들 때 각 행을 독립적인 객체로 만들고 싶다면 리스트 컴프리헨션을 사용할 수 있다. 이 방식은 루프를 실행할 때마다 [] 표현식을 새롭게 해석하여 매번 새로운 리스트 객체를 생성한다.
Java 예시
자바에서는 파이썬의 [[]]*3과 같이 곱셈 연산자로 객체를 복제하는 문법은 없지만, 객체를 다른 변수에 할당하거나 메서드를 통해 복사할 때 동일한 원리가 적용된다.
자바에서의 복사 기본
자바에서 객체 변수는 객체 자체가 아닌, 객체가 저장된 메모리 주소(참조)를 가리킨다.
// 이것은 복사가 아니라, 같은 객체를 두 변수가 가리키는 것
Student studentA = new Student();
Student studentB = studentA; // studentB는 studentA와 완전히 동일한 객체를 참조
객체의 복사본을 만들려면 clone() 메서드를 사용하는 것이 일반적이다.
얉은 복사
// 과목 클래스
class Course {
String subject;
public Course(String subject) {
this.subject = subject;
}
}
// 학생 클래스 (Cloneable 인터페이스 구현)
class Student implements Cloneable {
int id;
Course course; // 참조 타입 필드
public Student(int id, Course course) {
this.id = id;
this.course = course;
}
// 얕은 복사를 수행하는 clone() 메서드
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // Object 클래스의 기본 clone()은 얕은 복사를 수행
}
다음과 같이 과목 클래스와 학생 클래스를 정의하고 clone 메소드를 이용해 얉은 복사를 적용해보면
public class ShallowCopyExample {
public static void main(String[] args) throws CloneNotSupportedException {
// 원본 객체 생성
Course originalCourse = new Course("수학");
Student originalStudent = new Student(1, originalCourse);
// 얕은 복사 수행
Student clonedStudent = (Student) originalStudent.clone();
System.out.println("--- 복사 직후 ---");
System.out.println("원본 학생의 과목: " + originalStudent.course.subject);
System.out.println("복제 학생의 과목: " + clonedStudent.course.subject);
// 복제된 객체의 '내부 객체'를 변경
clonedStudent.course.subject = "과학";
System.out.println("\\n--- 복제 객체의 내부 변경 후 ---");
System.out.println("원본 학생의 과목: " + originalStudent.course.subject); // <- 원본도 변경됨!
System.out.println("복제 학생의 과목: " + clonedStudent.course.subject);
}
}
결과
--- 복사 직후 ---
원본 학생의 과목: 수학
복제 학생의 과목: 수학
--- 복제 객체의 내부 변경 후 ---
원본 학생의 과목: 과학
복제 학생의 과목: 과학
originalStudent의 과목까지 변경이 되는 것을 볼 수 있다.
Student 객체 자체는 복사되었지만, 그 안의 Course 객체는 주소값만 복사되어 둘이 같은 객체를 공유하기 때문이다.
깊은 복사
내부의 참조 객체까지 복사하도록 클래스를 수정한 후에 다시 테스트해보겠다.
class Course implements Cloneable {
String subject;
public Course(String subject) {
this.subject = subject;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Student implements Cloneable {
int id;
Course course;
public Student(int id, Course course) {
this.id = id;
this.course = course;
}
// 깊은 복사를 수행하도록 재정의된 clone() 메서드
@Override
protected Object clone() throws CloneNotSupportedException {
// 1. 먼저 자신을 얕은 복사한다.
Student clonedStudent = (Student) super.clone();
// 2. 내부의 참조 객체를 명시적으로 복사한다.
clonedStudent.course = (Course) this.course.clone();
return clonedStudent;
}
}
public class DeepCopyExample {
public static void main(String[] args) throws CloneNotSupportedException {
// 원본 객체 생성
Course originalCourse = new Course("수학");
Student originalStudent = new Student(1, originalCourse);
// 깊은 복사 수행
Student clonedStudent = (Student) originalStudent.clone();
System.out.println("--- 복사 직후 ---");
System.out.println("원본 학생의 과목: " + originalStudent.course.subject);
System.out.println("복제 학생의 과목: " + clonedStudent.course.subject);
// 복제된 객체의 '내부 객체'를 변경
clonedStudent.course.subject = "과학";
System.out.println("\\n--- 복제 객체의 내부 변경 후 ---");
System.out.println("원본 학생의 과목: " + originalStudent.course.subject); // <- 원본은 그대로!
System.out.println("복제 학생의 과목: " + clonedStudent.course.subject);
}
}
이제 Student객체의 course를 다시 변경해보면
결과
--- 복사 직후 ---
원본 학생의 과목: 수학
복제 학생의 과목: 수학
--- 복제 객체의 내부 변경 후 ---
원본 학생의 과목: 수학
복제 학생의 과목: 과학
복제 학생만 변경이 된다.