Prototype Pattern 정리

테사벨로그·2025년 10월 23일

Design Pattern

목록 보기
14/19

1. 왜 Prototype Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 클래스마다 Factory를 만들어야 하는 코드
public class GraphicEditor {
    public Graphic createGraphic(String type) {
        if (type.equals("Line")) {
            return new Line();
        } else if (type.equals("Rectangle")) {
            return new Rectangle();
        } else if (type.equals("Circle")) {
            return new Circle();
        }
        // 새로운 도형을 추가하려면 코드 수정 필요!
    }
}

문제점:

  • 새로운 타입을 추가할 때마다 코드 수정 필요
  • 런타임에 동적으로 새로운 타입 추가 불가능
  • 복잡한 객체를 매번 처음부터 생성해야 함 (비용 증가)
  • Factory 클래스 계층이 Product 클래스 계층과 병렬로 증가

2. Prototype Interface 왜 필요한가?

Prototype Interface

  • "나를 복제하고 싶으면 clone()을 호출하세요"
  • 복제 기능의 규격만 정의
  • "can-clone" 관계 (복제 가능한 객체)
public interface Prototype {
    Prototype clone();  // 자신의 복사본 반환
}

왜 Interface인가?

  • 다양한 객체(도형, 문서, 설정 등)가 복제 가능해야 함
  • 복제 방법은 각 객체마다 다를 수 있음
  • 클라이언트는 구체적인 클래스를 몰라도 복제 가능

3. Shallow Copy VS Deep Copy

Shallow Copy (얕은 복사)

Original Object          Clone Object
    ↓                        ↓
  [data]                  [data]
    ↓                        ↓
  참조 ──────────────────→ 같은 객체
  • 참조만 복사: 복사된 객체가 같은 내부 객체를 가리킴
  • 문제: 한 쪽에서 내부 객체 수정 시 다른 쪽도 영향받음

Deep Copy (깊은 복사)

Original Object          Clone Object
    ↓                        ↓
  [data]                  [data]
    ↓                        ↓
  참조 → 객체A            참조 → 객체A의 복사본
  • 실제 객체까지 복사: 완전히 독립적인 복사본 생성
  • 장점: 서로 영향을 주지 않음

4. Prototype Pattern 핵심 구조

      Client  ───────────>  Prototype (Interface)
   (객체 사용자)            (복제 규격 정의)
                                   ↑
                    ┌──────────────┴──────────────┐
             ConcreteProduct1          ConcreteProduct2
            (구체적 복제 구현)          (구체적 복제 구현)

   - Client는 구체 클래스를 몰라도 됨
   - prototype.clone() 호출만으로 새 객체 생성
   - 런타임에 프로토타입 교체 가능

5. 예시 코드

Step 1: Prototype 인터페이스 정의

// Prototype 인터페이스
public interface Prototype {
    Prototype clone();
}

Step 2: 구체적인 Prototype 구현 (Shallow Copy)

// Java의 Cloneable 사용 (Shallow Copy)
public class Stack implements Cloneable {
    private final int maxSize;
    private int top;
    private Object[] store;  // 참조 타입!
    
    public Stack(int size) {
        this.maxSize = size;
        this.store = new Object[size];
        this.top = -1;
    }
    
    // Shallow Copy: store 배열은 참조만 복사됨
    @Override
    public Object clone() throws CloneNotSupportedException {
        try {
            return super.clone();  // 기본 clone()은 shallow copy
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
    
    public void push(Object item) {
        if (top < maxSize - 1) {
            store[++top] = item;
        }
    }
}

Step 3: Deep Copy 구현

// Deep Copy 버전
public class Stack implements Cloneable {
    private final int maxSize;
    private int top;
    private Object[] store;
    
    public Stack(int size) {
        this.maxSize = size;
        this.store = new Object[size];
        this.top = -1;
    }
    
    // Deep Copy: store 배열까지 복사
    @Override
    public Object clone() throws CloneNotSupportedException {
        try {
            Stack result = (Stack) super.clone();
            // 배열도 복사해서 완전히 독립적인 객체 생성
            result.store = store.clone();
            return result;
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
    
    public void push(Object item) {
        if (top < maxSize - 1) {
            store[++top] = item;
        }
    }
}

Step 4: Cloneable 없이 직접 구현 (Deep Copy)

// Cloneable을 사용하지 않고 직접 구현
public class Stack {
    private final int maxSize;
    private int top;
    private Object[] store;
    
    public Stack(int size) {
        this.maxSize = size;
        this.store = new Object[size];
        this.top = -1;
    }
    
    // 직접 복제 메서드 구현
    public Stack createClone() {
        Stack result = new Stack(maxSize);
        result.top = this.top;
        // 배열 요소 하나하나 복사
        for (int i = 0; i <= top; i++) {
            result.store[i] = this.store[i];
        }
        return result;
    }
    
    public void push(Object item) {
        if (top < maxSize - 1) {
            store[++top] = item;
        }
    }
}

Step 5: 실제 사용 예제 (그래픽 에디터)

// Prototype 인터페이스
public interface Graphic extends Cloneable {
    Graphic clone();
    void draw();
}

// 구체적인 도형들
public class Line implements Graphic {
    private int x1, y1, x2, y2;
    
    public Line(int x1, int y1, int x2, int y2) {
        this.x1 = x1;
        this.y1 = y1;
        this.x2 = x2;
        this.y2 = y2;
    }
    
    @Override
    public Graphic clone() {
        try {
            return (Graphic) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
    
    @Override
    public void draw() {
        System.out.println("선 그리기: (" + x1 + "," + y1 + 
                         ") -> (" + x2 + "," + y2 + ")");
    }
}

public class Rectangle implements Graphic {
    private int x, y, width, height;
    
    public Rectangle(int x, int y, int width, int height) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
    }
    
    @Override
    public Graphic clone() {
        try {
            return (Graphic) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
    
    @Override
    public void draw() {
        System.out.println("사각형 그리기: (" + x + "," + y + 
                         ") 크기: " + width + "x" + height);
    }
}

// Palette: 프로토타입을 관리하는 클라이언트
public class Palette {
    private List<Graphic> prototypes = new ArrayList<>();
    
    // 프로토타입 등록
    public void addPrototype(Graphic prototype) {
        prototypes.add(prototype);
    }
    
    // 인덱스로 프로토타입의 복제본 생성
    public Graphic makeGraphic(int index) {
        if (index >= 0 && index < prototypes.size()) {
            // 프로토타입을 복제하여 반환 (구체 클래스 몰라도 됨!)
            return prototypes.get(index).clone();
        }
        return null;
    }
    
    // 런타임에 프로토타입 제거 가능
    public void removePrototype(int index) {
        if (index >= 0 && index < prototypes.size()) {
            prototypes.remove(index);
        }
    }
}

Step 6: 실행

public class GraphicEditor {
    public static void main(String[] args) {
        Palette palette = new Palette();
        
        // 초기 프로토타입 등록
        palette.addPrototype(new Line(0, 0, 100, 100));
        palette.addPrototype(new Rectangle(0, 0, 50, 50));
        
        // 사용자가 Line을 선택하여 새 객체 생성
        Graphic newLine1 = palette.makeGraphic(0);
        Graphic newLine2 = palette.makeGraphic(0);
        
        // 사용자가 Rectangle을 선택하여 새 객체 생성
        Graphic newRect = palette.makeGraphic(1);
        
        // 그리기
        newLine1.draw();
        newLine2.draw();
        newRect.draw();
        
        // 런타임에 새로운 프로토타입 추가 가능!
        palette.addPrototype(new Line(50, 50, 150, 150));
        Graphic newLine3 = palette.makeGraphic(2);
        newLine3.draw();
    }
}

출력 결과

선 그리기: (0,0) -> (100,100)
선 그리기: (0,0) -> (100,100)
사각형 그리기: (0,0) 크기: 50x50
선 그리기: (50,50) -> (150,150)

6. Abstract Factory와 함께 사용하기

// Prototype을 활용한 MazeFactory
public class MazeFactory {
    private Maze prototypeMaze;
    private Wall prototypeWall;
    private Room prototypeRoom;
    private Door prototypeDoor;
    
    // 생성자에서 프로토타입 설정
    public MazeFactory(Maze m, Wall w, Room r, Door d) {
        this.prototypeMaze = m;
        this.prototypeWall = w;
        this.prototypeRoom = r;
        this.prototypeDoor = d;
    }
    
    // 프로토타입을 복제하여 객체 생성
    public Wall makeWall() {
        return prototypeWall.clone();
    }
    
    // 런타임에 프로토타입 변경 가능!
    public void setWallPrototype(Wall w) {
        this.prototypeWall = w;
    }
    
    public Door makeDoor(Room r1, Room r2) {
        Door door = prototypeDoor.clone();
        door.initialize(r1, r2);
        return door;
    }
}

7. 핵심 정리

Prototype Pattern의 구성

요소역할특징
Prototype복제 인터페이스 정의clone() 메서드 선언
ConcretePrototype실제 복제 구현clone()을 구체적으로 구현
Client프로토타입 사용구체 클래스를 몰라도 복제 가능

언제 사용하는가?

  • 객체 생성 비용이 클 때 (데이터베이스 조회, 파일 읽기 등)
  • 런타임에 생성할 클래스가 결정될 때
  • 비슷한 객체가 많이 필요할 때
  • Factory 클래스 계층을 피하고 싶을 때
  • 복잡한 객체의 설정을 재사용하고 싶을 때

장점

  • 성능 향상: 복잡한 초기화 과정을 한 번만 수행
  • 런타임 유연성: 동적으로 프로토타입 추가/제거 가능
  • 서브클래스 감소: Factory Method 대신 프로토타입 사용
  • 클라이언트 독립성: 구체 클래스 몰라도 객체 생성 가능

주의사항

  • ⚠️ Shallow Copy vs Deep Copy: 상황에 맞게 선택
  • ⚠️ 순환 참조 주의: Deep Copy 시 무한 루프 가능
  • ⚠️ clone() 구현 복잡도: 복잡한 객체는 Deep Copy 구현이 어려움

핵심 원칙

  • 복제를 통한 생성: new 대신 clone() 사용
  • 프로토타입 기반: 기존 객체를 템플릿으로 활용
  • 느슨한 결합: 클라이언트는 인터페이스만 의존
  • 동적 구성: 런타임에 프로토타입 변경 가능
profile
다들 응원합니다.

0개의 댓글