Study/Design Pattern

[Design pattern] 1-5. 프로토타입 패턴 (Prototype pattern)

v명월v 2021. 10. 22. 20:11

안녕하세요. 명월입니다.


이 글은 디자인 패턴의 프로토타입 패턴(Prototype pattern)에 대한 글입니다.


프로토타입은 패턴 자체는 굉장히 단순한 패턴입니다만, 개념적으로 포인터와 스택, 힙 메모리에 대한 개념을 잘 모르신다면 이해하기 어려울 수도 있는 패턴입니다.

우리가 프로그램 상에서 클래스의 인스턴스를 생성하게 되면 변수에 포인터 주소가 입력되고 주소에 따른 인스턴스가 힙 메모리에 할당이 됩니다. 그래서 변수로 새로운 객체를 생성하지 않고 등호(equal: =) 기호로 새로운 변수명으로 인스턴스의 주소 값을 넘기면 두개의 변수에서 하나의 인스턴스를 가르키기 때문에 두 변수의 처리에 있어서 데이터 영향이 생깁니다.

using System;

namespace Example
{
  // 예제 클래스
  class Node
  {
    // 데이터 프로퍼티
    public int Data { get; set; }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node = new Node();
      // node.Data의 프로퍼티에 1이란 값을 넣는다.
      node.Data = 1;
      // 새로운 변수를 생성하고 node의 인스턴스 주소를 넣는다.
      var nodeClone = node;
      // nodeClone.Data에 2의 값을 넣는다.
      nodeClone.Data = 2;
      // 이 때, node.Data의 값은 1일까 2일까?
      Console.WriteLine(node.Data);
      
      // 메모리 주소 출력
      Console.WriteLine(node.GetHashCode());
      Console.WriteLine(nodeClone.GetHashCode());
      // 아무 키나 누르면 종료
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

그렇죠.. 당연히 영향이 가죠.. 결과를 보시면 node 변수와 nodeClone 변수의 메모리 주소가 같다는 걸 확인할 수 있습니다.

위의 그림같은 구조로 두개의 변수에 하나의 인스턴스가 묶여 있어서 그렇습니다.

link - [C#] 10. 인스턴스 생성(new)과 메모리 할당(Stack 메모리와 Heap 메모리) 그리고 null

link - [Java] 10. 메모리 할당(stack 메모리와 heap 메모리 그리고 new)과 Call by reference(포인터에 의한 참조)


그렇다면 반대로 저 인스턴스의 주소 값을 복사하지 말고 클래스 안의 데이터는 모두 같게 하고 클래스를 복제할 수 없을까?

위 예제 클래스는 워낙 단순한 구조라 새로 new Node하고 Data 값을 복사하는 것으로 끝나지만, 복잡한 클래스이고 대부분의 맴버 함수가 private으로 설정되어 있을 경우에는 단순하게 복사하는 게 안되겠네요.

출처 - https://en.wikipedia.org/wiki/Prototype_pattern

#pragma once
#include <stdio.h>
#include <iostream>
using namespace std;
// 예제 클래스
class Node {
private:
  // 맴버 변수
  int _data;
public:
  // 생성자
  Node(int data) {
    // 맴버 변수에 데이터를 넣는다.
    this->_data = data;
  }
  // 출력 함수
  void print() {
    // 콘솔 출력
    cout << "data = " << this->_data << endl;
  }
  // 프로토타입(prototype)의 메모리 복사
  Node* clone()
  {
    // 새로운 인스턴스 리턴
    return new Node(*this);
  }
};
// 실행 함수
int main() {
  // 인스턴스 생성
  Node* node = new Node(1);
  // 인스턴스 복사
  Node* nodeClone = node->clone();
  // 출력 함수 호출
  node->print();
  nodeClone->print();
  
  // 메모리 주소 출력
  cout << node << endl;
  cout << nodeClone << endl;
  // 메모리 해제
  delete node;
  delete nodeClone;
  return 0;
}

C/C++에서는 단순하게 clone 함수에서 새로운 인스턴스를 생성하는 new 키워드를 사용하고 this를 통해서 메모리를 넣으면 인스턴스가 복사가 됩니다.

결과를 보시면 data에 들어가 있는 값은 같지만 메모리 주소가 다른 것을 확인할 수 있습니다.

// 프로토타입을 사용하기 위해서는 Cloneable 인터페이스를 상속받아야 한다.
class Node implements Cloneable {
  // 맴버 변수
  private int data;
  // 생성자
  public Node(int data) {
    // 맴버 변수에 값 설정
    this.data = data;
  }
  // 출력 함수
  public void print() {
    // 콘솔 출력
    System.out.println("data = " + data);
  }
  // 프로토타입(prototype)의 메모리 복사
  public Node clone() {
    try {
      // 메모리 복사한다.
      return (Node) super.clone();
    } catch (CloneNotSupportedException e) {
      // Cloneable 인터페이스를 상속 받지 않으면 에러가 발생한다.
      return null;
    }
  }
}

public class Program {
  // 실행 함수
  public static void main(String[] args) {
    // 인스턴스 생성
    var node = new Node(1);
    // 인스턴스 복사
    var nodeClone = node.clone();
    // 출력 함수 호출
    node.print();
    nodeClone.print();
    // 메모리 주소 출력
    System.out.println(node.hashCode());
    System.out.println(nodeClone.hashCode());
  }
}

자바의 경우는 Cloneable 인터페이스를 상속받아야 프로토타입 함수 clone 함수를 사용할 수 있습니다. 그리고 Object 클래스에서는 clone이 protected의 접근 제한자로 설정되어 있기 때문에 public으로 재정의해야 합니다.

자바도 맴버 변수의 값은 같은 것으로 확인이 됩니다만 메모리 주소는 다르니 서로 다른 인스턴스라는 것을 확인 할 수 있네요.

using System;

namespace Example
{
  // 예제 클래스
  class Node
  {
    // 맴버 변수
    private int data;
    // 생성자
    public Node(int data)
    {
      // 맴버 변수에 값을 설정
      this.data = data;
    }
    // 출력 함수
    public void Print()
    {
      // 콘솔에 출력
      Console.WriteLine("data = " + this.data);
    }
    // 프로토타입(prototype)의 메모리 복사
    public Node Clone()
    {
      // Object 클래스에 protected 형태로 존재한다.
      return this.MemberwiseClone() as Node;
    }
  }
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 인스턴스 생성
      var node = new Node(1);
      // 인스턴스 복사
      var nodeClone = node.Clone();
      // 출력 함수 호출
      node.Print();
      nodeClone.Print();
      // 메모리 주소 출력
      Console.WriteLine(node.GetHashCode());
      Console.WriteLine(nodeClone.GetHashCode());
      // 아무 키나 누르면 종료
      Console.WriteLine("Press any key...");
      Console.ReadLine();
    }
  }
}

C#에서는 java와 다르게 따로 상속받아야 할 인터페이스는 없습니다. java와 마찬가지로 Object 클래스에 MemberwiseClone의 함수가 protected로 설정되어 있기 때문에 public으로 재정의해야 합니다.

역시나 맴버 변수의 값은 같은 것으로 확인이 되지만 메모리 주소가 다른 것을 확인할 수 있습니다.


여기까지 디자인 패턴의 프로토타입 패턴(Prototype pattern)에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.