Deep Copy & Shallow Copy : 깊은 복사 & 얕은 복사

  • 얕은 복사(shallow copy)와 깊은 복사(deep copy)는 객체 복사의 방식에 따라 차이가 있다.

Python에서의 얕은 복사와 깊은 복사 #

얕은 복사 (Shallow Copy) #

  • Python에서 얕은 복사는 객체를 복사할 때, 원본 객체가 참조하는 객체들(즉, 리스트나 딕셔너리와 같은 복합 객체들)은 복사하지 않고 원본 객체의 참조만 복사하는 방식이다.
  • 즉, 새로운 객체가 생성되지만, 그 객체가 참조하는 내용은 여전히 원본 객체와 동일하다.
    • list.copy() 또는 copy.copy()를 사용하여 얕은 복사를 만들 수 있다.
    • 얕은 복사에서 복사된 객체는 독립적인 객체이지만, 그 객체 내부의 참조된 요소들은 여전히 원본 객체의 요소들과 연결되어 있다.
 1import copy
 2
 3# 원본 리스트
 4original = [1, 2, [3, 4]]
 5
 6# 얕은 복사
 7shallow_copy = copy.copy(original)
 8
 9# 복사된 객체 수정
10shallow_copy[2][0] = 999
11
12print("원본 리스트:", original)  # 원본 리스트가 수정됨
13print("얕은 복사 리스트:", shallow_copy)  # 복사된 리스트도 수정됨
원본 리스트: [1, 2, [999, 4]]
얕은 복사 리스트: [1, 2, [999, 4]]
  • 얕은 복사를 사용하면, 내부 리스트 **[3, 4]**는 복사되지 않고, 원본 객체와 복사된 객체 모두 같은 참조를 가진다.

깊은 복사 (Deep Copy) #

  • Python에서 깊은 복사는 객체와 그 객체가 참조하는 모든 객체들까지 재귀적으로 복사하여, 완전히 독립적인 객체를 생성하는 방식이다.
  • 즉, 원본 객체와 복사된 객체는 완전히 별개이며, 복사된 객체는 원본 객체와의 참조 관계가 없다.
    • copy.deepcopy()를 사용하여 깊은 복사를 만들 수 있다.
 1import copy
 2
 3# 원본 리스트
 4original = [1, 2, [3, 4]]
 5
 6# 깊은 복사
 7deep_copy = copy.deepcopy(original)
 8
 9# 복사된 객체 수정
10deep_copy[2][0] = 999
11
12print("원본 리스트:", original)  # 원본 리스트는 수정되지 않음
13print("깊은 복사 리스트:", deep_copy)  # 복사된 리스트만 수정됨
원본 리스트: [1, 2, [3, 4]]
깊은 복사 리스트: [1, 2, [999, 4]]
  • 깊은 복사를 사용하면, 내부 객체도 복사되므로 원본 객체와 복사된 객체는 완전히 독립적이다.

Java에서의 얕은 복사와 깊은 복사 #

얕은 복사 (Shallow Copy) #

  • Java에서 얕은 복사는 객체를 복사할 때, 객체의 “참조"만 복사하고, 객체 내부의 참조된 객체들은 복사하지 않고 원본 객체의 참조를 그대로 공유하는 방식이다.
    • Object.clone() 메서드를 사용하여 얕은 복사를 할 수 있습니다. 단, clone() 메서드를 사용하려면 Cloneable 인터페이스를 구현해야 한다.
 1class Person implements Cloneable {
 2  String name;
 3  int age;
 4
 5  Person(String name, int age) {
 6    this.name = name;
 7    this.age = age;
 8  }
 9
10  @Override
11  public Person clone() throws CloneNotSupportedException {
12    return (Person) super.clone(); // 얕은 복사
13  }
14
15  @Override
16  public String toString() {
17    return name + ", " + age;
18  }
19}
20
21public class Main {
22  public static void main(String[] args) throws CloneNotSupportedException {
23    Person person1 = new Person("John", 25);
24    Person person2 = person1.clone();  // 얕은 복사
25
26    person2.name = "Jane"; // 이름을 변경
27
28    System.out.println("person1: " + person1);
29    System.out.println("person2: " + person2);
30  }
31}
person1: John, 25
person2: Jane, 25
  • 이 경우, person1과 person2는 Person 객체가 얕게 복사되었고, name 필드는 별도로 수정된다.
  • 그러나 만약 Person 객체가 다른 객체를 필드로 가지고 있었다면, 그 필드는 참조만 복사된다.

깊은 복사 (Deep Copy) #

  • Java에서 깊은 복사는 객체와 그 객체가 참조하는 모든 객체들을 재귀적으로 복사하여 독립적인 객체를 만드는 방식이다.
  • 복사하려는 객체가 참조하는 모든 객체들을 별도로 복사해야 하므로, 수동으로 깊은 복사를 구현해야 한다.
 1class Person implements Cloneable {
 2  String name;
 3  int age;
 4  Address address;
 5
 6  Person(String name, int age, Address address) {
 7    this.name = name;
 8    this.age = age;
 9    this.address = address;
10  }
11
12  @Override
13  public Person clone() throws CloneNotSupportedException {
14    // 깊은 복사: Address도 복사
15    Person cloned = (Person) super.clone();
16    cloned.address = new Address(address.street, address.city); // 새 객체 생성
17    return cloned;
18  }
19}
20
21class Address {
22  String street;
23  String city;
24
25  Address(String street, String city) {
26    this.street = street;
27    this.city = city;
28  }
29}
30
31public class Main {
32  public static void main(String[] args) throws CloneNotSupportedException {
33    Address address = new Address("123 Main St", "Springfield");
34    Person person1 = new Person("John", 25, address);
35    Person person2 = person1.clone(); // 깊은 복사
36
37    person2.address.street = "456 Elm St"; // 주소를 변경
38
39    System.out.println("person1 address: " + person1.address.street); // 변경되지 않음
40    System.out.println("person2 address: " + person2.address.street); // 변경됨
41  }
42}
person1 address: 123 Main St
person2 address: 456 Elm St
  • 깊은 복사를 위해 Address 객체도 새로 생성하여 복사하였기 때문에, person1과 person2는 독립적인 Address 객체를 가지고 있다.

C++에서의 얕은 복사와 깊은 복사 #

얕은 복사 (Shallow Copy) #

  • C++에서 얕은 복사는 객체를 복사할 때, 객체의 멤버 변수나 포인터는 복사하지만, 포인터가 가리키는 데이터는 복사하지 않는 방식이다.
  • 기본적으로 얕은 복사는 컴파일러가 제공하는 복사 생성자를 사용하여 수행된다.
 1#include <iostream>
 2using namespace std;
 3
 4class Person {
 5public:
 6  string name;
 7  int age;
 8  Person(string n, int a) : name(n), age(a) {}
 9
10  // 복사 생성자 (얕은 복사)
11  Person(const Person& p) {
12    name = p.name;
13    age = p.age;
14  }
15};
16
17int main() {
18  Person person1("John", 25);
19  Person person2 = person1;  // 얕은 복사
20
21  cout << "person1: " << person1.name << ", " << person1.age << endl;
22  cout << "person2: " << person2.name << ", " << person2.age << endl;
23
24  return 0;
25}
person1: John, 25
person2: John, 25
  • C++에서 기본적으로 제공하는 얕은 복사는 객체의 값만 복사하며, 포인터나 동적 할당된 메모리와 같은 복잡한 데이터를 처리하지 않는다.

깊은 복사 (Deep Copy) #

  • C++에서 깊은 복사는 객체가 참조하는 동적 메모리나 자원을 새로 할당하고 복사하는 방식이다.
  • 깊은 복사를 위해서는 복사 생성자나 복사 대입 연산자를 수동으로 구현해야 한다.
 1#include <iostream>
 2using namespace std;
 3
 4class Person {
 5public:
 6  string name;
 7  int age;
 8  int* data;
 9
10  Person(string n, int a) : name(n), age(a) {
11    data = new int(100); // 동적 메모리 할당
12  }
13
14  // 복사 생성자 (깊은 복사)
15  Person(const Person& p)

프로토 타입 패턴 #

  • 객체 생성 패턴 중 하나로, 객체의 복제를 통해 새로운 객체를 생성하는 방법이다.
  • 즉, 객체를 “클론(clone)“해서 새 객체를 만드는 방식으로, 기존 객체를 복제하여 새로운 객체를 만들어낸다.
  • 이는 객체 생성 비용을 줄이고, 복잡한 객체를 효율적으로 생성할 수 있는 장점이 있다.

프로토타입 패턴의 특징 #

  • 복제(Cloning): 객체를 복제하여 새로 만들기 때문에, 복잡한 객체나 자원을 새로 생성하는 비용을 줄일 수 있다.
  • 새로운 인스턴스를 생성하는 데 필요한 정보를 프로토타입 객체에서 얻어온다.
    • 즉, 기존 객체를 기반으로 새로운 객체를 만들어내는 방식이다.
  • 객체 생성 시점에서 어떤 구체적인 클래스를 사용해야 하는지 모를 때 유용하다.
    • 기존 객체를 복사하여 새로운 객체를 생성할 수 있기 때문에, 특정 클래스에 종속되지 않고도 객체를 동적으로 생성할 수 있다.

프로토타입 패턴의 구조 #

  • 프로토타입 패턴은 주로 다음과 같은 구성 요소로 이루어진다:
    • Prototype (프로토타입): 이 인터페이스는 clone() 메서드를 정의합니다. 이 메서드는 객체를 복제할 수 있도록 한다.
    • ConcretePrototype (구체적인 프로토타입): Prototype을 구현한 실제 객체이다. clone() 메서드를 구현하여 객체를 복사하는 방법을 정의한다.
    • Client (클라이언트): 복제된 객체를 필요로 하는 클라이언트로, Prototype 객체를 통해 복제된 객체를 사용한다.

Python에서 프로토타입 패턴 구현 #

  • 얕은 복사

     1import copy
     2
     3# Prototype 인터페이스
     4class Prototype:
     5  def clone(self):
     6    raise NotImplementedError("Subclass must implement abstract method.")
     7
     8# ConcretePrototype (구체적인 프로토타입)
     9class ConcretePrototype(Prototype):
    10  def __init__(self, value):
    11    self.value = value
    12
    13    def clone(self):
    14      return copy.copy(self)  # 얕은 복사를 사용하여 복제
    15
    16    def __str__(self):
    17      return f"ConcretePrototype with value {self.value}"
    18
    19# 클라이언트 코드
    20if __name__ == "__main__":
    21  prototype = ConcretePrototype("Original Object")  # 원본 객체
    22  clone = prototype.clone()  # 복제된 객체
    23
    24  print("Original:", prototype)
    25  print("Clone:", clone)
    26
    27  # 복제 후 값 변경
    28  clone.value = "Cloned Object"
    29  print("After modification - Original:", prototype)
    30  print("After modification - Clone:", clone)
    Original: ConcretePrototype with value Original Object
    Clone: ConcretePrototype with value Original Object
    After modification - Original: ConcretePrototype with value Original Object
    After modification - Clone: ConcretePrototype with value Cloned Object
    • Prototype 클래스는 clone() 메서드를 선언하지만 구현하지 않으며, 실제 복제 작업은 이 클래스를 상속받은 ConcretePrototype 클래스에서 구현한다.
    • ConcretePrototype 클래스에서 clone() 메서드는 copy.copy(self)를 사용하여 객체를 복제한다. 이는 얕은 복사를 사용한 복제이다.
    • 클라이언트는 ConcretePrototype 객체를 만들고, clone() 메서드를 통해 새로운 객체를 생성할 수 있다. 이후 복제된 객체를 변경하더라도 원본 객체는 영향을 받지 않는다.
  • 깊은 복사

     1import copy
     2
     3class Prototype:
     4  def clone(self):
     5    raise NotImplementedError("Subclass must implement abstract method.")
     6
     7class ConcretePrototype(Prototype):
     8  def __init__(self, value, data):
     9    self.value = value
    10    self.data = data
    11
    12  def clone(self):
    13    return copy.deepcopy(self)  # 깊은 복사를 사용하여 복제
    14
    15  def __str__(self):
    16    return f"ConcretePrototype with value {self.value} and data {self.data}"
    17
    18if __name__ == "__main__":
    19  original = ConcretePrototype("Original", [1, 2, 3])  # 복사될 객체
    20  clone = original.clone()  # 깊은 복사된 객체
    21
    22  print("Original:", original)
    23  print("Clone:", clone)
    24
    25  # 복제 후 데이터 수정
    26  clone.data.append(4)
    27  print("After modification - Original:", original)
    28  print("After modification - Clone:", clone)
    Original: ConcretePrototype with value Original and data [1, 2, 3]
    Clone: ConcretePrototype with value Original and data [1, 2, 3]
    After modification - Original: ConcretePrototype with value Original and data [1, 2, 3]
    After modification - Clone: ConcretePrototype with value Original and data [1, 2, 3, 4]
    • copy.deepcopy(self)를 사용하여 깊은 복사를 구현하였다. 이 방식은 객체 내에 포함된 리스트와 같은 가변 객체까지 복사한다.
    • 복제된 객체의 데이터를 변경하더라도 원본 객체의 데이터는 변경되지 않는다.

Java에서 프로토타입 패턴 구현 #

  • Java에서는 Cloneable 인터페이스와 Object.clone() 메서드를 이용하여 프로토타입 패턴을 구현한다.
  • clone() 메서드는 객체를 복제하는 데 사용되며, 이를 구현하기 위해서는 클래스가 Cloneable 인터페이스를 구현해야 한다.
 1// Prototype Interface
 2interface Prototype extends Cloneable {
 3  Prototype clone();
 4}
 5
 6// ConcretePrototype
 7class ConcretePrototype implements Prototype {
 8  private String field;
 9
10  public ConcretePrototype(String field) {
11    this.field = field;
12  }
13
14  public String getField() {
15    return field;
16  }
17
18  // clone() 메서드 구현
19  @Override
20  public Prototype clone() {
21    try {
22      return (Prototype) super.clone();  // 얕은 복사
23    } catch (CloneNotSupportedException e) {
24      e.printStackTrace();
25      return null;
26    }
27  }
28}
29
30// 클라이언트 코드
31public class PrototypePatternExample {
32  public static void main(String[] args) {
33    ConcretePrototype prototype1 = new ConcretePrototype("Original Object");
34
35    // 객체 복제
36    ConcretePrototype clone1 = (ConcretePrototype) prototype1.clone();
37
38    System.out.println("Original: " + prototype1.getField());
39    System.out.println("Clone: " + clone1.getField());
40  }
41}
Original: Original Object
Clone: Original Object

C++에서 프로토타입 패턴 구현 #

  • C++에서도 clone() 메서드를 사용하여 프로토타입 패턴을 구현할 수 있다.
  • C++에서 객체 복사는 기본적으로 얕은 복사만 제공하므로, 깊은 복사를 구현하려면 복사 생성자를 오버라이드해야 할 수 있다.
 1#include <iostream>
 2using namespace std;
 3
 4// Prototype Class
 5class Prototype {
 6public:
 7  virtual Prototype* clone() const = 0; // clone 메서드 선언
 8  virtual void display() const = 0; // 객체를 출력할 display 메서드
 9  virtual ~Prototype() {} // 가상 소멸자
10};
11
12// ConcretePrototype Class
13class ConcretePrototype : public Prototype {
14private:
15  string data;
16
17public:
18  ConcretePrototype(string data) : data(data) {}
19
20  // clone 메서드 구현
21  Prototype* clone() const override {
22    return new ConcretePrototype(*this); // 깊은 복사 (복사 생성자 호출)
23  }
24
25  void display() const override {
26    cout << "Data: " << data << endl;
27  }
28};
29
30// 클라이언트 코드
31int main() {
32  ConcretePrototype prototype1("Prototype Object");
33
34  // 객체 복제
35  ConcretePrototype* clone1 = dynamic_cast<ConcretePrototype*>(prototype1.clone());
36
37  cout << "Original: ";
38  prototype1.display();
39  
40  cout << "Clone: ";
41  clone1->display();
42
43  delete clone1; // 동적 메모리 해제
44  return 0;
45}
Original: Data: Prototype Object
Clone: Data: Prototype Object

프로토타입 패턴의 장점 #

  • 객체 생성 비용 절감

    • 기존 객체를 복제하여 새로운 객체를 만드는 방식이기 때문에 객체를 새로 생성하는 비용을 절감할 수 있다.
    • 특히, 객체 생성이 비용이 많이 드는 경우 유용하다.
  • 복잡한 객체 생성의 단순화

    • 객체를 복제하는 방식으로 복잡한 객체를 쉽게 생성할 수 있다.
    • 예를 들어, 복잡한 초기화 과정이 필요한 객체가 있을 때, 이를 복제하여 새 객체를 빠르게 생성할 수 있다.
  • 동적 객체 생성

    • 어떤 클래스를 사용할지 알기 어려운 경우, 기존 객체를 복제하는 방식으로 동적으로 객체를 생성할 수 있다.
  • 메모리 관리 용이

    • 복사하는 방식으로 객체를 생성하기 때문에, 객체가 다루는 자원(메모리)을 관리하는 데 있어 효율성을 높일 수 있다.

프로토타입 패턴의 단점 #

  • 복잡한 객체의 복사

    • 객체를 복제할 때, 객체가 내부에 다른 객체를 참조하거나 복잡한 상태를 가질 경우 깊은 복사와 같은 추가적인 관리가 필요할 수 있다.
    • 특히, 객체 간의 참조가 중요한 경우, 복사 후에 참조 관계가 잘못 복제될 위험이 있다.
  • 성능 이슈

    • 객체 복사가 복잡한 객체일 경우, 복사 과정에서 성능 저하가 발생할 수 있다.
    • 예를 들어, 매우 큰 객체나 복잡한 구조를 갖는 객체를 복제할 때 시간 소모가 클 수 있다.
  • 복사본의 일관성 문제

    • 복사본이 원본 객체와 다른 상태를 유지하게 되는 경우, 복사본과 원본 객체 간의 일관성을 관리하는 것이 어려울 수 있다.
Advertisement