Factory patterns in swift

Park Jong Ho·2022년 12월 21일
0

Factory method pattern의 등장 배경

Factory method pattern은 인스턴스 생성과 관련된 디자인 패턴으로, 구현 타입 인스턴스의 생성에 대한 책임을 분리하기 위해서 고안된 디자인 패턴이다.

말로만 하면 이해하기 어려우니 코드를 보며 이해해보도록 하자.

protocol IntFilter {
    func filter(_ array: [Int]) -> [Int]
}

Int 배열을 필터링해서 새로운 배열을 반환하는 filter 함수를 가지는 IntFilter라는 프로토콜이 존재한다. 우리 프로그램이 가지는 요구사항이라고 생각해보자.

사용자의 요구에 따라 짝수, 홀수만 반환하는 필터, 기준 숫자보다 작은 숫자들만 반환하는 필터, 또 특정 Int 값을 제외한 숫자들만 반환하는 필터를 다음과 같이 구현할 수 있다.

// 짝수만 반환하는 필터
struct EvenFilter: IntFilter {
    func filter(_ array: [Int]) -> [Int] {
        return array.filter { $0 & 1 == 0 }
    }
}
// 홀수만 반환하는 필터
struct OddFilter: IntFilter {
    func filter(_ array: [Int]) -> [Int] {
        return array.filter { $0 & 1 == 1 }
    }
}
// 특정 숫자보다 작거나 같은 숫자들만 반환하는 필터
struct LowerFilter: IntFilter {

    let criteria: Int

    func filter(_ array: [Int]) -> [Int] {
        array.filter { $0 <= criteria }
    }
}
// 특정 숫자들을 제외한 숫자들만 반환하는 필터
struct ExcludeFilter: IntFilter {
    let exclude: Set<Int>

    func filter(_ array: [Int]) -> [Int] {
        array.filter { !exclude.contains($0) }
    }
}

그리고 유저가 화면에서 원하는 필터를 선택하면, 필터링된 숫자 배열이 화면에 표시된다고 생각해보자.

final class ViewController: UIViewController {
  private var selectedFilter: IntFilter {
    didSet {
      // 새로 필터링함...
    }
  }
  
  // 사용자의 선택에 따라 필터를 바꾸는 함수
  func onEvenFilterButtonTapped() {
    selectedFilter = EvenFilter()
  }
  
  // 사용자의 선택에 따라 필터를 바꾸는 함수
  func onLowerFilterButtonTapped(_ value: Int) {
    selectedFilter = LowerFilter(criteria: value)
  }
}

그렇다면 의존성 다이어그램은 다음과 같다.

클라이언트가 아무리 추상화된 타입에 의존한다고 하더라도, 결국에 어떤 객체들은 구체 타입을 생성하는 역할을 수행할 수 밖에 없다. 그리고 그 역할은 위에서 IntFilter를 사용하는 ViewController가 수행하고 있다.

이 코드의 문제점이 무엇일까? 바로 ViewController는 추상 타입에 의존해야 하지만, 생성의 책임 때문에 구체 클래스에도 의존하고 있다는 점이다. 물론 구체 타입의 생성에만 관여하고 있어서, 당장은 그 단점이 별로 느껴지지 않을 수 있다.

그러나 초기화 시 굉장히 많은 정보를 필요로 하는 어떤 필터가 존재한다고 가정해보자. 그것도 아주 많이

struct SomeComplexFilter: IntFilter {
  let someInfo: String
  let someInfo2: String
  let someInfo3: Int
  let someInfo4: Int
  ....
}

그렇다면, ViewController는 이 타입의 인스턴스 또한 생성해야 한다. 만약 생성자가 바뀌거나, 생성하기 위해 필요한 전처리 작업 등이 변경될 경우엔, 이 구체타입에 의존하고 있는 클라이언트도 모두 수정해야 한다.

즉 생성에 대한 책임이 ViewController에 존재하기 때문에, 구체 타입들에 대한 불필요한 의존성 또한 생긴 것이다.

팩토리 패턴은 생성과 사용에 대한 역할을 분리하기 위해 생겨난 디자인 패턴이다.

Simple Factory

가장 간단한 Factory 패턴의 구현은, 바로 생성에 대한 책임을 Factory 라고 불리는 하나의 타입으로 옮기는 것이다. 즉 구체 클래스 생성에 대한 책임을 하나의 타입에다가 때려넣는 것이다.

struct FilterFactory {

    enum FilterType {
        case even
        case odd
        case lower(criteria: Int)
        case exclude(exclude: Set<Int>)
    }

    func createIntFilter(_ type: FilterType) -> IntFilter {
        switch type {
        case .even:
            return EvenFilter()
        case .odd:
            return OddFilter()
        case .lower(criteria: let criteria):
            return LowerFilter(criteria: criteria)
        case .exclude(exclude: let excludes):
            return ExcludeFilter(exclude: excludes)
        }
    }

}

가장 간단한 FilterFactory 타입을 구현했다. FilterFactory 타입은 입력값에 따라 적절한 IntFilter를 반환하는 함수를 가지고 있다. cretateIntFilter 함수는, Factory를 사용하는 클라이언트가 Filter의 구체 타입보다는 FilterFactory 내부의 enum에 의존하도록 변경해준다.

또 IntFilter의 구체 타입 생성에 대한 역할은, 모두 FilterFactory 내부에서만 이루어진다. 따라서 나중에 각 인스턴스를 생성하는 방식이 변경되더라도, 변경은 Factory 내부에서만 일어나게 된다.

따라서 VIewController는 더 이상 구체 타입에 의존하지 않는다.

또한 구체 타입의 생성에 대한 책임이 모두 Factory 내부에 있으므로, Factory 내부에서는 어떤 식으로 인스턴스가 반환되더라도 상관없다. 예를 들어서, 하나의 인스턴스를 생성하는데 굉장히 많은 비용이 소모되는 인스턴스 (DB, DateFormatter) 등의 경우, Factory 내부에서 인메모리로 캐싱 해놓고, 똑같은 인스턴스를 계속해서 반환시켜줄 수도 있다!

struct DBFactory {
  // DB 인스턴스가 굉장히 비용이 많이 드는 인스턴스라고 생각해보자!
  private let sql: DB // 프로퍼티로 인스턴스를 계속 갖고 있는다.
  private let mongo: DB // 프로퍼티로 인스턴스를 계속 갖고 있는다.
  
  func createDB(_ type: DBType) -> DB {
    /// 상황에 따라 똑같은 인스턴스를 계속 반환할 수 있다.
    return sql
    /// 상황에 따라 똑같은 인스턴스를 계속 반환할 수 있다.
    return mongo
  }
}

다만 여기도 단점은 존재하는데, 바로 enum 혹은 다른 방법을 사용해서 구체 타입의 인스턴스를 생성해야 하기 때문에, 새로운 필터들이 추가될 때마다 FilterFactory 내부 코드를 변경해야 한다는 점이다. (swift 문의 case 추가)

따라서 Factory 내부에 한정해서는, OCP 원칙을 달성할 수 없다.

Factory method 패턴

앞서 언급한 Simple Factory는 객체 생성에 대한 책임을 클라이언트로 분리시켜주지만, 새로운 구체 타입이 생성될 때마다 Factory 내부 코드를 수정해야 한다는 단점이 있었다 (swift 문의 case 추가)

따라서 이것을 해결하기 위해 나온 패턴이 Factory method 패턴으로, OCP 원칙을 달성하기 위해 나왔다. 원리는 간단한데, 바로 Factory에서 구체 타입을 생성하는 함수 자체를 추상화 시키는 것이다.

protocol FilterFactory {
  func createFilter() -> IntFilter
}

즉 Factory 자체를 추상화 시킴으로써, 새로운 구체 타입이 생성되면, 그 구체 타입을 생성하는 새로운 Factory를 생성하면 된다. 예를 들어 1000보다 큰 수만 반환하는 새로운 필터가 추가되었다고 생각해보자.

struct Over1000Filter: IntFilter {
    func filter(_ array: [Int]) -> [Int] {
        array.filter { $0 > 1000 }
    }
}

그렇다면, 이 Filter를 생성해서 반환하는 새로운 Factory를 추가하면 된다.

struct Over1000FilterFactory: FilterFactory {
  func createFilter() -> IntFilter {
    Over1000Filter()
  }
}

Over1000FilterFactory라는 구체 팩토리는, 여전히 IntFilter라는 프로토콜 타입을 반환하고 있으므로, FilterFactory를 사용하는 쪽에서 구체 타입에 의존할 일은 여전히 없다.

// ViewController.swift

Over1000FilterFactory().createFilter().filter([1,2,3,4,5])
EvenFilterFactory().createFilter().filter([1,2,3,4,5])

Client 쪽에서 더 이상 구체 Filter에 대한 의존성이 생기지 않는다. 또한 만약 새로운 필터가 추가될 경우, 해당 필터를 반환하는 새로운 팩토리를 구현하면 된다.

여기서 Factory 또한 추상화된 타입이기 때문에, 만약 ViewController 외부에서 Factory 를 주입한다면, ViewController는 구체 Filter 타입뿐 아니라 구체 Factory 타입에 대한 의존성도 사라지게 된다.

// ViewController.swift

final class ViewController: UIViewController {
  private var filterFactory: FilterFactory
  init(_ factory: FilterFactory) {
    self.filterFactory = factory
    super.init(nibname: nil, bundle: nil)
  }
}

따라서 Factory method 패턴은, OCP 원칙을 달성하면서 클라이언트 쪽에서 구체 클래스에 대한 의존성을 없앨 수 있다. 그러나, 구체 타입이 하나 추가될 수록 구체 Factory도 계속해서 추가되어야 하니까, 솔직히 코드의 규모가 엄청나게 큰 경우가 아니라면 별로 매력적으로 느껴지지 않는다.

References

https://bcp0109.tistory.com/367

https://refactoring.guru/ko/design-patterns/factory-method

profile
iOS 개발자입니다.

0개의 댓글