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

Swift에서의 싱글톤에 대한 생각정리

by Mintta 2023. 9. 11.

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

 

 


최근들어서 프로젝트를 같이 하는 팀원과 이야기를 하던 중 싱글톤에 대해 다시한번 알게 된 사실이 있어서 그 사실을 포함해서 저의 싱글톤에 대한 생각을 글로서 정리해보고자 합니다. (제 생각이 포함되어 있어 정답이 아닐 수도 있습니다.)

싱글톤은 무조건 나쁠까 ?

싱글톤으로 구글링을 해보면 사람들이 강조하는 나쁜 이유에 대한 것들, 그것이 정말 Swift에서도 적용되는 말일까 ?

나쁜 이유 1. 싱글톤은 멀티 스레드 환경에서 2개의 객체가 생성될 수 있다 ?

싱글톤의 가장 치명적인 단점으로 많이들 이야기하는 것이 바로 오직 하나의 인스턴스만이 생성됨을 보장하는 싱글톤의 객체가 멀티스레딩 환경에서 2개 이상 생성될 수 있다는 것입니다. 절대 할당 해제되지 않는 객체가 2개 이상이 되는 것입니다.

 

결론부터 말하자면 Swift에 한해서 이 말은 사실이 아닙니다. 

전역 변수(구조체 및 열거형의 정적 멤버에도 해당)는 항상 lazy하게 계산되고, lazy initializer는 처음 액세스될 때 실행되며, 초기화가 atomic인지 확인하기 위해 dispatch_once로 실행됩니다.

 

따라서 이니셜라이저가 있는 전역 변수를 선언하고 init을 private으로 표시하기만 하면 코드에서 dispatch_once를 멋지게 사용할 수 있습니다.

 

dispatch_once ? 🤔

dispatch once는 Objective-C의 메서드로, 여러 스레드에서 동시에 호출되는 경우 이 함수는 블록이 완료될 때까지 동기적으로 대기한다고 합니다. 그렇기에 atomic의 특성을 유지할 수 있게 되는 것이죠.

 

static let shared = SingleTonClass()
private init() {}

 

Swift에서 싱글톤으로 객체를 만들 때 위와 같이 코드를 작성합니다.

 

static으로 선언: lazy initializer덕분에 접근할 때 객체를 처음 생성하게 되는데, 이 때 ! dispatch_once로 실행되므로 객체의 생성을 atomic하게 할 수 있습니다.

 

그리고 private init을 통해서 외부로부터의 객체 생성을 막아주면 thread-safe한 객체 생성이 가능할 뿐더러, 접근할 때에 처음으로 생성되기에 앞단의 불필요한 시간을 줄일 수 있습니다.

 

물론 메모리에서 할당 해제가 되지 않기 때문에 사용하지 않을 때에도 메모리에 남아있게 된다는 단점은 존재합니다.

 

어쨋건, 멀티 스레드 환경에서 객체가 2개 이상 생성될 수 있다는 Swift에서는 아니다! 라는 것을 확인할 수 있었습니다.

 

나쁜 이유 2. 싱글톤을 사용할 때 data race 문제가 있다 ?

싱글톤으로 우리는 thread safe하게 객체를 생성했습니다.

이제 앱 전역에서 언제 어디서나 접근할 수 있는 불사의 객체 인스턴스가 하나 생긴 것 입니다.

 

하지만 객체가 단 한가지만 존재하기에 생기는 문제가 있습니다. 바로 data race입니다.

한가지 데이터를 경쟁적으로 서로 수정할려고 하기에 data race, 데이터 경합입니다.

 

싱글톤은 하나의 객체가 전역으로 공유되기 때문에, 이러한 위험에 노출되어있습니다.

actorLock이나 semaphore와 같이 추가적인 동기화 로직이 없다면 피할 수 없습니다.

이것은 반박할 수 없는 사실이기에 어쩔 수 없습니다.

 

하지만 모든 경우에 이 문제가 해당되는지 의구심이 들었습니다 🤔

 

실제 프로젝트를 개발할 때 사용했었던 싱글톤을 가져와봤습니다.

 

final class AuthService {
    static let shared = AuthService()
    private init() {}

    func signUp() {...}
    func login() {...}
    func logout() {...}
}

 

코드를 보시면 메서드만 존재할 뿐 data race가 일어날 프로퍼티는 존재하지 않습니다.

읽고, 쓰는 작업이 존재하지 않는다는 것 입니다.

 

이 객체는 그저 “API 호출해줘”라는 메시지를 받아서 해당 로직을 수행하고 그 결과값을 돌려줄 뿐입니다.

read / write 작업을 하는 프로퍼티 또한 존재하지 않으니, data race가 발생할 가능성도 없다고 생각했습니다.

 

그렇다면 이런 경우(프로퍼티가 없어 data race가 생길 가능성이 없는 경우)에는

생성할 때에도 thread safe하고 사용할 때도 thread safe하면 사실상 아무 문제 없이 사용할 수 있는게 아닐까?

하고 생각했습니다.

하지만! 그럼에도 싱글톤은 지양하는게 맞지 않을까..

제목그대로 그럼에도 싱글톤을 지양하게 되는 몇가지 제가 생각하는 이유를 정리하고자 합니다.

SRP (Single Responsibilty Principle) 위반

Single Responsibilty Principle에 위반됩니다.

싱글톤 객체를 사용하는 객체는 우선 싱글톤 객체와 강한 의존성, 결합도가 높을 수 밖에 없습니다.

이 말은 즉, 싱글톤 객체에 생기는 변경사항이 싱글톤 객체를 사용하는 객체에까지 영향을 미친다는 말입니다.

 

예를 들어 싱글톤의 메서드가 바뀌었다고 칩시다. 이름이나 파라미터 등 무언가 바뀌었다면 그 변경사항은 곧바로 싱글톤 객체를 사용하는 객체로까지 전파되고 컴파일 에러가 나고 결국 해당 코드를 사용하는 모든 곳을 다 수정해줘야 합니다.

 

단일 책임 원칙에서의 책임은 해당 객체가 변경되어야 하는 이유와도 같습니다.

그리고 단일 책임 원칙은 객체가 변경되어야하는 이유는 한가지여야한다고 말하는 것입니다.

하지만 위의 경우에는 싱글톤 객체를 사용한 객체의 경우에 변경되어야하는 이유가 두가지가 됩니다.

이는 SRP의 위반이라고 볼 수 있습니다.

OCP (Open Closed Principle) 위반

Open Closed Principle에 위반됩니다. 확장에는 열려있고, 변경에는 닫혀있어야 합니다.

앞서 말했듯이 싱글톤을 사용하게 되면 해당 객체를 사용하는 객체와의 결합도는 증가합니다. 이는 확장을 어렵게 만듭니다.

 

확장을 하기 위해서는 느슨한 결합을 통해 객체들이 연결돼있어야하는데, 이렇게 강하게 서로 엉겨 붙어있다면 당연히 기능 추가를 하기 위해서 기존의 코드를 수정하게 되고, 이는 변경에 열리게 되는 것입니다.

Unit test에서의 싱글톤

Unit test를 할 때 싱글톤을 사용하게 되면 우선 생성자를 사용하지 못하기에 객체 생성을 할 수가 없습니다.

따라서 Mock 객체를 만들고자 해도 만들 수가 없어 테스트를 하기에 매우 까다로운 상태가 됩니다.

 

보통 test를 진행하고자 하는 목적이 ‘어떤 특정 input에 대한 output이 우리가 의도한대로의 결과값으로 나오는가’를 검증하고자 하는 것인데 특정 input을 줄 수 가 없는 것이죠. Unit test를 고려하고 있다면 싱글톤은 지양해야합니다.


“근데 SRP니, OCP니 하는 것도 결국 싱글톤에 뭔가 데이터가 있을 때 얘기잖아 ? 나는 메서드만 호출하는 용도로만 쓰는 데 아무 상관 없지 않을까 ? 🤔”

 

이렇게 싱글톤의 단점도 장황하게 말했지만 위에 적은 생각이 드는 것도 사실입니다.

따라서 이는 상황에 따라서 매우 다르게 작용할 수 있겠다라는 생각이 들었습니다. 제가 하고 있는 이런 메서드만 부르는 싱글톤의 경우에는 사실 큰 문제는 없겠다라는 생각이 들었습니다.

 

하지만!!

 

싱글톤에 프로퍼티가 추가되는 것만으로 별도의 동기화 로직이 없다면 data race문제가 생기고 SRP와 OCP 위반 문제가 생길 수 있는 시한폭탄?과 같은 것을 이것밖에 대안이 없는 것이 아니라면 꼭 써야만 하는 상황이 아니라면 굳이 안쓰는게 맞지 않을까요??🤔


오늘은 싱글톤에 대한 제 생각을 쭉 정리해봤고, 향후 싱글톤을 사용하게 될 때 이런 제 생각들을 근거로 결정을 내릴 수 있을 것 같네요.

혹시 누군가 이 글을 보시고 뭔가 틀린 부분이 있다면 가감없이 댓글에 남겨주시면 감사하겠습니다 !

 

(2023.10.27 내용 일부 수정 및 추가)

Ref

https://ios-development.tistory.com/1211

이 글을 보고 싱글톤 객체가 Swift에서는 생성시에 안전하다는 것을 알게 되었고, 이게 계기가 되어 이 아티클을 쓰게 되었습니다 !

 

[iOS - Swift] 스위프트에서의 singleton 싱글톤 동작 이해하기 (lazy, thread safe)

싱글톤 패턴 인스턴스는 오직 한개를 사용하고, 라이프사이클 동안 절대 해제되지 않는 하나의 인스턴스 유지 swift에서는 static와 함께 전역변수로 선언하면 lazy하게 동작하는 장점이 존재 swift

ios-development.tistory.com

 

댓글