본문 바로가기

다우 & iOS/[Swift] Design Pattern

스위프트에서 빌더 패턴 구현

두끼 전건우

UIKit을 사용해 iOS 개발을 하다보면 개발 속도 향상 등의 이유로 인해 Storyboard를 사용하지 않고 코드만으로 UI를 구성하게 되는 경우가 종종 있다. UI를 코드로 반복해서 구현하다 보면 UI Component 마다 자주 호출되는 Property가 따로 있다는 것을 알게된다. 예를 들어 UILabel을 구현할 땐 경험상 text, font, textColor, textAlignment 순으로 자주 호출한다. UILabel 객체를 Builder 패턴을 사용해 생성하는 예제를 통해 자주 사용되는 Property를 어떻게 간단히 초기화할 수 있는지 알아보자.

Builder 패턴이란 여러 속성을 가진 복잡한 객체를 간결하게 생성하기 위해 사용한다. 먼저 Builder의 Protocol을 살펴보자.

protocol BuilderType {
  associatedtype Product

  func build() -> Product
}

Bulider의 Protocol은 간단하다. 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의 Property 들은 build() 메서드 호출 시에 UILabel를 만들고 초기화하는데 사용된다. 각각의 Property는 withProperty() 메서드를 사용해 값을 넣어줄 수 있다. 이때 set이 아니라 with를 사용한 이유는 값을 변경한 이후 자기 자신을 반환해 메서드 체이닝이 가능하다는 것을 명시적으로 표현해주기 위함이다. 일반적으로 setter는 값을 반환하지 않기 때문이다.

그럼 흰색 글씨, 시스템 폰트에 크기가 14, 가운데 정렬된 hello world를 표시하는 UILabel을 Builder 패턴을 사용하지 않은 방법과 Builder 패턴을 적용한 방법으로 각각 구현한 예제를 살펴보자.

// 절차적 구현
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의 중복이 빠져서 생성 및 초기화 코드와 이후에 등장할 로직에 관련된 코드를 명확하게 분리할 수 있다. 클로저를 사용한 구현이 이러한 코드의 분리를 위해 사용되고는 하는데 빌더 패턴을 사용하게 되면 클로저를 사용하는 것보다 코드의 길이는 더 짧아지면서 의미는 명확해지는 것을 확인할 수 있다.

끝.


잘못된 내용이나 표현을 발견하신 분 혹은 궁금한 점이 있으신 분은 댓글로 남겨주세요.

'다우 & iOS > [Swift] Design Pattern' 카테고리의 다른 글

스레드 안전한 객체 구현  (0) 2019.10.28