Dandy Now!
  • [Python] 객체 복사: 참조 할당, 얕은 복사, 깊은 복사 완벽 이해하기
    2025년 07월 28일 12시 58분 26초에 업로드 된 글입니다.
    작성자: DandyNow
    728x90
    반응형

    파이썬에서 객체 복사: 참조 할당, 얕은 복사, 깊은 복사 완벽 이해하기

    파이썬을 사용하다 보면 리스트나 딕셔너리 같은 객체를 다룰 때 예상치 못한 결과에 당황할 때가 있다. 특히 객체를 복사하는 과정에서 '어라? 원본을 바꿨는데 복사본도 바뀌었네?' 혹은 '복사본을 바꿨는데 원본이 왜 안 바뀌지?' 같은 경험을 해본 적이 있을 것이다. 이는 파이썬의 참조 할당, 얕은 복사, 깊은 복사 개념을 정확히 이해하지 못했기 때문이다.

    이 세 가지 개념을 명료한 예제와 함께 자세히 알아보자.

    1. 참조 할당 (Reference Assignment)

    가장 기본적인 형태의 '복사'처럼 보이지만, 사실은 복사가 아닌 같은 객체를 가리키게 하는 것이다.

    원리와 특징

    • 원리: 한 변수가 가리키는 객체의 메모리 주소 자체를 다른 변수에 할당한다.
    • 결과: 두 변수는 메모리 상의 동일한 하나의 객체를 참조한다.
    • 특징: 한 변수를 통해 객체를 변경하면, 다른 변수도 동일한 객체를 가리키고 있으므로 변경 사항이 그대로 반영된다.

    💡 핵심 포인트: 이는 실제로는 "복사"가 아니라 "별명 만들기"에 가깝다. 마치 한 사람을 "철수"라고도 부르고 "형"이라고도 부르는 것과 같다.

    예제

    original_list = [1, 2, 3]
    assigned_list = original_list  # 참조 할당
    
    print(f"Original ID: {id(original_list)}")
    print(f"Assigned ID: {id(assigned_list)}")
    print(f"두 변수는 동일한 객체인가? {original_list is assigned_list}")
    
    original_list.append(4)  # original_list를 변경
    print(f"original_list 변경 후: {original_list}")
    print(f"assigned_list: {assigned_list}")  # assigned_list도 함께 변경됨

    출력 결과:

    Original ID: 140234567890123
    Assigned ID: 140234567890123
    두 변수는 동일한 객체인가? True
    original_list 변경 후: [1, 2, 3, 4]
    assigned_list: [1, 2, 3, 4]

    2. 얕은 복사 (Shallow Copy)

    원본 객체와는 별개의 새로운 객체를 만들지만, 그 내부의 중첩된 객체(가변 객체)들은 여전히 원본과 공유하는 방식이다.

    원리와 특징

    • 원리: 새로운 객체를 생성하고, 원본 객체의 1단계 하위 요소들에 대한 참조(주소)만 복사한다.
    • 사용 방법:
      • 리스트: new_list = old_list[:], new_list = old_list.copy(), new_list = list(old_list), new_list = [*old_list]
      • 딕셔너리: new_dict = old_dict.copy(), new_dict = dict(old_dict), new_dict = {**old_dict}
      • 범용: copy 모듈의 copy.copy() 함수 사용
    • 결과:
      • 외부 객체: 원본과 복사본은 서로 다른 독립적인 객체이다.
      • 내부 가변 객체: 만약 리스트 안에 리스트(중첩 리스트)처럼 가변 객체가 있다면, 그 가변 객체들은 원본과 복사본이 여전히 동일한 것을 참조한다.

    ⚠️ 주의사항: 얕은 복사는 "1단계 깊이"까지만 새로운 객체를 만든다. 마치 상자는 새로 만들었지만, 상자 안의 작은 상자들은 여전히 원본과 공유하는 것과 같다.

    예제

    import copy
    
    original_nested_list = [1, 2, [3, 4]]
    shallow_copied_list = original_nested_list[:]  # 얕은 복사
    
    print(f"Original ID: {id(original_nested_list)}")
    print(f"Shallow Copied ID: {id(shallow_copied_list)}")
    print(f"두 변수는 동일한 객체인가? {original_nested_list is shallow_copied_list}")
    
    print(f"원본 내부 리스트 ID: {id(original_nested_list[2])}")
    print(f"복사본 내부 리스트 ID: {id(shallow_copied_list[2])}")
    print(f"내부 리스트는 동일한 객체인가? {original_nested_list[2] is shallow_copied_list[2]}")
    
    original_nested_list.append(5)  # 외부 리스트 변경: 서로 영향 없음
    print(f"\n외부 리스트 변경 후 (original): {original_nested_list}")
    print(f"외부 리스트 변경 후 (shallow): {shallow_copied_list}")
    
    original_nested_list[2].append(50)  # 내부 중첩 리스트 변경: 서로 영향 있음
    print(f"\n내부 리스트 변경 후 (original): {original_nested_list}")
    print(f"내부 리스트 변경 후 (shallow): {shallow_copied_list}")

    출력 결과:

    Original ID: 140234567890123
    Shallow Copied ID: 140234567890456
    두 변수는 동일한 객체인가? False
    원본 내부 리스트 ID: 140234567890789
    복사본 내부 리스트 ID: 140234567890789
    내부 리스트는 동일한 객체인가? True
    
    외부 리스트 변경 후 (original): [1, 2, [3, 4], 5]
    외부 리스트 변경 후 (shallow): [1, 2, [3, 4]]
    
    내부 리스트 변경 후 (original): [1, 2, [3, 4, 50], 5]
    내부 리스트 변경 후 (shallow): [1, 2, [3, 4, 50]]

    불변 객체와 얕은 복사

    # 불변 객체가 포함된 경우의 예제
    original = [1, "hello", (2, 3)]
    shallow = original.copy()
    
    print(f"문자열 객체 동일성: {original[1] is shallow[1]}")  # True
    print(f"튜플 객체 동일성: {original[2] is shallow[2]}")    # True
    # 불변 객체는 얕은 복사에서도 참조를 공유하지만, 변경할 수 없으므로 문제없음

    💡 알아두기: 문자열, 숫자, 튜플 등의 불변 객체는 얕은 복사에서 참조를 공유해도 변경할 수 없기 때문에 문제가 되지 않는다. 파이썬은 메모리 효율성을 위해 불변 객체를 공유하는 경우가 많다.

    3. 깊은 복사 (Deep Copy)

    원본 객체뿐만 아니라 그 내부에 있는 모든 중첩된 객체들까지 완전히 새로운 객체로 복사하는 방식이다.

    원리와 특징

    • 원리: 원본 객체와 그 안에 포함된 모든 하위 객체들을 재귀적으로 복사하여 완전히 독립적인 새로운 객체 트리를 생성한다.
    • 사용 방법: copy 모듈의 copy.deepcopy() 함수를 사용해야 한다.
    • 결과: 원본과 복사본은 완전히 독립적이며, 어떤 변경도 서로에게 영향을 주지 않는다.
    • 특징: 복사본의 어떤 부분(외부 객체든 내부 객체든)을 수정해도 원본은 영향을 받지 않는다.

    🔍 성능 고려사항: 깊은 복사는 가장 안전하지만, 복잡한 객체 구조에서는 메모리 사용량과 처리 시간이 크게 증가할 수 있다. 따라서 정말 필요한 경우에만 사용하는 것이 좋다.

    예제

    import copy
    
    original_nested_list = [1, 2, [3, 4]]
    deep_copied_list = copy.deepcopy(original_nested_list)  # 깊은 복사
    
    print(f"Original ID: {id(original_nested_list)}")
    print(f"Deep Copied ID: {id(deep_copied_list)}")
    print(f"두 변수는 동일한 객체인가? {original_nested_list is deep_copied_list}")
    
    print(f"원본 내부 리스트 ID: {id(original_nested_list[2])}")
    print(f"복사본 내부 리스트 ID: {id(deep_copied_list[2])}")
    print(f"내부 리스트는 동일한 객체인가? {original_nested_list[2] is deep_copied_list[2]}")
    
    original_nested_list[2].append(50)  # 내부 중첩 리스트 변경: 서로 영향 없음
    print(f"\n내부 리스트 변경 후 (original): {original_nested_list}")
    print(f"내부 리스트 변경 후 (deep): {deep_copied_list}")

    출력 결과:

    Original ID: 140234567890123
    Deep Copied ID: 140234567890456
    두 변수는 동일한 객체인가? False
    원본 내부 리스트 ID: 140234567890789
    복사본 내부 리스트 ID: 140234567891012
    내부 리스트는 동일한 객체인가? False
    
    내부 리스트 변경 후 (original): [1, 2, [3, 4, 50]]
    내부 리스트 변경 후 (deep): [1, 2, [3, 4]]

    4. 실무에서의 선택 가이드

    언제 어떤 복사를 사용할까?

    상황 권장 방법 이유
    단순한 1차원 리스트/딕셔너리 복사 얕은 복사 성능이 좋고 충분함
    중첩된 가변 객체가 있고, 내부 객체도 독립적으로 수정해야 하는 경우 깊은 복사 완전한 독립성 보장
    설정값이나 템플릿 객체를 기반으로 새 객체 생성 깊은 복사 원본 보호
    단순히 같은 객체를 다른 이름으로 참조 참조 할당 메모리 효율적

    복사 방법별 성능 비교

    import copy
    import time
    
    # 복잡한 중첩 구조 생성
    complex_data = [[i + j for j in range(100)] for i in range(100)]
    
    # 참조 할당
    start = time.time()
    ref_assigned = complex_data
    ref_time = time.time() - start
    
    # 얕은 복사
    start = time.time()
    shallow_copied = complex_data.copy()
    shallow_time = time.time() - start
    
    # 깊은 복사
    start = time.time()
    deep_copied = copy.deepcopy(complex_data)
    deep_time = time.time() - start
    
    print(f"참조 할당: {ref_time:.6f}초")
    print(f"얕은 복사: {shallow_time:.6f}초")  
    print(f"깊은 복사: {deep_time:.6f}초")

    📊 성능 팁: 대부분의 경우 얕은 복사로 충분하며, 깊은 복사는 정말 필요한 경우에만 사용하자. 특히 대용량 데이터를 다룰 때는 복사 방식의 선택이 성능에 큰 영향을 미칠 수 있다.

    5. 결론 및 베스트 프랙티스

    • 참조 할당은 단순히 이름만 추가하는 것이므로, 원본과 복사본이 항상 함께 변경된다.
    • 얕은 복사는 새로운 껍데기를 만들지만, 그 안의 중첩된 가변 객체들은 여전히 원본과 공유한다. 중첩된 리스트나 딕셔너리를 다룰 때 주의해야 한다.
    • 깊은 복사는 모든 것을 완전히 새로 만들어 원본과 복사본을 완벽하게 분리한다. 가장 안전한 복사 방법이지만, 객체 구조가 복잡할수록 메모리와 시간 소모가 커질 수 있으니 필요할 때만 사용하는 것이 좋다.

    🎯 실무 권장사항

    1. 기본적으로는 얕은 복사를 사용하고, 문제가 생기면 깊은 복사를 고려하자.
    2. 불변 객체(문자열, 튜플, 숫자)만 포함된 구조라면 얕은 복사와 깊은 복사의 결과가 동일하므로 얕은 복사를 사용하자.
    3. API 응답이나 설정 데이터처럼 원본을 보호해야 하는 경우에는 깊은 복사를 사용하자.
    4. 디버깅할 때는 id() 함수와 is 연산자를 활용해 객체의 동일성을 확인하자.

    각 상황의 필요에 따라 어떤 방식의 복사가 필요한지 정확히 이해하고 사용하는 것이 파이썬 코드를 작성할 때 오류를 줄이고 효율성을 높이는 중요한 방법이다.


    728x90
    반응형
    댓글