Do.

Singleton Pattern vs Dependency Injection 본문

General Dev

Singleton Pattern vs Dependency Injection

Hey_Hen 2022. 6. 28. 12:09

Intro

코드를 작성하다 보면, 특정 기능을 다른 객체에 의존해야 하는 상황이 생깁니다. 간단한 예로 TIL 앱을 만들었다고 가정했을 때, 데이터를 저장하는 객체를 DataManager 와 같은 이름으로 만들어서 쓸 수 있겠죠.
이 의존성을 받는 객체는 유저의 액션에 따라, 로직 흐름에 따라 호출이 이루어지는데요. 호출을 하는 대표적인 두 가지 방법이 바로 Singleton Pattern과 Dependancy Injection입니다.
각 개념에 대해서는 별도로 소개하지 않겠습니다.

Singleton

두 개념은 서로 반대 진영에 있는 것으로 볼 수 있는데요, 우선 Singleton은 인스턴스를 전역에 올려두고, 어디에서든 쉽고 빠르게 접근할 수 있다는 장점이 있습니다.
아래 코드는 어떤 DataSource 클래스를 간단하게 Singleton으로 구현한 것입니다.

struct DataSource {

  static let shared = DataSource()

  private init() {}

  func fetchId() -> Int {
    0
  }

  func requestRegisterCoupon() {
    // some network request
  }

  func fetchSomeData(userId: String) -> Result<Data, Error> {
    return .success(Data())
  }

  func save(with data: Data) throws {
    // save...
  }

  func edit(with data: Data) throws {
    // edit...
  }
}

Singleton은 하나의 인스턴스를 전역에 올려두고 관리하는 것이기 때문에, 일반적인 생성자는 private로 막아두고 흔히 하는 shared를 통해서 접근할 수 있도록 합니다.

그래서 실제로 사용할 때는 아래와 같이 호출합니다.

let result = DataSource.shared.fetchSomeData(userId: id)

Singleton Pattern은 위 코드에서 보았듯 구현하기도 쉽고, 어디서든 빠르고 쉽게 접근 가능하기 때문에, 자주 선호됩니다. 그리고 핵심은 하나의 인스턴스로 관리하기 때문에, 인스턴스가 둘 이상 생성되어서는 안되는 상황을 보장할 수 있습니다.
하지만 너무 쉬운 코드, 사용의 자유는 다른 불편함을 낳기도 합니다.
Singleton의 대표적인 단점은 유지보수가 어려워 진다는 점입니다. 이 부분이 사람에 따라서는 크게 와 닿지 않을 수도 있는데요.
실제 서비스에서는 코드의 관리자는 빠르게 변합니다. 신규입사자가 들어오기도 하고, 관리한지 오래된 코드를 오랜만에 다시 열어볼 수도 있습니다.
그러니까 기존 코드에 대해서 모르는 상황에서 이 Singleton 객체는 굉장히 불친절합니다.
우선 Singleton객체가 굉장히 큰 책임을 가지고 있는 경우를 생각해 볼까요?
아주 많은 메서드가 있고 그것이 접근에 제한이 없다면 (다양한 UseCase나 View에서 Singleton을 호출하기 때문에, private로 접근 제한이 되어 있지 않을 것입니다)
DataSource.shared를 입력하고 한번 더 .을 누르면 일차적으로 멘탈에 금이갑니다.
대체 어느 메서드를 호출해야 원하는 결과를 얻을 수 있을까요? 모릅니다. 알아내기 위해서는 DataSource 클래스를 열어서 코드 한줄 한줄 파악해가며 해당 메서드가 무슨 역할을 하는지 알아내야 합니다
왜냐하면 Singleton 객체는 작성자에게 너무 큰 자유를 주었기 때문에, 담당하는 기능을 만들면서 손쉽게 Singleton에 기능을 추가했을 것이고, 비슷하거나 겹치는 이름을 피해 메서드 명이 점점 기괴해지거나 길어졌기 때문입니다.
이를 해결하기 위해 Facade 패턴을 이용해 너무 큰 Singleton 객체를 분리하는 방법 도 있습니다.
그리고 기능을 개발하기 위해 Singleton 객체가 이미 존재해야 하는 불편함이 있습니다. 다른 말로 Singleton 객체와 기능이 따로 개발 될 수 없습니다. 한 사람이 한번에 개발을 해야 하죠.

Dependency Injection

Dependency Injection은 어떤 기능이 의존해야 하는 객체를 외부에서 주입하는 방법을 말합니다. 이름 그대로 의존성 주입이죠. 보통 줄여서 DI라고 부릅니다.
//some feature

  private let provider: DataSource

  init(provider: DataSource) {
    self.provider = provider
  }

DI는 위와 같이 some feature 기능에서 DataSource를 멤버로 가지게 되고, 이를 생성자를 통해서 외부에서 주입 받습니다.
이 덕분에, some feature는 DataSource에 대해서 '그다지' 자세히 알 필요가 없습니다. 외부에서 주입되기 때문에 코드 작성자가 어느 인스턴스를 호출해야 할지 '자유롭게' 생각하지 않아도 됩니다. some feature가 데이터를 가져올 떄 부를 수 있는 코드는 provider로 제한되기 때문입니다.

let result = provider.fetchSomeData(userId: userId)

DI 덕분에 의존 관계가 명확해 졌네요. 지금은 단순한 예제를 보았지만, 대형 서비스 코드들을 보고, 해당 Feature가 의존하고 있는 경우를 명백하게 분석할 수 있습니다.
한가지 더 장점은 DI의 경우 interface처리가 훨씬 편하다는 것입니다. Singleton이 기능이 비대해 져서 접근할 수 있는 범위를 제한하기 위해 Facade를 사용해서 implement를 만든다면
DI는 필요한 기능을 protocol로 간단하게 처리해 버릴 수 있습니다.
지금은 some feature가 someData를 가져올 때 DataSource라는 Concrete한 객체에 의존하지만, 나중에 DataSource 객체가 교체가 될 수도 있습니다.
또는 some feature를 작성하는 도중에 DataSource가 완성이 되지 않았을 수도 있죠.
그래서 우리는 protocol을 이용합니다.
fetchSomeData(userId:) 를 제공해주는 어떤 객체든 상관없도록 만드는 것이죠

protocol SomeViewProvider {
  func fetchSomeData(userId: String) -> Result<Data, Error>
  func saveSomeData(with data: Data)
}

프로토콜을 만들고

private let provider: SomeViewProvider

init(provider: SomeViewProvider) {
  self.provider = provider
}

생성자에서 Concrete 타입이 아니라 프로토콜을 타입으로 받습니다.
이렇게 해서 some feature를 작성하는 동안은 DataSource 작업도에 영향을 받지 않을 뿐더러, DataSource가 다른 객체로 변경되더라도 SomrViewProvider만 채용 해달라고 부탁하면 되는 것이죠
또 접근할 수 있는 메서드가 (지금의 겨웅) fetchSomeData,와 saveSomeData로 제한되기 때문에, 해당 feature에서 어떤 기능을 사용하는지 더 명백해 집니다.
그리고 위와 같은 코드 패턴은 Testable한 코드도 겸사겸사 만들게 됩니다.

Singleton VS Dependency Injection

둘 중에 뭘 쓰면 더 좋을까 하는 고민은, 큰 의미는 없습니다. 왜냐하면 각각의 장단이 있기 때문인데요, Singleton은 어쨌거나 단 하나의 인스턴스를 만들어야 하는 경우는 DI보다 더 낫습니다. DI는 주입하는 과정에서 서도 다른 인스턴스를 만들 수 있는 가능성이 충분하기 때문이죠. 만약 local에 특정 user 정보를 보관하고 있다면 이는 따로 관리 되어서는 안될 것입니다.
하지만 디바이스 성능이 갈 수록 좋아지고, 기본적인 서비스 크기가 대폭 늘어남에 따라 Singleton이 주는 편리함은 치가 떨리는 재사용성을 낳습니다. 후임자에게 의존관계 분석 지옥을 선사하죠. 그래서 사견으로는 작은 서비스, 작은 책임에서는 Singleton은 좋은 선택이지만 유지보수가 필요한 상용 서비스에서는 Singleton을 멀리하고 Dependency Injection을 가까이 하는게 낫습니다.

Comments