앱 내 전역적으로 햅틱 이벤트를 호출하기 위해 HapticService를 구현하려 합니다. 함수 두 개만 작성하면 되는 아주 간단한 것이죠!
/// 알림 유형에 따른 햅틱 출력
func notification(type: UINotificationFeedbackGenerator.FeedbackType) {
UINotificationFeedbackGenerator().notificationOccurred(type)
}
/// 강도에 따른 햅틱 출력
func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) {
UIImpactFeedbackGenerator(style: .medium).impactOccurred()
}
그러다 문득 이를 타입 메서드로 구현해야 할지 싱글톤으로 구현해야 할지 고민이 되었습니다. 언뜻 봤을 때는 거의 비슷해 보이지만 분명 성능상의 차이가 있지 않을까,, 하는 의문으로부터 포스팅을 시작해 봅니다.
첫 번째 방식, 타입 메서드
struct HapticService {
private init() {}
private static let notificationGenerator = UINotificationFeedbackGenerator()
private static let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
static func notification(type: UINotificationFeedbackGenerator.FeedbackType) {
notificationGenerator.notificationOccurred(type)
}
static func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) {
impactGenerator.impactOccurred()
}
}
위 방식은 타입 메서드를 이용한 구현입니다. 유지해야 할 값이 없으므로 struct를 사용하고(타입 프로퍼티는 lazy 하게 초기화된 이후 앱 종료까지 메모리에 남아있기 때문에 다른 상황입니다!) 인스턴스의 생성 또한 필요가 없으니 생성자에 접근제어자 private을 붙여줍니다.
HapticService.notification(type: .success)
타입.함수 구문으로 간결하게 호출이 가능합니다.
두 번째 방식, 싱글톤
final class HapticService {
static let shared = HapticService()
private init() {}
private let notificationGenerator = UINotificationFeedbackGenerator()
private let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
func notification(type: UINotificationFeedbackGenerator.FeedbackType) {
notificationGenerator.notificationOccurred(type)
}
func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) {
impactGenerator.impactOccurred()
}
}
위 방식은 싱글톤을 이용한 구현입니다. 싱글톤은 하나의 객체를 보장해야므로 class를 이용(이와 관련해서 이전 포스팅 '구조체로 싱글톤 만들기?'에서 다루었습니다)하고 인스턴스의 생성 또한 필요가 없으니 동일하게 private 접근제어자를 붙여줍니다.
싱글톤 방식은 타입 메서드에서 인스턴스 메서드로 바뀐 것 외의 내부 구현은 동일합니다.
HapticService.shared.notification(type: .success)
싱글톤의 유일한 객체 shared에 접근해 호출이 가능합니다.
성능 비교하기
타입 메서드 | 싱글톤 | |
내부 인스턴스 생성 | 타입 프로퍼티로 최초 1회 생성 | shared 인스턴스 최초 1회 생성 |
내부 인스턴스 재사용 | 타입 프로퍼티로 동일한 인스턴스 사용 | shared에서 생성된 동일한 인스턴스 사용 |
메모리 해제 시점 | 앱이 종료될 때까지 유지 | 앱이 종료될 때까지 유지 |
호출 방식 | HapticService.method() → 상대적 간결 |
HapticService.shared.method() → 상대적 장황 |
표에서 비교했듯 성능 상의 차이는 존재하지 않는다고 봐도 무방합니다. 타입 메서드와 싱글톤 모두 정적 타입 static을 기반으로 한 구현 방식이기 때문입니다. 타입 메서드로 구현하는 방식은 정의상 싱글톤이라고 이야기할 순 없지만, 그 내부 동작은 싱글톤과 거의 동일하게 실행됩니다.
struct HapticService {
private init() {}
/// 하나의 싱글톤을 들고 있는 것과 같음.
private static let notificationGenerator = UINotificationFeedbackGenerator()
/// 하나의 싱글톤을 들고 있는 것과 같음.
private static let impactGenerator = UIImpactFeedbackGenerator(style: .medium)
static func notification(type: UINotificationFeedbackGenerator.FeedbackType) {
notificationGenerator.notificationOccurred(type)
}
static func impact(style: UIImpactFeedbackGenerator.FeedbackStyle) {
impactGenerator.impactOccurred()
}
}
다시 코드를 살펴보면 notificationGenerator, impactGenerator가 이미 하나의 객체(싱글톤과 같음)를 보장하고 있습니다. 결국 위 맥락에서 타입 메서드를 사용한 호출 방식은 싱글톤의 단축 구문 정도로 해석할 수 있게됩니다.
그럼 항상 타입 메서드 구현 방식이 좋은 것인가?
타입 메서드로 구현한다는 것은, 위에서 살펴봤듯 사용하는 인스턴스 또한 타입 프로퍼티로 들고 있어야 합니다. 즉, 공유 상태 관리 또한 타입 프로퍼티로 가능합니다.
struct Sample {
static var count = 0
static func increase() {
count += 1
}
}
하지만 이 방식은 매번 타입 프로퍼티와 타입 메서드를 이용해 생성해줘야 한다는 점(인스턴스 프로퍼티는 접근이 불가능)에서 확장의 어려움을 가져올 수 있습니다.
또한 정적 프로퍼티(var)는 기본적으로 Swift6에서 안전하지 않습니다.(not concurrency-safe) actor를 이용하거나 Sendable 프로토콜을 채택함으로써 이를 해결할 수 있지만 추가 오버헤드가 발생하는 것은 피할 수 없습니다.
결국 타입 메서드 방식은 단축 구문으로서의 명확한 장점이 있는 반면, 확장이나 동시성 모델을 생각했을 때의 단점도 존재합니다.
정리하기
타입 메서드의 간결한 호출 방식은 여러 곳에서 동일하게 사용하는 유틸리티의 목적상 더 부합할 수 있습니다. 물론 .shared 한 단어 더 붙인다고 크게 달라지진 않겠지만 상대적으로 사용의 편리함을 주고 가독성을 높이는 데 분명 도움이 될 것입니다.
반대로 확장성과 동시성을 고려했을 때 싱글톤이 부합할 수 있습니다. 그러니 모든 개념은 프로젝트의 목표와 맥락에 맞게 선택해야 함을 유념해야겠습니다. (저는 HapticService를 타입 메서드로 구현하기로 결정했답니다 🎶)
이렇게 천천히 뜯어보니 포스팅할만한 주제가 아니었나,, 생각이 들기도 합니다만! 사소하지만 모호한 개념을 파고드는 것은 언제나 재밌습니다. 나름대로 근거를 쌓아나가는 과정이라 믿습니다!
'iOS > 내 방식대로 풀어보기' 카테고리의 다른 글
Array / Set / Dictionary Swift의 컬렉션 알아보기 (2) | 2025.02.06 |
---|---|
구조체로 싱글톤 만들기? (0) | 2025.01.30 |
비동기 작업의 단위, Task 알아보기 (0) | 2025.01.22 |
MVC와 Cocoa MVC, 뭐가 다를까? (0) | 2025.01.16 |