PROTOTYPE

기존 객체를 복사(clone)하여 새로운 객체를 생성하는 방식의 생성(Creational) 패턴이다. 객체를 새로 생성하는 대신, 기존 객체를 복제하여 초기화 비용을 줄이고, 복잡한 객체 구조를 효율적으로 관리할 수 있다.

Design Principles

Structure:

  • Prototype: Declares an interface for cloning itself.
  • ConcretePrototype: Implements an operation for cloning itself.
  • Client: Creates a new object by asking a prototype to clone itself.

Examples: External Library Object Copy

프로덕션 코드에서 타사 라이브러리에서 제공하는 구상 클래스에 의존하면 안되는 경우 Prototype 패턴을 사용할 수 있다.

External Library Object:

// 외부 라이브러리에서 제공하는 복잡한 클래스들
interface ExternalDocument {
    fun clone(): ExternalDocument  // 복제 기능만 제공
    fun getContent(): String
}

class PDFDocument(private val content: String) : ExternalDocument {
    override fun clone(): ExternalDocument = PDFDocument(content)
    override fun getContent() = "PDF: $content"
}

class WordDocument(private val content: String) : ExternalDocument {
    override fun clone(): ExternalDocument = WordDocument(content)
    override fun getContent() = "Word: $content"
}

Apply Prototype:

// 프로토타입 패턴을 적용한 도구 클래스
class DocumentProcessor(private val prototype: ExternalDocument) {
    fun processDocument(): ExternalDocument {
        val clonedDoc = prototype.clone()  // 구체적인 클래스에 의존하지 않고 복제
        println("Processing Document: ${clonedDoc.getContent()}")
        return clonedDoc
    }
}

Client:

fun main() {
    // 외부 객체 (프로토타입으로 사용)
    val pdfPrototype = PDFDocument("Kotlin Design Patterns")
    val wordPrototype = WordDocument("Prototype Pattern Example")

    // 프로세서에 프로토타입 전달
    val pdfProcessor = DocumentProcessor(pdfPrototype)
    val wordProcessor = DocumentProcessor(wordPrototype)

    // 프로토타입 복제 및 처리
    pdfProcessor.processDocument()   // Processing Document: PDF: Kotlin Design Patterns
    wordProcessor.processDocument()  // Processing Document: Word: Prototype Pattern Example
}

이 처럼 Prototype 을 적용하게 되면, 새로운 문서 타입이 추가되어도 DocumentProcessor 는 수정할 필요가 없으며, 새로운 문서 클래스가 clone() 만 구현하면 된다. 또한 외부 코드 수정 없이도 내부 로직을 유연하게 확장할 수 있다.

Examples: Non-Disruptive Cache Update

해시값 계산과 같은 복잡한 계산을 통해 데이터를 얻거나, RPC, Network, Database 등에 대한 접근을 통해 데이터를 얻어서 객체를 생성해야 하는 경우에는 시간이 많이 소요된다. 이 경우 기존 객체를 복사해서 생성하는 방식을 적용할 수 있다.

예를 들어 상품 클래스는 이름과 가격만 존재한다. 특정 keyword 로 상품을 조회하는데, 매번 데이터베이스에서 조회하면 비용이 크기 때문에 메모리에 올려두어서 사용한다고 가정한다. 이때 메모리 있는 값은 valid 한지를 보장하지 못하기 때문에, 데이터베이스에 새 상품이 추가되는 경우, 항상 모든 데이터를 데이터베이스에서 조회해야한다. 또한 데이터가 10만건 이상이라고 가정한다.

Product Model:

data class Product(val id: String, var name: String, var price: Double) : Cloneable {
    // 얕은 복사 (속도 빠름)
    public override fun clone(): Product = super.clone() as Product

    // 깊은 복사 (데이터 독립성 보장)
    fun deepClone(): Product = Product(id, name, price)

    // 변경 사항 비교
    fun isDifferentFrom(other: Product): Boolean {
        return this.name != other.name || this.price != other.price
    }
}

Apply Prototype:

class ProductCacheManager {
    private var productCache: MutableMap<String, MutableList<Product>> = mutableMapOf()

    // 초기 데이터 로딩
    fun loadInitialData() {
        val dbData = mapOf(
            "electronics" to mutableListOf(Product("1", "Laptop", 1200.0), Product("2", "Smartphone", 800.0)),
            "furniture" to mutableListOf(Product("3", "Desk", 300.0), Product("4", "Chair", 150.0))
        )
        productCache.putAll(dbData)
    }

    // 데이터 조회
    fun getProductsByKeyword(keyword: String): List<Product>? = productCache[keyword]

    // 캐시 업데이트 (얕은 복사 + 깊은 복사 병행)
    fun updateCache(keyword: String, latestProducts: List<Product>) {
        // 1️⃣ 기존 캐시를 얕은 복사 (빠른 스냅샷)
        val clonedCache = productCache.toMutableMap()

        // 2️⃣ 기존 캐시 데이터와 비교하여 변경된 데이터만 깊은 복사
        val existingProducts = clonedCache[keyword]?.associateBy { it.id } ?: emptyMap()

        val updatedProducts = latestProducts.map { newProduct ->
            val existingProduct = existingProducts[newProduct.id]
            if (existingProduct != null && newProduct.isDifferentFrom(existingProduct)) {
                newProduct.deepClone() // 변경된 경우 깊은 복사
            } else {
                newProduct.clone() // 변경되지 않은 경우 얕은 복사
            }
        }.toMutableList()

        // 3️⃣ 캐시 스왑
        clonedCache[keyword] = updatedProducts
        productCache = clonedCache
    }
}

References

  • Gangs of Four Design Patterns
  • 设计模式之美 / 王争