Prototype Pattern

2021. 12. 11. 13:34Computer Sciences/Design Patterns

개요

패턴에 앞서 프로토타입이 무슨 뜻인지 생각해보고 가자. 일반적으로 프로토타입이라고 하면 실제 제품을 만들기 전의 샘플 정도라고 알고 있을 것이다. 패턴도 이와 비슷한 맥락이다.

프로토타입 패턴은 생성 비용이 비싼(네트워크를 타거나, 생성 로직이 복잡하다던가) 객체나 비슷한 객체를 생성해야 할 때 사용되는 생성 패턴이다.

예를 들어 DB에서 데이터 구조가 거의 변하지 않는 데이터를 가져온다고 해보자. 필요한 데이터마다 객체를 네트워크를 거쳐서 가져오게 되면 비용이 만만치 않다. 대신 가져온 데이터를 복사해서 일부만 수정하는 방식이 있다. 이는 메모리 상에서 이루어지므로 네트워크 비용보다 훨씬 싸다. 또한 데이터 구조도 거의 비슷하기 때문에 복사하여 수정하는 것이 어렵지 않다.

예제

비슷한 구조에 대해 프로토타입 패턴을 적용한 예제를 살펴보자.

문제

텍스트를 입력하면 해당 텍스트를 꾸며서 출력하는 프로그램을 만들고 싶다. 예를 들어 텍스트를 특정 문자로 둘러싸던가, 밑줄을 추가하는 것이다. 객체를 설계하면 다음과 같다.

Product

  • 추상 메서드 use()와 createClone()이 선언된 인터페이스

Manager

  • createClone()으로 인스턴스를 복제하여 제공하는 팩토리 클래스

MessageBox

  • 문자열에 테두리를 추가하는 클래스.

UnderlinePen

  • 문자열에 밑줄을 추가하는 클래스.

Class Diagram

코드

// clone()을 사용하는 클래스는 Cloneable을 구현해야 한다.
// Cloneable은 아무런 메서드도 없는데
// 이처럼 특정 타입을 의미하도록 사용하는 인터페이스를 마커 인터페이스라고 한다.
public interface Product extends Cloneable {
	// 사용할 문자 지정 메서드
	void use(String s);
	Product createClone();
}
public class Manager {

	private HashMap<String, Product> showcase = new HashMap<>();

	public void register(String name, Product proto) {
		showcase.put(name, proto);
	}

	public Product create(String protoname) {
		Product p = (Product)showcase.get(protoname);
		return p.createClone();
	}
}
// 모든 Product는 같은 createClone() 메서드를 사용하기 때문에 추상화했다.
// use()는 각 Product에서 구현한다.
public abstract class AbstractProduct implements Product {

	public Product createClone() {
		Product p = null;
		try {
			// 자바에서 clone은 Object에 정의되어 있다.
			// 따라서 Cloneable이 구현된 객체는 clone()을 사용할 수 있다.
			p = (Product)clone();
			return p;
		} catch (CloneNotSupportedException e) {
			e.printStackTrace();
		}
	}
}
public class MessageBox extends AbstractProduct {

	private char decochar;

	public MessageBox(char decochar) {
		this.decochar = decochar;
	}

	@Override
	public void use(String s) {
		int length = s.getBytes().length;
		for (int i = 0; i < length + 4; i++) {
			System.out.print(decochar);
		}
		System.out.println();
		System.out.println(decochar + " " + s + " " + decochar);
		for (int i = 0; i < length + 4; i++) {
			System.out.print(decochar);
		}
		System.out.println();
	}
}
public class UnderlinePen extends AbstractProduct {

	private char ulchar;

	public UnderlinePen(char ulchar) {
		this.ulchar = ulchar;
	}

	@Override
	public void use(String s) {
		int length = s.getBytes().length;
		System.out.println("\\"" + s + "\\"");
		for (int i = 0; i < length; i++) {
			System.out.print(ulchar);
		}
		System.out.println();
	}
}
public class Test {

	public static void main(String[] args) {
		Manager manager = new Manager();
		UnderlinePen up = new UnderlinePen('_');
		MessageBox mbStar = new MessageBox('*');
		MessageBox mbAt = new MessageBox('@');
		manager.register("important message", up);
		manager.register("notice message", mbStart);
		manager.register("emphasize message", mbAt);

		// 반횐되는 product들은 new로 생성된 것이 아니라
		// 등록되어 있는 인스턴스를 clone한 것이 반환된 것이다.
		Product p1 = manager.create("important message");
		p1.use("Hello World");
		Product p2 = manager.create("notice message");
		p2.use("Hello World");
		Product p3 = manager.create("emphasize message");
		p3.use("Hello World");
	}
}

장단점

장점

  • 클라이언트는 새로운 인스턴스를 만드는 방법을 몰라도 된다.
  • 클라이언트는 구체적인 방식을 몰라도 객체를 만들 수 있다.
  • 상황에 따라 객체를 생성하는 것보다 복사하는 것이 이득일 때 적용할 수 있다.

단점

  • 복사본을 만드는 일이 매우 복잡한 경우가 있다.
    • 기본적으로 clone()은 얕은 복사(shallow copy)를 진행한다. 이는 객체의 경우 내부에 연결되어 있는 값들을 모두 복사하는 게 아니라 참조값만 복사한다는 의미이다. 즉 얕은 복사로 만들어진 객체를 수정하면 원본에도 변경이 일어나게 된다. 따라서 완전히 다른 객체를 만들고 싶다면 참조를 타고 들어가 연관된 값들을 모두 복사해주어야 한다. 이를 깊은 복사(deep copy)라고 한다.

'Computer Sciences > Design Patterns' 카테고리의 다른 글

Flyweight Pattern  (0) 2021.12.11
Visitor Pattern  (0) 2021.12.11
14. Chain of Responsibility Pattern  (0) 2021.11.14
13. Bridge Pattern  (0) 2021.11.14
12. State Pattern  (0) 2021.10.15