클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자입니다. 하지만 모든 프로그래머가 꼭 알아둬야 할 기법이 하나 더 있습니다. 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있습니다. 그 클래스의 인스턴스를 반환하는 단순한 정적 메서드입니다.
public static func valueOf(b: Bool) -> Bool {
return b ? true : false
}
지금 얘기하는 정적 팩터리 메서드는 디자인 패턴의 팩터리 메서드(Factory Method)와 다릅니다. 디자인 패턴 중에는 이와 일치하는 패턴은 없습니다.
클래스는 클라이언트에 public 생성자 대신 (혹은 생성자와 함께) 정적 팩터리 메서드를 제공할 수 있습니다. 이 방식에는 장점과 단점이 모두 존재합니다. 먼저 정적 팩터리 메서드가 생성자보다 좋은 장점을 알아봅시다.
이 덕분에 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있습니다. 따라서 (특히 생성비용이 큰) 같은 객체가 자주 요청되는 상황이라면 성능을 상당히 끌어올려 줍니다. 플라이웨이트 패턴(Flyweight pattern)도 이와 비슷한 기법이라 할 수 있습니다.
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있습니다. 이런 클래스를 인스턴스 통제(instance-controlled) 클래스라 합니다. 그렇다면 인스턴스를 통제하는 이유는 무엇일까요?
인스턴스를 통제하면 클래스를 싱글턴(singleton)으로 만들 수도, 인스턴스화 불가(noninstantiable)로 만들 수도 있습니다.
// 인스턴스화 불가
class NonInstanceClass {
private init() { }
public static func initMethod() -> NonInstanceClass {
return NonInstanceClass()
}
}
let n = NonInstanceClass.initMethod()
이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 ‘엄청난 유연성’을 선물합니다. 이러한 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있습니다.
class Animal {
private var hasWings: Bool?
private var hasLegs: Bool?
private var legCount: Int?
init(hasWings: Bool? = false, hasLegs: Bool? = false, legCount: Int? = nil) {
self.hasWings = hasWings
self.hasLegs = hasLegs
self.legCount = legCount
}
static func owl() -> Owl {
return Owl(hasWings: true, hasLegs: true, legCount: 2)
}
static func snake() -> Snake {
return Snake()
}
}
final class Owl: Animal {
func fly() {
print("fly")
}
}
final class Snake: Animal {
func crawl() {
print("crawl")
}
}
let owl = Animal.owl()
let snake = Animal.snake()
위와 같이 작성하게 되면 Owl이나 Snake의 실제 구현 클래스를 알지 않아도 Animal을 통해 생성할 수 있게 됩니다.
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없습니다.
class Animal {
private var hasWings: Bool?
private var hasLegs: Bool?
private var legCount: Int?
init(hasWings: Bool? = false, hasLegs: Bool? = false, legCount: Int? = nil) {
self.hasWings = hasWings
self.hasLegs = hasLegs
self.legCount = legCount
}
static func canFly(hasWings: Bool) -> Animal {
return hasWings ? Owl(hasWings: true, hasLegs: true, legCount: 2) : Snake()
}
}
서비스 제공자 프레임워크에 대해 설명해 놓은 글 - 이펙티브 자바 01. 정적 팩토리 메소드와 서비스 제공자 인터페이스 (JDBC 예제)
class A {
static AInterface of() {
// 명시했으므로 클래스 A의 정보가 메모리에 로드될 때
// Aimpl의 클래스 정보 또한 클래스 영역에 로드된다.
// 그래야 of 메서드가 실행될 때 Aimpl을 인스턴스화해서 돌려줄 수 있음
return Aimpl()
}
}
(내가 생각하기에) 이 방법의 장점은 DB 드라이버의 구현체를 아무데서도 명시하지 않아서, 모든 드라이버들이 컴파일은 되지만, Class.forName으로 명시한 클래스만 메모리에 로드되는 듯합니다.
// 인스턴스화 불가
class NonInstanceClass {
private init() { }
public static func initMethod() -> NonInstanceClass {
return NonInstanceClass()
}
}
class InheritedClass: NonInstanceClass {
}
let inheritedClass = InheritedClass() // 에러 발생 'InheritedClass' cannot be constructed because it has no accessible initializers
이런식으로 인스턴스화가 불가하도록 만들어진 클래스는 상속이 불가합니다. 불변타입으로 만들기 위해서는 이러한 제약이 장점으로 받아들여질 수도 있습니다.
정적 팩터리 메서드가 유용하게 사용될 수 있는 두가지 방법에 대해 소개하고자 합니다.
UIViewController의 하위 요소들을 configure할 때 보통은 아래와 같은 방법을 사용합니다.
class TitleLabel: UILabel {
override init(frame: CGRect) {
super.init(frame: frame)
font = .boldSystemFont(ofSize: 24)
textColor = .darkGray
adjustsFontSizeToFitWidth = true
minimumScaleFactor = 0.75
}
}
위와 같은 접근방식에 문제가 있는 것은 아니지만, 우리는 종종 같은 종류의 UI에 대해 세부 요소만 다른 여러 개의 하위 클래스를 갖게 됩니다.(TitleLabel
, SubtitleLabel
, FeaturedTitleLabel
등)
서브클래싱은 중요한 작업이지만, 현재 작업은 새로운 동작을 추가하는 것이 아닌 인스턴스화 하는 과정이어서 서브클래싱 하는 것이 이 목적에 부합한가 하는 의문이 듭니다. 그래서 어떤 동작을 가지고 있는 것이 아니라면, 정적 팩터리 메서드를 통해 새로운 인스턴스를 만들 수 있습니다.
private extension UILabel {
static func makeForTitle() -> UILabel {
let label = UILabel()
label.font = .boldSystemFont(ofSize: 24)
label.textColor = .darkGray
label.adjustsFontSizeToFitWidth = true
label.minimumScaleFactor = 0.75
return label
}
}
이런 접근의 장점은, 설정 부분을 실질적인 동작 부분과 분리할 수 있다는 것입니다. 또한 private 접근 제한자를 추가해 단일 파일로 범위를 지정할 수 있어서 앱의 일부에만 단일 기능을 가지도록 확장할 수 있습니다.
class ProductViewController {
private lazy var titleLabel = UILabel.makeForTitle()
}
그럼 위와 같이 간단하게 UI 요소를 만들 수 있습니다.
그리고 테스트 코드를 작성하는 경우가 있습니다. 특히 특정 모델에 의존하여 테스트 코드를 작성할 때, 보일러플레이트가 많이 발생하는 코드를 작성하게 되는 경우가 많아 읽기 어렵고 디버깅이 어려워질 수 있습니다.
이럴 때 정적 팩터리 메서드로 stub 데이터를 가진 모델객체를 생성하게끔 만들어 놓으면, 테스트 시 해당 메소드만을 호출하여 간단하게 stub을 가져다 쓸 수 있습니다.
extension User {
static func makeStub(permissions: Set<User.Permission>) -> User {
return User(
name: "TestUser",
age: 30,
signUpDate: Date(),
permissions: permissions
)
}
}
정적 팩토리 메서드의 이름을 지정하여 메인 앱에 추가하지 않고 테스트용으로만 사용할 수 있습니다. 이렇게 하게되면 코드를 실제 로직과 명확하게 구분할 수 있고, 깨끗한 테스트 코드를 쉽게 작성하는데에 도움이 됩니다.
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋습니다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고쳐야 합니다.
https://www.swiftbysundell.com/articles/static-factory-methods-in-swift/