2025. 3. 25. 06:58ㆍProgramming Languages/Python
파이썬의 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)
파이썬에서 객체를 복사할 때 이해해야 할 중요한 개념이 얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)입니다. 이 두 가지 복사 방식의 차이를 이해하지 못하면 예상치 못한 버그가 발생할 수 있습니다.
1. 객체와 참조의 기본 개념
파이썬에서 변수는 실제 데이터를 직접 저장하지 않고, 데이터가 저장된 메모리 위치를 가리키는 참조(reference)를 저장합니다. 이를 이해하기 위한 간단한 예를 살펴보겠습니다.
a = [1, 2, 3] # 리스트 생성
b = a # b는 a와 같은 리스트를 가리킴
# b를 통해 리스트를 수정하면 a도 영향을 받음
b[0] = 99
print(a) # [99, 2, 3]
print(b) # [99, 2, 3]
# id() 함수로 객체의 메모리 주소 확인
print(id(a)) # 예: 140230416810696
print(id(b)) # 예: 140230416810696 (a와 동일)
위 예제에서 a와 b는 동일한 리스트 객체를 가리키고 있기 때문에, b를 통해 리스트를 수정하면 a를 통해 볼 때도 변경이 적용됩니다. 이것은 단순한 대입 연산자(=)가 객체 자체를 복사하는 것이 아니라, 객체에 대한 참조만 복사하기 때문입니다.
2. 얕은 복사(Shallow Copy)
얕은 복사는 객체 자체의 새로운 복사본을 만들지만, 그 객체가 참조하는 내부 객체는 복사하지 않습니다. 즉, 최상위 컨테이너만 복사하고 내부 요소는 원본과 복사본이 공유합니다.
얕은 복사 방법들
파이썬에서는 얕은 복사를 수행하는 여러 방법이 있습니다:
리스트의 copy() 메서드 사용:
original = [1, 2, 3]
copied = original.copy()
list() 생성자 사용:
original = [1, 2, 3]
copied = list(original)
copy 모듈의 copy() 함수 사용:
import copy
original = [1, 2, 3]
copied = copy.copy(original)
얕은 복사의 특징 (간단한 객체)
간단한 객체(내부에 다른 컨테이너 객체가 없는 경우)에서는 얕은 복사가 예상대로 작동합니다:
original = [1, 2, 3]
copied = original.copy()
# copied를 수정해도 original은 변하지 않음
copied[0] = 99
print(original) # [1, 2, 3]
print(copied) # [99, 2, 3]
# 각 리스트의 메모리 주소는 다름
print(id(original)) # 예: 140230416810696
print(id(copied)) # 예: 140230416810920 (다른 주소)
얕은 복사의 한계 (중첩된 객체)
얕은 복사의 한계는 객체 내부에 다른 컨테이너 객체(리스트, 딕셔너리 등)가 있을 때 분명하게 드러납니다
original = [1, 2, [3, 4]]
copied = original.copy()
# 내부 리스트의 메모리 주소는 같음
print(id(original[2])) # 예: 140230416811144
print(id(copied[2])) # 예: 140230416811144 (같은 주소)
# 내부 리스트를 수정하면 두 리스트 모두 영향 받음
copied[2][0] = 99
print(original) # [1, 2, [99, 4]]
print(copied) # [1, 2, [99, 4]]
위 예제에서 original과 copied는 서로 다른 리스트 객체이지만, 내부에 있는 [3, 4] 리스트는 동일한 객체를 참조합니다. 따라서 copied[2][0]을 수정하면 original[2][0]도 변경됩니다. 이것이 얕은 복사의 중요한 특징입니다.
다음 그림으로 생각해보면 이해가 쉽습니다:
얕은 복사 전:
original = [1, 2, [3, 4]]
|
v
[3, 4] (메모리 주소: 0x123)
얕은 복사 후:
original = [1, 2, [3, 4]]
|
v
[3, 4] (메모리 주소: 0x123)
^
|
copied = [1, 2, [3, 4]]
3. 깊은 복사(Deep Copy)
깊은 복사는 객체와 그 객체가 참조하는 모든 내부 객체까지 재귀적으로 복사합니다. 즉, 원본 객체와 그 내부의 모든 객체들이 완전히 새로운 객체로 복사됩니다.
깊은 복사 수행 방법
파이썬에서 깊은 복사를 수행하려면 copy 모듈의 deepcopy() 함수를 사용해야 합니다:
import copy
original = [1, 2, [3, 4]]
deep_copied = copy.deepcopy(original)
# 내부 리스트의 메모리 주소가 다름
print(id(original[2])) # 예: 140230416811144
print(id(deep_copied[2])) # 예: 140230416811368 (다른 주소)
# 내부 리스트를 수정해도 서로 영향 없음
deep_copied[2][0] = 99
print(original) # [1, 2, [3, 4]]
print(deep_copied) # [1, 2, [99, 4]]
위 예제에서 deep_copied는 original의 완전한 복사본으로, 내부 리스트도 새로 복사되었습니다. 따라서 deep_copied[2][0]을 수정해도 original[2][0]은 변하지 않습니다.
다음 그림으로 생각해보면 이해가 쉽습니다:
4. 복잡한 예제로 알아보는 얕은 복사와 깊은 복사
더 복잡한 중첩 구조에서 얕은 복사와 깊은 복사의 차이를 살펴보겠습니다
import copy
# 복잡한 중첩 구조
original = {
'a': [1, 2, 3],
'b': {
'c': 4,
'd': [5, 6]
}
}
# 얕은 복사
shallow_copy = copy.copy(original)
# 깊은 복사
deep_copy = copy.deepcopy(original)
# 얕은 복사의 내부 객체 수정
shallow_copy['a'][0] = 99
shallow_copy['b']['d'][0] = 88
# 깊은 복사의 내부 객체 수정
deep_copy['a'][1] = 77
deep_copy['b']['d'][1] = 66
print("원본:", original)
# 원본: {'a': [99, 2, 3], 'b': {'c': 4, 'd': [88, 6]}}
# 얕은 복사로 인해 original의 내부 객체들이 수정됨
print("얕은 복사:", shallow_copy)
# 얕은 복사: {'a': [99, 2, 3], 'b': {'c': 4, 'd': [88, 6]}}
print("깊은 복사:", deep_copy)
# 깊은 복사: {'a': [1, 77, 3], 'b': {'c': 4, 'd': [5, 66]}}
# 깊은 복사는 원본에 영향을 주지 않음
위 예제에서 얕은 복사 후 shallow_copy의 내부 객체를 수정하면 original도 함께 변경됩니다. 반면에 깊은 복사 후 deep_copy의 내부 객체를 수정해도 original은 변경되지 않습니다.
5. 얕은 복사와 깊은 복사의 성능 비교
복사 방식 선택 시 고려해야 할 중요한 측면 중 하나는 성능입니다:
- 얕은 복사는 최상위 객체만 복사하므로 더 빠르고 메모리를 적게 사용합니다.
- 깊은 복사는 모든 중첩 객체를 복사하므로 더 느리고 메모리를 더 많이 사용합니다.
특히 대용량 데이터나 깊게 중첩된 구조를 다룰 때는 이러한 성능 차이가 중요합니다.
import copy
import time
# 큰 중첩 리스트 생성
large_list = [[i for i in range(1000)] for _ in range(1000)]
# 얕은 복사 시간 측정
start_time = time.time()
shallow = copy.copy(large_list)
shallow_time = time.time() - start_time
# 깊은 복사 시간 측정
start_time = time.time()
deep = copy.deepcopy(large_list)
deep_time = time.time() - start_time
print(f"얕은 복사 시간: {shallow_time:.6f}초")
print(f"깊은 복사 시간: {deep_time:.6f}초")
print(f"깊은 복사는 얕은 복사보다 약 {deep_time/shallow_time:.1f}배 느립니다.")
6. 언제 얕은 복사를 사용하고, 언제 깊은 복사를 사용해야 할까?
얕은 복사가 적합한 경우:
- 최상위 컨테이너만 독립적으로 수정할 필요가 있을 때
- 내부 객체는 공유해도 문제없을 때
- 성능과 메모리 효율성이 중요할 때
- 내부 객체가 불변(immutable) 객체일 때(문자열, 숫자, 튜플 등)
깊은 복사가 적합한 경우:
- 원본과 완전히 독립적인 복사본이 필요할 때
- 중첩된 가변(mutable) 객체를 포함하고 있고, 원본에 영향을 주지 않고 수정해야 할 때
- 데이터 무결성이 성능보다 중요할 때
7. 자주 발생하는 실수와 주의사항
1. 불변 객체의 복사
불변(immutable) 객체(예: 정수, 문자열, 튜플 등)는 복사 방식을 고려할 필요가 없습니다. 왜냐하면 이들은 변경할 수 없기 때문입니다.
# 튜플은 불변 객체
original_tuple = (1, 2, 3)
copied_tuple = original_tuple # 단순 참조 복사도 안전
# 하지만 내부에 가변 객체가 있다면 주의해야 함
tuple_with_list = (1, [2, 3], 4)
copied_tuple = tuple_with_list # 내부 리스트는 공유됨
2. 얕은 복사와 단순 참조 복사의 혼동
초보자들이 자주 실수하는 부분은 얕은 복사와 단순 참조 복사를 혼동하는 것입니다.
# 단순 참조 복사 (복사가 아님)
list1 = [1, 2, 3]
list2 = list1 # 참조만 복사
# 얕은 복사 (최상위 객체는 새로 생성)
list3 = list1.copy()
3. 복사 후 원본 변경 추적 문제
깊은 복사를 한 후에는 원본과 복사본 간의 연결이 완전히 끊어집니다. 이는 원본 변경 사항을 추적하지 못하게 됩니다.
import copy
original = {'name': 'Alice', 'scores': [85, 90, 92]}
deep_copy = copy.deepcopy(original)
# 원본 변경
original['scores'].append(88)
# 복사본에는 반영되지 않음
print(deep_copy['scores']) # [85, 90, 92] (변경 사항 없음)
8. 실제 프로젝트에서의 활용 예시
1. 데이터 처리 파이프라인에서의 활용
데이터 처리 과정에서 원본 데이터 보존이 필요할 때:
import copy
def process_data(data):
# 원본 데이터를 변경하지 않고 작업하기 위해 깊은 복사
working_copy = copy.deepcopy(data)
# 데이터 변환 작업
for item in working_copy:
item['processed'] = True
# ... 다른 처리 작업
return working_copy
original_data = [{'id': 1, 'values': [10, 20]}, {'id': 2, 'values': [30, 40]}]
processed_data = process_data(original_data)
# 원본 데이터는 그대로 유지
print(original_data) # [{'id': 1, 'values': [10, 20]}, {'id': 2, 'values': [30, 40]}]
2. 게임 상태 관리
게임에서 되돌리기(undo) 기능 구현 시:
import copy
class GameState:
def __init__(self):
self.player_positions = {'player1': [0, 0], 'player2': [10, 10]}
self.scores = {'player1': 0, 'player2': 0}
self.items = {'gold': 100, 'silver': 200}
self.history = []
def save_state(self):
# 현재 상태의 깊은 복사본을 히스토리에 저장
self.history.append(copy.deepcopy({
'positions': self.player_positions,
'scores': self.scores,
'items': self.items
}))
def move_player(self, player, x, y):
self.save_state() # 이동 전 상태 저장
self.player_positions[player][0] += x
self.player_positions[player][1] += y
def undo(self):
if self.history:
last_state = self.history.pop()
self.player_positions = last_state['positions']
self.scores = last_state['scores']
self.items = last_state['items']
return True
return False
요약
- 얕은 복사(Shallow Copy)는 최상위 컨테이너만 복사하고 내부 객체는 원본과 공유합니다.
- 깊은 복사(Deep Copy)는 객체와 그 내부의 모든 객체들을 재귀적으로 복사합니다.
- 얕은 복사는 copy() 메서드, 슬라이싱, list() 생성자, copy.copy() 함수로 수행할 수 있습니다.
- 깊은 복사는 copy.deepcopy() 함수를 사용해야 합니다.
- 얕은 복사는 빠르지만 내부 객체가 공유되어 예상치 못한 결과를 낼 수 있습니다.
- 깊은 복사는 완전한 독립성을 보장하지만 더 많은 시간과 메모리를 사용합니다.
- 프로젝트의 요구사항과 데이터 구조에 따라 적절한 복사 방식을 선택해야 합니다.
'Programming Languages > Python' 카테고리의 다른 글
| 시퀀스 타입의 공통 연산 (0) | 2025.03.25 |
|---|---|
| 튜플 (Tuples) (0) | 2025.03.25 |
| 리스트 (Lists) (0) | 2025.03.25 |
| 중첩 반복문 (Nested Loops) (0) | 2025.03.25 |
| 반복문 제어 및 활용 (0) | 2025.03.21 |