오늘 알아볼 Creational Patterns은 Prototype입니다.
1. 설계 목적
코드가 클래스에 의존되지 않고도 기존 객체를 복사할 수 있게 해주는 패턴입니다.

2. 문제 상황
어떠한 객체가 있고, 그 객체의 정확한 복사본을 만들고 싶다고 가정해봅시다. 단순하게 생각해서 같은 클래스의 새 객체를 생성하고, 원본 객체의 필드(프로퍼티)들의 값을 새 객체에 똑같이 복사 혹은 저장하면 됩니다.
하지만 객체의 일부 필드(프로퍼티)들이 private
한 성질을 갖는다면 어떨까요? 복사를 위한 외부 접근이 불가능 할겁니다.
따라서 모든 복사가 항상 가능한 것은 아닙니다.

추가적으로 패턴의 의미를 다시 한 번 들여다봅시다. 코드가 클래스에 의존되지 않는다는건 어떤 의미일까요?
말했듯 객체를 복사하기 위한 단순한 방법은 새 객체를 생성하는 것입니다. 새 객체를 생성하기 위해서는 클래스를 알아야하죠.
따라서 코드가 클래스에 의존하게 됩니다. Protocol로 타입이 가려져있는 경우, 정확한 클래스 타입을 알 수 있는 방법이 없습니다.
3. 해결책
이를 해결하기 위해 Prototype 패턴을 사용합니다. 실제로 복제되는 객체들에 복제 프로세스(Cloning Process)를 위임(Delegate)합니다. 복제를 지원하는 모든 객체에 대한 공통 인터페이스를 선언하고, 이를 사용해서 클래스에 의존하지 않고도 객체를 복제할 수 있습니다.
일반적으로 인터페이스에 clone
메서드를 포함하여 사용합니다.
clone
메서드는 현재 클래스의 객체를 만든 후 이전 객체의 모든 필드(프로퍼티) 값을 새 객체로 전달합니다.
같은 클래스에 속한 다른 객체의 private
한 필드들에 접근 할 수 있기에 복사하는 것도 가능합니다.
문장으로 이해한다면 조금 복잡할 수 있으나, 실제 코드는 굉장히 단순합니다.
class Person {
private var name: String
private var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func clone() -> Person {
Person(name: self.name, age: self.age)
}
func info() {
print("Person: name = \(name), age = \(age)")
}
}
var originPerson = Person(name: "Rey", age: 999)
// private 프로퍼티에는 접근 불가능
// var copyPerson = Person(name: originPerson.name, age: originPerson.age)
var copyPerson = originPerson.clone()
copyPerson.info()
// Person: name = Rey, age = 999
이처럼 복제를 지원하는 객체를 Prototype(프로토타입)이라고 합니다.
객체의 구조(프로퍼티, 메소드가) 너무 복잡한 경우 자식 클래스를 정의하는 것보다 Prototype을 사용하는 것이 더 나을 수 있습니다.
위 이미지의 비행기를 통해 코드로 예시를 작성해봅시다.
class Aircraft {
enum AircraftType {
case airbus350
case airbus380
case boeing737
}
var model: String
var seatingCapacity: Int
var maxSpeed: Int
var range: Int
init(model: String, seatingCapacity: Int, maxSpeed: Int, range: Int) {
self.model = model
self.seatingCapacity = seatingCapacity
self.maxSpeed = maxSpeed
self.range = range
}
// 복제 메서드
func copy() -> Aircraft {
Aircraft(
model: self.model,
seatingCapacity: self.seatingCapacity,
maxSpeed: self.maxSpeed,
range: self.range
)
}
func info() {
print("Aircraft: Model = \(model), Seating Capacity = \(seatingCapacity), Max Speed = \(maxSpeed) km/h, Range = \(range) km")
}
}
이후 프로토타입에 사용할 Key를 선언하고, 프로토타입을 등록하고 생성할 수 있는 클래스를 정의합니다.
enum AircraftType {
case airbus350
case airbus380
case boeing737
}
class AircraftPrototypeFactory {
private var prototypes: [AircraftType: Aircraft] = [:]
func registerPrototype(key: AircraftType, aircraft: Aircraft) {
prototypes[key] = aircraft
}
func createAircraft(from prototypeKey: AircraftType) -> Aircraft? {
return prototypes[prototypeKey]?.copy() as? Aircraft
}
}
새로운 자식 클래스를 선언하는 것보다 이와 같이 사용하는 것이 클래스의 종류를 단순하게 사용할 수 있습니다.
4. 구조
기본 구조는 다음과 같습니다.

여기에 PrototypeRegistry를 추가하여, 자주 사용하는 프로토타입에 쉽게 접근할 수 있도록 할 수 있습니다.
위 예제 코드에서 AircraftPrototypeFactory
가 이에 해당한다고 볼 수 있습니다.

5. 예제 코드의 사용
위의 비행기 예제코드가 어떻게 사용되는지 살펴봅시다.
// Prototype 객체 생성
let boeing737Prototype = Aircraft(model: "Boeing 737", seatingCapacity: 162, maxSpeed: 850, range: 5460)
let airbusA380Prototype = Aircraft(model: "Airbus A380", seatingCapacity: 555, maxSpeed: 1020, range: 15700)
let airbusA350Prototype = Aircraft(model: "Airbus A350", seatingCapacity: 320, maxSpeed: 999, range: 15200)
let factory = AircraftPrototypeFactory()
// Prototype 객체 등록
factory.registerPrototype(key: .boeing737, aircraft: boeing737Prototype)
factory.registerPrototype(key: .airbus380, aircraft: airbusA380Prototype)
factory.registerPrototype(key: .airbus380, aircraft: airbusA350Prototype)
원하는 프로토타입의 객체를 생성합니다.
if let cloneAirbus350 = factory.createAircraft(from: .airbus350) {
cloneAirbus350.info()
}
// Aircraft: Model = Airbus A350, Seating Capacity = 320, Max Speed = 999 km/h, Range = 15200 km
또는 다음과 같은 구조로도 사용할 수 있습니다.

먼저 Shape에 대해 정의합니다. 단, Swift에는 abstract class
타입이 존재하지 않기 때문에 일반 class
를 활용합니다.
class Shape {
var x: Int
var y: Int
var color: String
func clone() -> Shape {
Shape(source: self)
}
init() {
self.x = 0
self.y = 0
self.color = "clear"
}
init(x: Int, y: Int, color: String) {
self.x = x
self.y = x
self.color = color
}
init(source: Shape) {
self.x = source.x
self.y = source.y
self.color = source.color
}
}
Rectangle과 Circle을 정의합니다.
class Rectangle: Shape {
var width: Int
var height: Int
init(width: Int, height: Int) {
self.width = width
self.height = height
super.init()
}
init(source: Rectangle) {
self.width = source.width
self.height = source.height
super.init(source: source)
}
override func clone() -> Rectangle {
Rectangle(source: self)
}
}
class Circle: Shape {
var radius: Int
init(radius: Int) {
self.radius = radius
super.init()
}
init(source: Circle) {
self.radius = source.radius
super.init(source: source)
}
override func clone() -> Circle {
Circle(source: self)
}
}
클라이언트 코드에서는 다음과 같이 복사됩니다.
var circle: Circle = Circle(radius: 20)
circle.x = 10
circle.y = 10
let copyCircle: Circle = circle.clone()
6. 장단점
Prototype 패턴의 장점은 객체의 상태를 유지하면서 필요한 값을 지닌 맞춤형 객체 생성 비용을 절감할 수 있다는 점입니다.
하지만 클래스 내부에 다른 클래스가 존재할 경우 복사의 깊이가 깊어지면서 구현이 복잡해질 수 있습니다.
또한 객체를 계속해서 복사할 경우 메모리 사용량이 증가합니다.
7. 실제 사용 사례
사실 Swift는 ValueType
과 ReferenceType
의 개념이 존재하기 때문에 참고 사이트에서의 설명과는 접근법이 조금 다를 수 있고, NSCopying
프로토콜이 존재하여 이미 복제 기능을 제공하고 있습니다.
따라서 Shallow Copy(얕은 복사)와 Deep Copy(깊은 복사) 개념에 대해서만 추가적으로 이해하면 좋다고 생각합니다.
Shallow Copy
Swift에서 Shallow Copy는 실제 값의 복사가 아닌 주소 값의 복사를 의미합니다.
Reference Type
의 특징으로 클래스 인스턴스를 다른 변수에 저장할때 새로운 변수가 기존 변수의 객체 값에 영향을 줍니다.
class Point {
var x: Int
var y: Int
init(x: Int = 10, y: Int = 10) {
self.x = x
self.y = y
}
}
let originPoint = Point()
let copyPoint = originPoint
originPoint.x += 100
print(copyPoint.x)
또한 객체가 복사될 때 Value Type
의 복사는 이뤄지지만, Reference Type
은 여전히 주소 값 복사가 이뤄지는 경우를 의미합니다.
NSCopying
을 통해 클래스 객체를 복사해봅시다.
class Point: NSCopying {
var x: Int
var y: Int
init(x: Int = 10, y: Int = 10) {
self.x = x
self.y = y
}
func copy(with zone: NSZone? = nil) -> Any {
return Point(x: self.x, y: self.y)
}
}
let originPoint = Point()
let copyPoint = originPoint.copy() as! Point
originPoint.x += 100
print(copyPoint.x)
// 10
두 객체의 값이 독립적으로 작동합니다. 하지만 Point
내부에 또 Reference Type
이 있다면 어떻게 될까요?
class Owner {
var name: String
var age: Int
init(name: String = "Rey", age: Int = 999) {
self.name = name
self.age = age
}
}
class Point: NSCopying {
var x: Int
var y: Int
var owner: Owner
init(x: Int = 10, y: Int = 10) {
self.x = x
self.y = y
self.owner = Owner()
}
func copy(with zone: NSZone? = nil) -> Any {
let point = Point(x: self.x, y: self.y)
point.owner = self.owner
return point
}
}
let originPoint = Point()
let copyPoint = originPoint.copy() as! Point
originPoint.x += 100
print(copyPoint.x) // 10
originPoint.owner.name = "Swift"
print(copyPoint.owner.name) // Swift
예상대로 Int
프로퍼티는 복사가 되었지만, owner
값은 아직 주소를 공유하는듯 합니다.
Owner
가 struct(Value Type)
일 때는 어떻게 작동하는지도 직접 해보시기 바랍니다.
Deep Copy
Shallow Copy와는 반대로 모든 프로퍼티의 값 복사가 이뤄지는 것을 말합니다.
Swift의 struct(Value Type)
는 기본적으로 Deep Copy가 이뤄집니다.
원본 객체와 복사된 객체가 완전한 독립성을 갖습니다. 각 객체에 상태가 변화가 일어나도 서로에게 영향을 주지 않습니다.
8. Conclusion
복잡한 객체 생성 비용을 줄이거나, 동일한 객체를 여러 번 초기화하는 상황에서 사용할 수 있습니다.
종합적으로 생각했을 때 Swift 활용한 iOS 내에서는 실제로 Prototype 패턴을 직접 구현하는 것은 오히려 코드의 복잡성을 증가시킬 수 있습니다.
패턴의 개념적인 이해 후 현재 다루는 데이터의 Type에 따라 NSCopying
사용 혹은 단순 데이터 복제를 통해 데이터를 사용하는 것이 중요하다고 생각합니다.
또한 Swift 내 타입의 특성상 내부 프로퍼티에 ReferenceType
이 있는지 확인하여 복사의 깊이를 조절해야합니다.
Reference
https://refactoring.guru/design-patterns/prototype
Prototype
/ Design Patterns / Creational Patterns Prototype Also known as: Clone Intent Prototype is a creational design pattern that lets you copy existing objects without making your code dependent on their classes. Problem Say you have an object, and you want to
refactoring.guru
'Design Pattern > GoF - Creational Patterns' 카테고리의 다른 글
[GoF Design Patterns] Singleton (0) | 2024.10.18 |
---|---|
[GoF Design Patterns] Builder (0) | 2024.09.28 |
[GoF Design Patterns] Abstract Factory - 실제 사용 예제 (1) | 2024.09.26 |
[GoF Design Patterns] Abstract Factory (0) | 2024.09.26 |
[GoF Design Patterns] Factory Method (0) | 2024.09.24 |