디자인 패턴, 팩토리 메서드 in Swift

iOS 개발자로 막 취업에 성공한지 약 4개월이 조금 넘었다.
지금까지 일을 하면서 느낀 것은 역시 기초인 것 같다. 물론 회사에서 내가 성장할 때까지 일을 시키지 않거나 그런 것은 아니다. 짧은 기간이지만 바쁘 때는 새벽까지 개발을 하면서 지내고 있다..!

몸은 힘들지만 내가 이 회사의 일원이 되어간다는 기쁨으로 하루는 재밌게 보내고 있는 것 같다 ㅎㅎ

회사에서 새로운 기술이나 라이브러리를 접목하는 것은 쉬운 결정이 아닌 것 같다. 때문에 내가 당장 공부하고 잘 적용할 수 있는 것이 기초적인 문법, CS, 그리고 패턴과 같은 공부라고 생각했다.

때문에 틈틈히 다시 공부를 시작할 것이고 그 처음이 디자인 패턴이 될 것 같다.

생성 패턴이란?

생성 패턴은 객체의 생성과 연관된 패턴이다. 생성된 객체들을 어떻게 조합하고 합성할지에 대해 다룬다.

생성 패턴의 특징

  • 시스템이 어떤 구체적인 클래스를 사용하는지에 대한 정보를 *캡슐화 한다.
  • 객체가 언제, 어디서, 어떻게 생산되는지를 다루고 이를 통해 유지보수와 확장성을 향상시키는 것을 목표로 한다.

* 캡슐화란? – 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것을 말한다.

다음과 같은 특징을 가진다.

  • 데이터 보호: 외부로부터 클래스에 정의된 속성과 기능을 보호한다.
  • 데이터 은닉: 내부의 동작을 감추고 외부에는 필요한 부분만 노출한다.

오늘은 생성 패턴 중 팩토리 메서드 패턴에 대해서 다룰 예정이다.

팩토리 메서드 패턴이란?

객체 생성을 캡슐화하여 *클라이언트 코드가 객체 생성 과정에 종속되지 않도록 하는 디자인 패턴이다.

*클라이언트 코드란? – 간단히 말해 우리가 만든 코드를 사용하는 쪽의 코드를 말한다. 예를 들어 usecase를 사용하는 viewmodel이 usecase의 클라이언트 코드라고 할 수 있다.

팩토리란?

인스턴스의 생성을 담당한다. 예를 들어 골리앗, 탱크, 벌쳐의 생성을 팩토리에서 담당하듯 각 인스턴스의 생성을 팩토리라는 객체에서 담당하는 것을 말한다.

왜 팩토리에서 인스턴스 생성 역할을 맡길까?

생성하는 역할과 구현하는 역할의 결합을 피할 수 있다. 예시 코드로 파악해 보겠다.

protocol Unit {
    var hp: Int { get set }
    
    init(hp: Int)
    
    func attack()
    func move()
    func getHp()
}

먼저 공통의 기능을 프로토콜로 빼낸다.

class Goliath: Unit {
    var hp: Int
    
    required init(hp: Int) {
        self.hp = hp
    }
    
    func attack() {
        print("골리앗 공격")
    }
    
    func move() {
        print("골리앗 이동")
    }
    
    func getHp() {
        print("골리앗의 HP \(hp)")
    }
}

class Tank: Unit {
    var hp: Int
    
    required init(hp: Int) {
        self.hp = hp
    }
    
    func attack() {
        print("탱크 공격")
    }
    
    func move() {
        print("탱크 이동")
    }
    
    func getHp() {
        print("탱크의 HP \(hp)")
    }
}

class Vulture: Unit {
    var hp: Int
    
    required init(hp: Int) {
        self.hp = hp
    }
    
    func attack() {
        print("벌쳐 공격")
    }
    
    func move() {
        print("벌쳐 이동")
    }
    
    func getHp() {
        print("벌쳐의 HP \(hp)")
    }
}

프로토콜을 채택에 각 유닛을 만들어준다.

이를 사용하는 쪽에선
enum UnitFactory {
    enum UnitType {
        case tank
        case vulture
        case goliath
    }

    static func createUnit(type: UnitType) -> Unit {
        switch type {
        case .tank:
            return Tank(hp: 200)
        case .vulture:
            return Vulture(hp: 100)
        case .goliath:
            return Goliath(hp: 150)
        }
    }
}

팩토리 객체는 해당 유닛 인스턴스를 만드는 역할을 수행한다.

class Client {
    let unit1: Unit
    let unit2: Unit
    
    init(unit1: Unit, unit2: Unit) {
        self.unit1 = unit1
        self.unit2 = unit2
    }
    
    func attack1() {
        unit1.attack()
    }
    func attack2() {
        unit2.attack()
    }
}

// 클라이언트(사용하는 쪽)에서 객체 생성에 대한 관심사를 팩토리가 담당함
let client = Client(unit1: Vulture(hp: 100), unit2: UnitFactory.createUnit(type: .vulture))

Client의 객체 생성을 보면 unit1은 일반적인 객체 생성, unit2는 팩토리 메서드를 활용한 객체 생성이다.
객체 생성에 대한 관심사를 팩토리가 담당하는 것을 알 수 있다.

공부하고 느낀점

기능을 사용하는 클라이언트 쪽에서 기능 객체를 생성하는 것에 대한 관심사를 나눌 수는 있었다.
-> 기능을 수정해야 할 때 클라이언트 코드에서는 수정이 필요 없다, 팩토리에서 수정하면 된다.
하지만, 팩토리 객체를 만들면서까지 관심사를 나눌 필요가 있을지는 의문이 들었다.

예를 들어 테스트 코드 작성 시, Mock 객체들의 여러가지 케이스를 만드 때 사용해볼 수 있지 않을까 생각해봤다.

Leave a Comment