jane1choi/TIL

[Design Pattern] 생성 패턴 - Builder Pattern

Closed this issue · 0 comments

생성패턴이란?

생성패턴은 객체의 생성에 관련된 패턴으로 객체의 생성절차를 추상화하는 패턴입니다.
객체를 생성-합성하는 방법 / 객체의 표현방법과 시스템을 분리합니다.

생성패턴의 특징

  1. 생성패턴은 시스템이 어떤 구체 클래스(구체적인 클래스)를 사용하는지에 대한 정보를 캡슐화합니다.
  2. 생성패턴은 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 서로 맞붙는지에 대한 부분을 완전히 가립니다.
    즉, 객체의 생성과 조합을 캡슐화해 특정 객체가 생성되거나 변경되어도 프로그램 구조에 영향을 크게 받지 않도록 유연성을 제공합니다.

생성패턴 종류

  1. 추상 팩토리 패턴(Abstract Factory Pattern)
    : 동일한 주제의 다른 팩토리를 묶어 준다.
  2. 빌더 패턴(Builder Pattern)
    : 생성과 표기를 분리해 복잡한 객체를 생성한다.
  3. 팩토리 메서드 패턴(Factory Method Pattern)
    : 생성할 객체의 클래스를 국한하지 않고 객체를 생성한다.
  4. 프로토타입 패턴(Prototype Pattern)
    : 기존 객체를 복제함으로써 객체를 생성한다.
  5. 싱글턴 패턴(Singleton Pattern)
    : 한 클래스에 한 객체만 존재하도록 제한한다.

Bulider Pattern(빌더 패턴)

Builder(빌더) 디자인 패턴은 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차를 통하여 서로 다른 결과를 만들 수 있게 해주는 패턴입니다.
즉, 생성 절차는 항상 동일하되 결과는 다르게 만들어주는 디자인 패턴으로, 내부 프로퍼티가 굉장히 많은 객체이거나 복잡한 객체를 생성할 때 유용하게 사용되는 패턴입니다.
객체 내의 여러 속성들에 대해서 체이닝 형식으로 생성할 수도 있습니다.
→ 여러 속성을 가진 복잡한 객체를 간결하게 생성하기 위해 사용.

스크린샷 2022-06-23 오후 11 52 31

빌더 패턴은 위의 그림과 같이 3가지 요소로 나눌 수 있습니다.

  • Product
    : 생성하고 싶은 객체(구조체나 클래스 객체)
  • Builder
    : Product를 생성 및 반환해주는 구성요소
  • Director
    : Builder를 이용해서 필요한 product를 받아서 처리하는 구성 요소. 사용하는 용도에 따라 Director를 이용할 수 있고, 직접 Builder를 접근해서 product 반환할 수 있기 때문에 필수 구성요소는 아님

예시를 통해 어떻게 사용하는지 자세히 봅시다!

Bulider Pattern 사용해보기

먼저 Bulider의 프로토콜부터 살펴보면,

protocol BuilderType { 
	associatedtype Product 

	func build() -> Product 
}

위와 같이 BuilderType이라는 프로토콜과 같은 형태의 구조를 띄게 됩니다.
Builder 객체가 생성할 클래스인 Product와, 이를 생성하는 메서드인 build()를 갖고 있습니다.

그럼 이제 text, font, textColor, textAlignment를 초기화할 수 있는 UILabel의 Builder를 만들어보겠습니다!

extension UILabel { 
	typealias Builder = UILabelBuilder 
} 

class UILabelBuilder: BuilderType {
  private var frame: CGRect = .zero 
	private var text: String? = nil 
	private var font: UIFont? = nil 
	private var textColor: UIColor? = nil 
	private var textAlignment: NSTextAlignment = .left 

func withFrame(_ frame: CGRect) -> UILabelBuilder {
	self.frame = frame 
	return self 
} 

func withText(_ text: String?) -> UILabelBuilder { 
	self.text = text 
	return self 
} 

func withFont(_ font: UIFont?) -> UILabelBuilder {
	self.font = font 
	return self 
} 

func withTextColor(_ textColor: UIColor?) -> UILabelBuilder {
	self.textColor = textColor 
	return self 
} 

func withTextAlignment(_ textAlignment: NSTextAlignment) -> UILabelBuilder {
	self.textAlignment = textAlignment 
	return self 
} 

func build() -> UILabel { 
	let label: UILabel = .init(frame: self.frame) 
	label.text = self.text 
	label.font = self.font 
	label.textColor = self.textColor 
	label.textAlignment = self.textAlignment 
	return label 
 } 
}

먼저 UILabel의 extension에 typealias(타입 별칭)를 사용해 UILabelBuilder가 UILabel의 내부 클래스 Builder인것 처럼 사용할 수 있게 해주었습니다.
UILabelBuilder의 내부 프로퍼티들은 build() 메서드 호출 시에 UILabel를 만들고 초기화(==생성)하는데 사용됩니다.
각각의 프로퍼티는 withProperty() 메서드를 사용해 값을 넣어줄 수 있습니다.
이때 set이 아니라 with를 사용한 이유는 값을 변경한 이후 자기 자신을 반환해 메서드 체이닝이 가능하다는 것을 명시적으로 표현해주기 위함입니다. (일반적으로 setter는 값을 반환하지 않기 때문)

  • UILabel 구현 비교해보기
// 절차적 구현
let label: UILabel = .init(frame: .zero) 
label.text = "hello world" 
label.font = .systemFont(ofSize: 14) 
label.textColor = .white label.textAlignment = .center 

// 클로저를 사용한 구현 
let label: UILabel = { 
let label: UILabel = .init(frame: .zero) 
label.text = "hello world" label.font = .systemFont(ofSize: 14) 
label.textColor = .white label.textAlignment = .center 
return label 
}() 

// 빌더 패턴을 사용한 구현 (체이닝 형태 가능)
let label: UILabel = UILabel.Builder() 
.withText("hello world") 
.withFont(.systemFont(ofSize: 14)) 
.withTextColor(.white) 
.withTextAlignment(.center) 
.build()

빌더 패턴의 장점

위에서 비교 코드를 보면 사실 코드의 길이 자체는 드라마틱하게 줄지는 않습니다.
하지만, 복잡한 생성자를 줄일 수 있고, 인스턴스의 호출 횟수를 줄일 수 있기 때문에 실제 코드를 구현해보면
label의 중복이 빠진 것만으로도 구현 속도에서 차이가 나는 것을 경험할 수 있습니다.

또한, label의 중복이 빠져서 생성 및 초기화 코드와 이후에 등장할 로직에 관련된 코드를 명확하게 분리할 수 있습니다.
클로저를 사용한 구현이 이러한 코드의 분리를 위해 사용되고는 하는데, 빌더 패턴을 사용하게 되면 클로저를 사용하는 것보다 코드의 길이는 더 짧아지면서 의미는 명확해지는 것을 확인할 수 있습니다.

빌더 패턴의 단점

생성 및 프로퍼티 지정이 단순한 경우에는 빌더 패턴이 오히러 복잡성을 증가시킵니다.