본문 바로가기
Dev/고민과 삽질의 기록들🤔

객체 생성에 대한 책임 분리를 위한 Factory Pattern 도입기

by Mintta 2023. 10. 25.

https://github.com/Team-LionHeart/LionHeart-iOS

 

GitHub - Team-LionHeart/LionHeart-iOS: 라이옹 🦁

라이옹 🦁. Contribute to Team-LionHeart/LionHeart-iOS development by creating an account on GitHub.

github.com

이전글

https://codingmon.tistory.com/64

 

Coordinator Pattern 도입 이유와 실제 도입기

https://github.com/Team-LionHeart/LionHeart-iOS GitHub - Team-LionHeart/LionHeart-iOS: 라이옹 🦁 라이옹 🦁. Contribute to Team-LionHeart/LionHeart-iOS development by creating an account on GitHub. github.com Coordinator Pattern을 공부를 시

codingmon.tistory.com

 

Coordinator Pattern 적용기에 이어서 Factory Pattern 적용기 입니다. 그 과정에서 많은 시행착오가 있었는데요 그 과정이 절대 간단하지 않았고 그 내용도 많아서 여러 게시글로 나누어서 이렇게 작성하고 있습니다.

 

오늘은 Factory Pattern을 도입하기까지의 의사결정 과정과 프로젝트 실제 도입기에 대해 정리하여 남겨보고자 합니다.

 


책임 분리의 trade off로 생겨난 수많은 의존성 객체들

Coordinator에서 ViewController를 생성을 하게 된다면 위와 같은 말도안되는 의존성이 꼬리에 꼬리를 무는 객체를 생성해줘야했고, 이같은 코드를 여러곳에서 반복해서 사용해야하는 문제점이 있었습니다.

 

또한, 화면전환 로직을 담당하는 Coordinator가 객체 생성에 대한 책임을 지닐 필요가 없다 판단했고, 객체 생성에 대한 책임을 전담해줄 방법을 찾게 됩니다..!

 

그 과정에서 저번 글에서 의존성 객체를 관리해주기 위해서 Custom DI Container를 만들기를 시도했고, 그 과정에서 생겼던 문제점을 해결하기 위해서 Swinject 내부 코드를 살펴보았고, Swinject에서 해당 문제점을 해결한 방법을 엿볼 수 있었습니다.

(그 과정이 궁금하다면 아래 링크로 !)

https://codingmon.tistory.com/60

 

Dependency Container 구현 (Swinject 파헤치기)

https://github.com/Team-LionHeart/LionHeart-iOS GitHub - Team-LionHeart/LionHeart-iOS: 라이옹 🦁 라이옹 🦁. Contribute to Team-LionHeart/LionHeart-iOS development by creating an account on GitHub. github.com Dependency Container 구현 (Swinject

codingmon.tistory.com

 

Swinject를 사용하면 Custom으로 DI Container를 구현할 때에는 해결하기 쉽지 않았던 문제점까지 해결된 Container를 사용할 수 있습니다. 이 container자체가 싱글톤은 아니기에 이 container를 가진 Wrapper class를 싱글톤으로 만들어주어 Swinject의 Container를 사용하는 DI Container를 구축해서 사용하는 방법이 있었습니다.

 

그리고 또 다른 방법으로 Factory Pattern을 통한 방법 또한 존재했습니다. Factory Pattern에 대해서는 글의 뒤쪽에서 자세히 다뤄보겠습니다 !

 

그리고 저희는 고민 끝에 결론적으로 저희는 Factory Pattern을 사용해서 ViewController를 객체 생성의 책임으로부터 분리시키기로 결정하였고 이를 적용하였습니다.

 

그 이유에 대한 설명으로 글을 이어나가보겠습니다.

Swinject 대신 Factory를 선택하게 된 이유

Swinject는 DI Framework로 소개되고 Injection의 예시로 아래와 같은 코드를 예시로 깃헙에서 소개하고 있습니다.

Swinject를 Petowner를 생성할 때 Pet이라는 객체를 의존성으로 가지고 있는데, 간단하게 resolve 메서드를 통해서 객체를 가져와 주입해줄 수 있습니다.

 

하지만 제가 생각했을 때 Swinject는 DI를 해주는 Framework라기보다는 단순히 Service Locator의 느낌이지 않나 싶었습니다.

DI를 외부에서 객체를 생성해서 넣어주는 것 뿐만이 아닌, 의존성 또한 분리해주는 것까지라고 생각한다면 DI는 결국 개발자의 몫으로 남아있는 것이 아닌가 하고 생각했습니다.

(Swinject를 본격적으로 사용해본 것이 아니라서 이런 생각이 드는 걸 수도 있을 것 같습니다...!)

 

그렇게 생각한다면 Service locator만의 역할을 수행하기에는 Swinject뿐만 아니라 Factory Pattern을 통해서도 이를 달성할 수 있을 것이라고 판단했습니다. 그렇다면 Swinject에서 제공해주는 다른 object scope등의 기능이 필요한 상황도 아닌 현상황에서 service locator역할만을 수행하기 위해 외부 라이브러리 Swinject를 사용하는 것은 과하다는 생각이 들었습니다.

 

따라서 Factory Pattern을 이용하기로 결정했습니다.

Factory Pattern을 구현하는 방법들에 대한 고민

방법 1. 간단한 팩토리 (Simple Factory)

image

protocol AuthFactory: AnyObject {
    func makeLoginVC() -> LoginViewController
    func makeRegisterVC() -> RegisterViewController
}

final class AuthFactoryImpl: AuthFactory {  
    func makeLoginVC() -> LoginViewController {
        let loginVC = LoginViewController()
        return loginVC
    }
}

 

실제 객체 타입을 return하게끔 하는 제일 간단한 형태의 Factory입니다.


하지만 이렇게 구현했을 때 AuthFactory에서의 return값이 concrete type이기 때문에 해당 Factory를 주입받는 Coordinator가 실제 객체를 바라보게 되고 해당 객체에 매우 의존적이게 됩니다.

 

따라서 결합도를 낮추기 위해서 ViewControllerable과 같은 interface를 바라보게 함으로써 DIP를 시켜줘야할 필요가 있습니다. 또한 이렇게 구현함으로써 온전한 캡슐화를 할 수 있습니다.

방법 2. 팩토리 메서드 패턴

image

두번째 방법인 팩토리 메서드 패턴입니다.

 

protocol Factory {
    func makeViewController() -> ViewControllerable
}

final class AuthFactoryImpl: Factory {
    func makeViewController() -> ViewControllerable {}
}

final class OnboardingFactoryImpl: Factory {
    func makeViewController() -> ViewControllerable {}
}

 

ViewController에서는 Factory 프로토콜 타입의 객체를 주입받고. 어떤 객체(AuthFactoryImpl, OnboardingFactoryImpl)를 주입받느냐에 따라 생성되는 ViewController가 달라지는 매우 유연한 방법입니다.


하지만 이렇게 했을 때 아래와 같은 문제점들이 있었습니다.

문제점 1. Factory Protocol 구성의 까다로움

한 Coordinator에서 필요한 여러개의 ViewController를 만들기 위한 Factory protocol을 구성하기가 까다롭습니다.
(어떤 Coordinator는 단 하나의 객체만 만들면 되지만 어떤 Coordinator는 2~3개의 객체를 생성해야합니다.)

문제점 2. ViewController를 config할 방법의 부재

Coordinator에서 바라보고 있는 타입이 ViewControllerable 프로토콜이기 때문에 기존의 ViewController에서 config를 해주고 싶을 때에 프로토콜에는 해당 값이 없기 때문에 문제가 있었습니다.

 

따라서 위 문제점들을 해결하기 위해 각각의 ViewController에 필요한 데이터와 coordinator를 추상화한 interface를 1:1로 가지고 있게 만들어 위의 구조를 유지한채 문제를 해결했습니다.

image

현재 LionHeart의 Factory pattern 구조

점선은 객체가 구현체가 아닌 프로토콜을 바라보고 있다 로 이해하면 됩니다

  1. ViewController는 각각의 unique한 ViewControllerable protocol을 가지고 있고, 해당 interface는 Coordinator(각각의 navigation interface type)와 데이터전달이 필요한 경우엔 해당 데이터를 넣어주는 메서드를 추상화하고 있습니다.
protocol CurriculumArticleByWeekControllerable where Self: UIViewController {
    func setWeekIndexPath(week: Int)
    var navigator: CurriculumListByWeekNavigation { get set }
}
  1. 각각의 coordinator는 unique한 factory interface를 가지고 있어, Coordinator와 Factory는 1:1 관계입니다. 그리고 Factory에서는 Coordinator에서 필요한 ViewControllerable타입을 return해줍니다.
    이 때 각각의 Coordinator는 1:1 대응되는 Factory 객체를 외부에서 주입받습니다.
protocol CurriculumFactory {
	func makeCurriculumListViewController(coordinator: CurriculumCoordinator) -> CurriculumArticleByWeekControllerable
}

 3. 구현체(impl)는 해당 객체를 주입받는 Coordinator와 1:1 관계이며, 어떠한 프로퍼티 또한 존재하지 않아 identity등에 대한 고민이 필요없기에 struct로 구현했습니다

이 근거에 대한 이야기를 더 듣고 싶다면 아래 링크를 참고해주세요 !

https://codingmon.tistory.com/66

 

[Swift] 객체 구현시에 class와 struct 어떤게 맞을까 ?? 🤔

제가 class와 struct을 정할 때 생각하는 기준들을 나열해보고자 합니다. 어떤 객체를 만들 때 그 객체를 class로 만들지, struct으로 만들지에 대한 고민이 늘 항상 개발하면서 있었던 것 같아 그 기준

codingmon.tistory.com

struct CurriculumFactoryImpl: CurriculumFactory {
    func makeCurriculumListViewController() -> CurriculumArticleByWeekControllerable {
        let apiService = APIService()
        let curriculumService = CurriculumServiceImpl(apiService: apiService)
        let bookmarkService = BookmarkServiceImpl(apiService: apiService)
        let curriculumListManager = CurriculumListManagerImpl(bookmarkService: bookmarkService, curriculumService: curriculumService)
        let curriculumListViewController = CurriculumListByWeekViewController(manager: curriculumListManager)
        return curriculumListViewController
    }
    ...
}

마무리

객체들의 책임 분리 여정, 첫번째는 화면전환 책임을 전담하는 Coordinator. 두번째로 객체 생성에 대한 책임을 전담해줄 Factory Pattern까지 적용해보았습니다.

 

디자인 패턴을 기존 환경 내에서 반복적으로 일어나는 문제들을 어떻게 풀어나갈 것인가에 대한 일종의 솔루션이라고 하는데, 뭔가 그 흐름에 맞게 솔루션을 적용해나가면서 문제점을 해결해나가고 있는 것 같아서 기분이 좋습니다. 과정이 녹록치는 않지만 객체간의 책임이 명확하게 분리된 객체간의 관계도를 보면 확실히 뿌듯합니다.

 

이제 마지막 Adapter만을 남겨두고 있네요. Adapter에 대한 글을 끝으로 1차 리팩토링에 대한 글은 드디어 다 쓰게 될 것 같습니다. 조금 더 화이팅해봐야겠습니다! 여기까지 읽어주신 분이 있으실진 모르겠지만 감사합니다 !!

댓글