Neoself의 기술 블로그

Swift에서 싱글톤 패턴 활용하기 본문

개발지식 정리/Swift

Swift에서 싱글톤 패턴 활용하기

Neoself 2024. 10. 2. 21:04

기존에 출시한 TyTE 어플리케이션의 성능 최적화를 위해 인스턴스의 생성 및 해제 시점을 파악하고자 각 ViewModel 및 서비스 레이어에 print문을 배치시키고 홈화면에 진입한 결과, 아래 로그 내용을 볼 수 있었습니다.

MainTabView initialized
TodoService initialized
SharedTodoViewModel initialized
TodoService initialized
HomeViewModel initialized
TodoService initialized
HomeView initialized
...

 

로그인 및 회원가입을 완료하게 될 경우, 가장 먼저 진입하는 뷰인 MainTabView,

하위 뷰들에게 주입하고자 MainTabView에서 초기화를 거친 SharedTodoViewModel과 HomeViewModel,

그리고, 가장 마지막으로 HomeViewModel을 @ObservedObject 프로퍼티래퍼로 참조하는 HomeView가 정상적으로 1회씩 초기화되고 있는 데에 반해, 서비스 레이어인 TodoService class가 불필요하게 반복 초기화되고 있다는 것을 확인할 수 있었습니다.

// TodoService class
class TodoService {
    private let apiManager = APIManager.shared
    
    init() {
        print("TodoService initialized")
    }

    deinit {
        print("TodoService deinitialized")
    }
    
    func fetchAllTodos(mode:String) -> AnyPublisher<[Todo], APIError> {
        let endpoint = APIEndpoint.fetchTodos(mode)
        
        return Future { promise in
            self.apiManager.request(endpoint) { (result: Result<[Todo], APIError>) in
                promise(result)
            }
        }.eraseToAnyPublisher()
    }
    ...
}

// TodoService 내부 메서드에 접근하는 뷰모델
class SharedTodoViewModel: ObservableObject {
    private let todoService: TodoService
  
    init(
        todoService: TodoService = TodoService(),
    ) {
        print("SharedTodoViewModel initialized")
        self.todoService = todoService
        fetchTags()
    }
    ...
  }

TodoService 내부 메서드에 접근하는 뷰모델들을 확인해본 결과, 각 뷰모델이 자신만의 TodoService 인스턴스를 생성 및 사용하고 있는 것이 원인인 것을 알 수 있었습니다... 한개만 존재하여도 되는 인스턴스가 여러번 생성되는 것이기에, 이는 메모리를 불필요하게 차지해 성능 저하의 주요 원인이 됩니다.

 

따라서 TodoService를 싱글톤으로 구현해, 앱 전체에서 하나의 인스턴스를 공유해 사용하게끔 리팩토링을 진행하였습니다!

 

여기서 싱글톤 패턴이란 특정 인스턴스가 전체 어플리케이션에서 단일로 존재하도록 보장하는 디자인 패턴입니다. 이 패턴을 사용하게 될 경우, 위 사례와 같이 동일한 기능을 하는 여러 인스턴스가 불필요하게 메모리를 차지하는 문제를 해결할 수 있습니다.

class TodoService {
    static let shared = TodoService() // 전역적으로 접근 가능한 단일 인스턴스 생성
    
    private let apiManager = APIManager.shared
    
    init() {
        print("TodoService initialized")
    }

    deinit {
        print("TodoService deinitialized")
    }
    func fetchAllTodos(mode:String) -> AnyPublisher<[Todo], APIError> {
        let endpoint = APIEndpoint.fetchTodos(mode)
        
        return Future { promise in
            self.apiManager.request(endpoint) { (result: Result<[Todo], APIError>) in
                promise(result)
            }
        }.eraseToAnyPublisher()
    }
    ...
  }

 

static let shared = TodoService() 구문을 추가하여 전역적으로 접근이 가능한 단일 인스턴스를 서비스 레이어에서 생성해준 후에,

class HomeViewModel: ObservableObject {
    private let todoService: TodoService 
    
    init(
        todoService: TodoService = TodoService.shared
    ) {
        self.todoService = todoService
        fetchTodos()
    }
    ...
  }

서비스 레이어 내부 메서드를 접근해야하는 뷰모델 내부에는 TodoService.shared 로 초기에 생성된 단일 인스턴스에 접근할 수 있습니다.

 

이제 위와 같이 서비스 레이어와 뷰모델를 수정한 후, 앱을 진입하게 될 경우,

MainTabView initialized
TodoService initialized
SharedTodoViewModel initialized
HomeViewModel initialized
HomeView initialized

TodoService가 하나만 생성되는 것을 확인할 수 있었습니다!

 

싱글톤 패턴은 위와 같이 앱 전체에서 일관된 데이터 접근이 필요하고, 인스턴스가 불필요하게 반복 생성될 경우 매우 강력하지만, 과도하게 사용할 경우, 아래와 같은 문제점에 유의해야 합니다.

1. 컴포넌트들 사이의 의존성이 증가해 재사용성이 저하될 수 있음.

2. 의존성 주입과 같은 패턴에 비해, 의존성이 숨겨져있기 때문에 코드 가독성이 떨어지고, 유지보수에 드는 리소스가 커질 수 있음.

 

따라서, 싱글톤 사용 시에는 의존성 주입과 같은 다른 패턴과 적합성을 비교해보는 등 프로젝트의 요구사항에 따라 신중히 고려해봐야 합니다.

패턴 채택 시 정답은 존재하지 않지만, 싱글톤 패턴이 특히 유용한 상황은 다음과 같습니다.

- DB 연결 관리자, 네트워크 연결 관리자와 같은 공유 리소스를 관리하는 class

- 앱의 전역 상태를 관리하는 class

 

감사합니다.