본문 바로가기
DesignPattern

[Design Pattern] Prototype pattern

by 봄석 2019. 9. 27.

[Design Pattern] Prototype pattern

 

Prototype패턴이란 ?

 

보통은 new키워드를 사용하여 인스턴스를 생성합니다.

하지만 인스턴스를 다시 생성해야 하는것이 아니라 기존 생성되어있는 인스턴스를 복사 하여 새로운것을 만들어야하는경우가 있습니다.

 

아래와 같습니다.

 

  1. 종류가 너무 많이 클래스로 정리되지 않는 경우

    취급하는 오브젝트의 종류가 너무 많아, 각각을 별도의 클래스로 만들어 다수의 소스 파일을 작성해야 함
  2. 클래스로부터 인스턴스 생성이 어려운 경우

    생성하고 싶은 인스턴스가 복잡한 작업을 거쳐 만들어지기 때문에, 클래스로부터 만들기가 매우 어려운 경우
  3. framework와 생성할 인스턴스를 분리하고 싶은 경우

    프레임워크를 특정 클래스에 의존하지 않고 만들고 싶은 경우. 클래스 이름을 지정하여 인스턴스를 만드는 것이 아니라, 이미 모형이 되는 인스턴스를 등록해 두고, 그 인스턴스를 복사하여 생성한다.

 

 

 

 

 

EX) Java

  • Product : 추상 메소드 use와 createClone이 선언되어 있는 인터페이스
    • use : 메서드가 해당하는 동작(앞뒤를 입력받은 문자로 감싼다)을 수행하게 하는 인터페이스
    • createClone : 자기 자신을 복사하여 새로운 인스턴스를 리턴하는 메소드
  • PrototypeService : 복사할 객체를 등록하는 register와 입력받은 객체를 복사하여 생성하는 create 메서드가 있는 서비스
    • register : 복사할 객체를 등록한다. key는 객체의 다운캐스팅한 클래스 이름으로 한다.
    • create : 등록되어 있는 객체에서 클래스명으로 해당하는 객체를 복사한다.
  • MessageBox : Product를 구현한 클래스

 

Product

public interface Product extends Cloneable {
    Map<String, String> maps = new HashMap<>();

    String use(String use);

    Product createClone();
}

 

자바에서는 객체를 복사하기 위해서 Cloneable 클래스를 구현하여야 합니다. Product 객체를 구현한 구현체를 복사할 것이기 때문에, Product 인터페이스에서는 Cloneable 을 extends 하여야 합니다. 

 

PrototypeService

public class PrototypeService {
	private HashMap showcase = new HashMap<>();
	public void register(String name, Product proto) {
		showcase.put(name, proto);
	}

	public Product create(String protoName) {
		return ((Product)showcase.get(protoName)).createClone();
	}
}

복사할 객체를 등록하고, 입력받은 키로 객체를 복사하여 반환하는 역할을 하는 객체입니다. 내부에는 showcase라는 HashMap이 있고, 인스턴스를 이름으로 저장합니다.

 

코드에서 객체의 실제 이름이 나오지 않음을 주의해서 보면 좋습니다. 내부에서 클래스의 이름이 노출되지 않기 때문에, 이 객체는 Product의 구현체와 별개로 독립적으로 수정할 수 있습니다. Product라는 인터페이스만이 실제 구현체와 징검다리 역할을 합니다.

 

MessageBox

public class MessageBox implements Product {
	private String deco;

	public MessageBox(String deco) {
		this.deco = deco;
	}

	@Override
	public String use(String s) {
		return deco + s + deco;
	}

	@Override
	public Product createClone() {
		Product p = null;
		try {
			p = (Product)clone();
		} catch (CloneNotSupportedException ex) {
			ex.printStackTrace();
		}
		return p;
	}
}

입력하는 메세지의 처음과 끝에 deco를 달아주는 Product를 구현한 MessagBox클래스입니다.

 

test 

public class ProtoTypeServiceTest {
    private static final String TEST = "TEST";

    MessageBox starBox = new MessageBox("*");
    MessageBox underlineBox = new MessageBox("_");

    private PrototypeService prototypeService = new PrototypeService();

    @Before
    public void before() {
        prototypeService.register("starBox", starBox);
        prototypeService.register("underlineBox", underlineBox);
    }

    @Test
    public void create_messageBox() {
        // given

        // when
        Product result = prototypeService.create("starBox");

        // then
        assertThat(result.use(TEST), is("*TEST*"));
        assertThat(result.getClass().toString(), is(starBox.getClass().toString()));
        assertThat(result.hashCode(), not(starBox.hashCode()));
        assertThat(result.maps.hashCode(), is(starBox.maps.hashCode()));
    }

    @Test
    public void create_underLineBox() {
        // given

        // when
        Product result = prototypeService.create("underlineBox");

        // then
        assertThat(result.use(TEST), is("_TEST_"));
        assertThat(result.getClass().toString(), is(underlineBox.getClass().toString()));
        assertThat(result.hashCode(), not(underlineBox.hashCode()));
        assertThat(result.maps.hashCode(), is(underlineBox.maps.hashCode()));
    }
}

 

 

ex) Kotlin prototype

open class Bike(
    val model: String? = null,
    private val bikeType: String,
    private val gear: Int = 0
) : Vehicle {

    val someObject = hashMapOf<String, String>()

    override fun createClone(): Vehicle {
        return clone() as Bike
    }

    public override fun clone(): Any {
//        return super.clone() ,,//Caused by: java.lang.ClassNotFoundException: java.lang.Cloneable$DefaultImpls
        return Bike(
            model = model,
            bikeType = bikeType,
            gear = gear
        )
    }
}
class PrototypeService<T : Vehicle> {
    private val showcase = hashMapOf<String, T>()

    fun register(name: String, model: T) {
        showcase[name] = model
    }

    fun createClone(name: String): T? {
        return (showcase[name]?.createClone()) as T
    }
}
interface Vehicle : Cloneable {
    fun createClone(): Vehicle
}

test

class BikeTest {
    private val service = PrototypeService<Bike>()
    private val basicBike = Bike("Leopard", "Standard", 4)
    private val advancedBike = Bike("Jaguar", "advanced", 6)

    @Before
    fun before() {
        service.register("Leopard", basicBike)
        service.register("Jaguar", advancedBike)
    }

    @Test
    fun checkModel() {
        //given

        //when
        val cloneBasicBike = service.createClone("Leopard")
        val cloneAdvancedBike = service.createClone("Jaguar")

        //then
        Assert.assertThat(cloneBasicBike?.model, `is`(basicBike.model))
        Assert.assertThat(cloneAdvancedBike?.model, `is`(advancedBike.model))
    }

    @Test
    fun checkHashCode() {
        //given

        //when
        val cloneBasicBike = service.createClone("Leopard")
        val cloneAdvancedBike = service.createClone("Jaguar")


        //then
        Assert.assertThat(basicBike.hashCode(), not(cloneBasicBike.hashCode()))
        Assert.assertThat(advancedBike.hashCode(), not(cloneAdvancedBike.hashCode()))

    }

    @Test
    fun checkInternalObjectHashCode() {
        //given

        //when
        val cloneBasicBike = service.createClone("Leopard")
        val cloneAdvancedBike = service.createClone("Jaguar")


        //then
        Assert.assertThat(basicBike.someObject.hashCode(), `is`(cloneBasicBike?.someObject.hashCode()))
        Assert.assertThat(advancedBike.someObject.hashCode(), `is`(cloneAdvancedBike?.someObject.hashCode()))
    }
}

 

 

Clone

자바에서 Clone은 Cloneable 클래스를 상속한 클래스에서 clone() 메서드를 호출하여 구현할 수 있습니다.

clone이 호출되면 해당 클래스의 인스턴스를 복사해서 반환하게 됩니다. 몇 가지 주의할 점이 있습니다.

  1. Cloneable 인터페이스를 상속하지 않은 경우
    • CloneNotSupportedException이 발생하게 됩니다.
  2. 클래스 명이 같다.
    • 클래스를 복사하였기 때문에 클래스 명이 같습니다.
  3. hashCode() 의 리턴값이 다르다.
    • 아예 다른 객체가 리턴되기 때문에, hashCode()의 리턴값이 다릅니다.
  4. 얕은 복사가 이루어진다.
    • 만약에 복사할 객체가 다른 객체를 가지고 있다면, 객체에 대한 참조가 복사되는 것이지 객체 자체가 따로 복사되는 것은 아닙니다. 만약에 객체를 깊은 복사로 완전히 복사할 필요가 있다면, clone 메서드를 오버라이드 하여 재정의해야 할 것입니다.
  5. 객체가 생성되는 것이 아니다.
    • 복사만 하는 것이고, 생성자를 따로 호출하는 것은 아닙니다.

 

정리 - (ProtoType Clone을 사용해야 할 때)

  • 클래스가 런타임에 인스턴스화 될 때
  • 객체 생성 비용이 비싸거나 복잡한 경우.
  • 응용 프로그램의 클래스 수를 최소로 유지하려는 경우
  • 클라이언트 응용 프로그램이 객체 생성 및 표현을 인식하지 못하는 경우

프로토 타입 패턴의 장점

  • 서브 클래 싱의 필요성을 줄입니다.
  • 객체 생성의 복잡성을 숨 깁니다.
  • 클라이언트는 어떤 유형의 객체인지 모른 채 새로운 객체를 얻을 수 있습니다.
  • 런타임에 객체를 추가하거나 제거 할 수 있습니다.

 

코틀린에서 Clonable을 구현하고 clone을 override한후, 

super.clone을 call하면 

Caused by: java.lang.ClassNotFoundException: java.lang.Cloneable$DefaultImpls

에러가 발생합니다 .

글쓴당시시점으로 아직해결되지 않은 문제인것같아서 

생성자를 통해 다시 객체를 생성하였습니다..?(정답인지 의문)

 

 

마무리

프로토타입 패턴은 실제로 다양하게 사용하는 부분을 만나기는 쉽지 않았습니다. 아무래도 클래스를 new 로 생성하지 않고 복사해서 생성하는 경우는 내부 데이터까지 완전히 동일한 인스턴스가 따로 필요한 경우일 거 같은데, 그림판에서 도형을 복사 - 붙여넣기 하는 것과 비슷한 코드를 많이 작성할 기회가 많지 않았던 거 같습니다.

 

 

 소스코드 보러가기

https://github.com/qjatjr1108/DesignPattern

 

qjatjr1108/DesignPattern

DesignPattern Sample. Contribute to qjatjr1108/DesignPattern development by creating an account on GitHub.

github.com

 

참고 - https://chercher.tech/kotlin/prototype-design-pattern-kotlin

댓글