안녕하세요!!
이번엔 싱글톤에 대한 오해와 사실?이라는 주제를 가지고온 킴스캐슬입니다
싱글톤은 우리가 정말 많이쓰는 패턴중에 하나죠
그런데 사람마다 다르겠지만 싱글톤에 대해서 회의적인 관점을 가지고 계신 분들이 많더라고요
그래서 이유를 듣다보니 분명히 납득이 되는 부분이 많더라고요
그렇다고는 해도 그 부분들이 사실 제가 지금까지 진행했던 프로젝트에서 실제로 느껴지는 불편함은 아니었습니다
아마도 다른 분들이 말하시는 불편함을 느낄 정도의 규모와 수준이 아니어서 그렇다는 이야기일수도있지만 반대로말하면 보편적인 불편함이 아니라 특정상황에서의 불편함
일수도 있겠다는 생각이 들었습니다
그래서 이번기회에 싱글톤에 대한 오해와 사실을 한번 정리해볼까 하는 마음이 들어서 포스팅을 하게되었습니다 ㅎㅎ
그럼 오늘도 한번 함께 이야기를 나눠보시죠!
본격적인 이야기를 하기전에 싱글톤이 뭔지부터 간단하게 알아보고 넘어갈게요
싱글톤 패턴이란?
소프트웨어 디자인 패턴에서 싱글턴 패턴(Singleton pattern)을 따르는 클래스는, 생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다. 이와 같은 디자인 유형을 싱글턴 패턴이라고 한다. 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool)와 같은 상황에서 많이 사용된다.
by위키백과
위키백과에서의 정의는 저런데 application입장에서 싱글톤을 설명해보면 여러곳에서쓰는 공유되야하는 데이터가 있으면 그 데이터를 가지는 클래스를 하나만
만들어서 여기저기서 접근해서 값을 사용할 수 있는 그런 디자인패턴이라고 생각하시면 됩니다
네것 내것이 없는 그런 누구나 사용할수있는 객체를 만들어서 공유하며 사용하는 디자인패턴입니다
싱글톤에서 중요한 점은 단 하나의 객체만 생성
한다는 점입니다
객체를 생성할때 중요한건 initialize죠? 그렇기때문에 initialize는 private으로 선언해서 다른곳에서 객체를 생성할 수 없게 만들어야합니다
class SingletonService {
static let shared = SingletonService()
private init () {}
func printHello() {
print("Hello")
}
}
Q : 아니근데 static을 이용하면 단 한번만 생성되나요...? 처음 생성은 어떻게 되는거죠...?
A : 객체에 접근한 순간 초기화가 됩니다!
라고 말하면 아마도 이게무슨 소리죠...?
라고 하실수있는데 이 말이 맞는말인거같아요
포스팅을 끝까지 읽으시면 이게 무슨말인지 이해하실수있을거예요
(우선은 그런갑다 하고 넘어갑시다ㅎㅎ)
그리고 이렇게 만든 싱글톤을 여러 ViewController에서
SingletonService.shared.printHello()
이렇게 사용할 수 있습니다
만약에 print("Hello")
라는 메서드를 여러 뷰컨에서 사용해야하는 경우에 매번 함수를 만들어서 호출할 필요없이 싱글톤으로 만들어놓으면 여러군데에서 사용할 수 있게되는겁니다
이젠 싱글톤에 대한 기본지식을 알게되셨습니다
지금부터는 싱글톤에 대한 여러가지 사실과 오해에 대해 알아보겠습니다
우선 이 이야기가 왜나왔는지부터 짚고 넘어가보겠습니다
기본적으로 싱글톤 코드를 보면 init이 private되어있는데 그러면 우리가 생성을 못하기 때문에 전역변수(global하게 작동하는)인 static으로 인해서 앱이 실행되면 메모리에 할당되고 우리가 사용할 수 있게된다의 흐름으로 이해하시는 분들이 간혹 계시더라고요
그렇다고 하면 우리가 싱글톤을 만들어놓고 만약 한번도 쓰지 않았다고 해볼까요?
이미 앱이 실행되는 순간 메모리에는 싱글톤객체가 올라갔는데 한번도 안쓰면 메모리공간이 낭비되는 셈인거죠
원래 여러곳에서 쓰려면 여러객체를 만들었어야해서 그 메모리 공간보다 하나만 만들어서 쓰게된거니까 메모리공간을 효율적으로 쓸수있는데 이런경우엔 오히려 메모리 공간의 낭비가 됩니다
이런 오해를 해결함과 동시에 이전에 말씀드렸던 객체에 접근한 순간 초기화가 됩니다!
라는 의미에대해 설명드리겠습니다
우리가 싱글톤을 만들때 static을 통해서 객체를 생성하죠?
static
은 우리가 정적변수
라고 이야기합니다
그리고 정적변수
는 전역변수
이기도 합니다
(타입을 통해 전역에서 접근이 가능한 프로퍼티니까요)
그리고 swift공식문서에 보면 위와같은 note가 존재합니다
요약을 해보면 global 변수는 lazy하게 동작한다
입니다
lazy하게 동작한다는건 해당 프로퍼티에 접근하는 순간 초기화가 된다는 뜻이기도 합니다
그러니까 애초에 접근을 한번도 안하면(사용을 단 한번도 안한다면) 메모리에 올라가지 않습니다
싱글톤은 init을 통해서 초기화가 되는것이 아니라 정적변수에 접근하는 순간 초기화가 되서 메모리에 올라가고 이후에는 init없이도 객체에 접근해서 여러 task를 실행할 수 있게 됩니다.
맞다 틀리다를 논하기전에 우리 thread-safe가 뭔지에 대해서 한번 짚고 넘어가보죠
swift는 muti-thread환경을 지원하죠? 그러다보니 어떤 특정 변수의 값을 여러 thread에서 동시에 읽거나 쓰다보면 우리가 예상하는 결과와 다른 값이 나올수도 있다는 겁니다
그래서 이런 상황이 일어나지 않는걸 thread-safe하다
라고 이야기 합니다
그러면 thread-safe
하기위한 조건에는 뭐가있을까요?
- 한번 생각을 해보면 어떤 변수를 write하고 있는데 read하면 우리가 예상하지 못한 결과가 도출될수도있겟죠?
- 객체에 접근하려하는데 갑자기 객체가 다른 메모리주소의 객체를 가리키면 이것도 예상치 못한 값이 출력되겠죠?
1번의 경우 multi-thread 환경에서 데이터가 반드시 변경전과 후의 상황에서만 접근하는 것을 보장한다고 조금더 세련되게 이야기할 수 있습니다
그리고 이 문장을 한단어로 표현하면
atomic하다
라고 표현할 수 있습니다
그리고 2번의 경우는 객체의 메모리주소를 바꾸지 않는 방법인데 객체를 담고있는 변수를 let으로 선언해주면 객체는 reference type이기때문에 객체내 값들은 바꿀수있지만 객체의 주소는 변하지 않아 객체의 접근에있어서 안정성을 보장할 수 있습니다
swift에서 atomic하게 하는 방법엔 뭐가 있을까 찾아봤습니다
이 글을 보면 static
같은 global
변수를 사용하면 dispatch_once
같이 atomic
함을 보장한다고 하네요
(dispatch_once
가 뭔지는 설명드리겠습니다, _옵젝씨의 유산인듯 싶네요)
그렇다면 static을 사용하면 atomic함을 보장받게 되고 let을 통해 메모리주소의 안정성을 보장받아 thread-safe한 싱글톤 객체 생성이 가능하게 되겠네요
그래서 우리가 싱글톤 객체를 생성할때 static let
으로 생성한것입니다
이제야 왜 굳이 싱글톤 객체를 static과 let을 사용해서 생성했는지 그 이유를 알게되었네요
좀전에 말씀드렸던 dispatch_once
는 object-C에서 싱글톤객체를 생성할때 써야했던 도구였던거같아요
그래야 thread-safe하게 생성할수있는... 근데 swift에 넘어와서도 사용되다가 특정버전에서부터 static let으로 대체되었다고 알고만 계시면 될거같습니다
그러면 싱글톤은 thread-safe한거네요????
방금 말씀드린건 싱글톤 객체의 생성시점
에 관한 이야기였습니다
실제로 사용할때 여러 thread에서 동시에 읽고 쓴다면 그건 완전 다른 경우이고 사용할때는 thread-safe하지 않습니다
결론적으로 싱글톤의 경우 객체를 생성할때는 thread-safe하지만 사용할때는 thread-safe하지 않다라고 이해하시면 될거같습니다
두번째 주제에서 우리는 싱글톤 객체를 사용할때 thread-safe하지 않다는 결론을 내렸습니다
해결방법이 없을까요?
아니요 방법이 있습니다
class SingletonService {
static let shared = SingletonService()
private init () {}
private var name: String = "Youth"
func printName() {
print(name)
}
func changeName(name: String) {
self.name = name
}
}
싱글톤 객체가 하나있고 thread-safe한 생성을 위해 static let을 이용했습니다
하지만 두개의 메서드중에 하나는 read 하나는 write이기때문에 muti-thread에서 동시에 읽고쓸 가능성이 존재합니다(thread-safe하지 않을수있죠)
이런 방식을 해결하기 위해서 각각의 taks를 serial queue에 넣어주면 task를 하나씩 실행할수있고 첫번째 task가 끝나야 두번째 task가 실행되기때문에 atomic해집니다
class SingletonService {
static let shared = SingletonService()
private init () {}
var serialQueue = DispatchQueue(label: "testQueue")
private var name: String = "Youth"
func printName() {
serialQueue.async {
print(name)
}
}
func changeName(name: String) {
serialQueue.async {
self.name = name
}
}
}
이렇게 만들면 되겠네요
여기서 성능향상을 위한 방법이 또 있는데요 바로 concurrent queue를 만들고 barrier를 이용하는 방법을 사용하면 atomic하게 동작하면서 serial queue를 이용하는것보다 최적화가 되어서 동작한다고 하네요
class SingletonService {
static let shared = SingletonService()
private init () {}
var serialQueue = DispatchQueue(label: "testQueue", attributes: .concurrent)
private var name: String = "Youth"
func printName() {
serialQueue.async(flags: .barrier) {
print(name)
}
}
func changeName(name: String) {
serialQueue.async(flags: .barrier) {
self.name = name
}
}
}
이렇게 하면 싱글톤 객체가 사용시점에서도 thread-safe하게 동작할 수 있습니다
자 그러면 이렇게 말씀하시는 분들도 계실거예요
이런방식을 이용해서 thread-safe하게 동작하게 만들면 이렇게 쓰면되겠네요???
맞습니다 이왕 쓰는거 이렇게 thread-safe하게 만들어서 사용하시면됩니다
근데 왜 많은 개발자들이 싱글톤을 사용하기를 약간 꺼려할까요?
👇여기서부터는 제가생각한 내용이기때문에 정답은 아닐수있습니다!👇
queue를 활용하면 thread-safe한 상황을 예방할수있기때문에 아마 단순히 thread-safe하지않아서!
는 싱글톤 디자인패턴을 사용하지 않을 납득할만한 이유가 될수는 없다고 생각합니다
(애초에 생성자체는 thread-safe하게 설계가 되어있기도하고요)
또한 어떤 싱글톤객체는 내부에 저장속성없이 단순히 메서드만 호출하는 경우가 있을수있겠죠?
저같은 경우도 api를 호출하는 메서드만 넣어놓고 APICaller라는 싱글톤 객체를 활용했었거든요
그러면 이런경우엔 데이터에 접근하지도 않으니 thread-safe에 관한 문제를 고민할 필요도 없습니다
지금 수준의 프로젝트 규모가 유지된다면 사실 지금 방식그대로 싱글톤객체를 사용해도 문제가 전혀 없습니다
하지만 프로젝트 규모가 커지고 여러 기능이 추가되면서 싱글톤 객체에 저장속성이 들어가고 read/write할 경우가 갑자기 생겼을때 queue를 가지고 처리해야하는 로직을 모든 싱글톤객체에 추가를 해줘야합니다
혹은 갑자기 팀원 누군가가 SOLID에서 OCP를 꼭 지키면서 개발을 하자고 제안을 하는데 싱글톤객체가 여러 VC에 결합되어있어서 이걸 지키려면 진짜 모든 싱글톤객체를 드러내야하는 일이 되어버립니다
만약에 프로젝트를 진행하다가 Unit test를 도입하려하면 싱글톤 객체에서 mock data를 얻기가 매우 어렵기때문에 싱글톤객체에서부터 api의 결과로부터 만들어진 mock을 이용한 unit test를 진행하기가 어려워집니다
프로젝트를 설계할때 프로젝트의 확장성을 생각하면
싱글톤이라는 디자인패턴은 thread-safe하더라도 선택하자니 추후에 발생하는 어려움이나 문제점을 생각하면 단순하게 사용하기 편리하다는 이유로 채택하기 어려운 디자인패턴인겁니다
즉, 프로젝트의 상황을 전체적으로 고려하고 선택해야하는 디자인패턴은 맞다고 생각합니다
저의 경우엔 지금까지 진행했던 프로젝트가 unit test할 예정이 없고 SOLID를 고려하기도 어렵고 단순히 api메서드들만 가지고있고 데이터자체를 공유하지 않는 규모였기때문에 단순히 싱글톤 디자인패턴을 채택했던거같네요
프로젝트를 진행할때 단순히 싱글톤은 안티패턴이니까 사용하지마세요!라고 하는 경우를 여럿봤던거같아요
그때마다 thread-safe하지 않고 객체가 여러개 생성될수있어요라는 이야기를 거의 대부분의 상황에서 들었던거같은데 이부분에 대한 오해를 풀고 갈 수 있었던 글이되었던거같아요
물론 싱글톤에 대한 생각은 개발자마다 다르고 언어마다 다를수있다고 생각합니다
그래도 어떤 디자인 패턴을 적용하거나 적용하지 않기 위해서는 근거
가 필요하다고 생각하고 그런 개발자가 되기위해서 공부를 하고있다고 생각하기도 합니다
이 글이 iOS개발자분들에게 근거가 될수있었으면 좋겠습니다 ㅎㅎ
그럼 20000!
싱글턴에 대해서 공부중이였는데 마침 좋은 글 잘 읽고갑니다.