Let's Swift 뉴스레터를 슥 훑어보는데 눈에 띄는 주제가 있었다.
Stop Making Singletons in Swift: A Dependency Injection Guide
웬만하면 사용하지 않으려고는 하나, 종종 사용했던 싱글톤 패턴인지라(ex. 의존성 주입)
이참에 특징과 장단점, 그리고 저 문서에서 얘기하는 내용이 무엇인지 공부하면서 정리해두면 좋을 것 같다는 생각에 포스팅을 작성하기 시작했는데,
(한편만에 끝낼 수 있었다면 좋았을텐데 아직 고찰을 끝내지 못했다..)
이번 1편에서는 위 포스팅의 작성자가 왜 싱글톤을 쓰지 말라고 하는가 정도만 적어본다.
1. 싱글톤이란 무엇인가
쉽고 간단하게, 앱 생명주기동안 한 번만 생성되는 객체를 싱글톤 객체라고 한다.
첫 회사에서 처음 넘겨 받은 코드에는 sharedInstance라는 싱글톤 객체가 이미 여러 개 존재하고 있었다. 프로젝트 내에서 동일 객체를 공유하며 ViewController 어디서든 하나의 데이터를 바라보는지라 Configuration 등을 쉽게 접근하고 수정했었다.
구현은 initialize 함수를 private으로 선언하고 public 타입의 static 변수 shared에 해당 객체를 선언해두는 형태로 한다.
(참고로 Flutter도 형태는 동일)
class ExampleClass {
static let shared = ExampleClass()
private init() {}
}
// 다른 곳에서 ExampleClass.shared로 사용
그냥 보기에는 어디서든 사용하는 객체라면 오히려 한 번만 생성해두는게 메모리 생성/해제도 반복하지 않고, 어디서나 접근하기 좋지 않나?
싶기도 한데, 위 링크에 단점이 요모조모 적혀있다.
2. 싱글톤을 왜 쓰면 안 되는가
위 링크에서는 크게 총 4가지를 얘기하고 있다.
- 종속성을 추적하기 어렵다
- 싱글톤 객체가 서로를 의존하면 교착 상태가 형성될 수 있다
- 목업 객체를 쉽게 전환할 수 없다 (테스트가 복잡해진다)
- 유연성이 부족하다
얘기한 경우들이 어떻게 보면 결국 하나로 연결되는 것 같다.
요즘 트렌드이자 추구하는 방향은 어떻게든 모듈을 분리하고 격리시키는 쪽이라고 생각하는데 (이렇게 구현하면 나아가 테스트를 하기에도 용이하고 유지보수를 해나가기도 좋다)
싱글톤은 그 흐름에 맞지 않는 셈이다.
3. 그렇다면 싱글톤 말고 어떻게 구현하라는 걸까
위 링크에서는 의존성 주입을 소개한다.
다만,
3-a. EnvironmentObject는 추천하지 않습니다
자사 서비스 구현 시 ViewModel을 ```ObservableObject```로 구현하고 ```@EnvironmentObject```로 주입한 적이 있는데, 이 방법 역시 추천하지 않는다고 한다.
- @EnvironmentObject로 사용할 수 있는 건 ObservableObject 뿐이고,
- protocol을 사용할 수 없으며(=동일 프로토콜로 목업 객체를 구현하는 형태로 사용할 수 없으며),
- SwiftUI 뷰 계층에서만 동작하고,
- 의도치 않은 누락이 생길 가능성이 높다는 게 그 이유다.
두 번째 경우는 말로 하는 것보다 코드로 보는 게 좀더 직관적인데,
protocol MyServiceProtocol {
func fetchData() -> String
}
class MyService: ObservableObject, MyServiceProtocol {
func fetchData() -> String {
return "MyService 데이터"
}
}
class MockService: ObservableObject, MyServiceProtocol {
func fetchData() -> String {
return "MockService 데이터"
}
}
struct ContentView: View {
@EnvironmentObject var service: MyServiceProtocol // ❌ 사용 불가능
}
struct ContentView: View {
@EnvironmentObject var service: MyService // ✅ 프로토콜이 아닌 구체적인 타입만 가능
}
오.. 설명 끝.
사실 네 번째 같은 경우도 벌써 Preview에서만 봐도 왕왕 일어나는 일이긴 하다.
실제 EnvironmentObject를 적용하면서 서치한 수많은 포스팅도 주입을 잊지 말라고 단단히 당부하기도 했고.
자, 그러면 또 뭘 사용하냐고?
2-b. 대신 Property Wrapper를 사용하세요
import SwiftUI
// 프로토콜 정의
protocol MyServiceProtocol {
func fetchData() -> String
}
// 실제 서비스와 Mock 서비스 구현
class MyService: MyServiceProtocol {
func fetchData() -> String {
return "MyService 데이터"
}
}
class MockService: MyServiceProtocol {
func fetchData() -> String {
return "MockService 데이터"
}
}
// Property Wrapper 정의
@propertyWrapper
struct Injected<T> {
private var service: T
init() {
// 테스트 조건에 따라 Mock 객체 반환
// 또는 하나의 구현체만 리턴하도록 선택할 수도 있음
if T.self == MyServiceProtocol.self {
#if DEBUG
self.service = MockService() as! T
#else
self.service = MyService() as! T
#endif
} else {
fatalError("지원하지 않는 타입")
}
}
var wrappedValue: T {
get { service }
}
}
// SwiftUI View에서 사용
struct ContentView: View {
@Injected var service: MyServiceProtocol // 프로토콜 기반 의존성
var body: some View {
Text(service.fetchData())
}
}
GPT가 작성해준 코드다
링크 게시글에서 소개하는 방법은 여기 있다.
DIContainer를 만들어서 필요한 객체를 가져오고 반환하고 해주는 것 같은데, 이건 나중에 직접 쳐보면서 좀더 뜯어보고 연구해보면 좋을 것 같다.
사실 DIContainer는 Clean Architecture를 공부하면서 어떻게 하면 이 구조에 어긋나지 않는 구현이 가능할까 때문에 이거저거 찾아보면서도 꽤 오래 고심했던 부분인데, 링크 게시글의 댓글을 보면 작성자가 제시한 방법도 결국 인스턴스를 한 번 생성한다고 하더라.
맞는 말이라고 생각한다.. 애초에 나도 자사 서비스에서 의존성 주입 객체 만들 때 열심히 서치하다가 결국 싱글톤을 사용한건데, 싱글톤을 안 쓰고 의존성 주입을 하려면 어떤 식으로 해줘야 하는 걸까?🤔
동일 댓글에서 ```factory closure or autoclosure``` 라고 언급한 내용에 대해서도 같이 고민해봐야겠다.
+ 사실 포스팅 내용 중에 Coordinator 사용을 권장한다는 링크도 있는데, 이 부분에 대해서는 개인적으로도 고민이 조금 필요하다고 생각해서(SwiftUI에서 Coordinator 사용이 옳은가? 그렇다면 어떤 식으로 접근 및 구현해야 하는가?) 적지는 않았다.
'개발노트 > iOS' 카테고리의 다른 글
the Certification Authority (CA) for Apple Push Notification service (APNs) is changing (5) | 2024.12.06 |
---|---|
iOS 프로젝트에서 Kakao Login 설정하기 (0) | 2024.11.27 |
iOS 인앱 결제 테스트하기 (3) | 2024.11.22 |
iOS에서 API Key 파일 분리하기 (0) | 2024.11.18 |
App Store Server API 사용을 위해 앱 개발자가 제공해줄것들 (2) | 2024.11.15 |