iOS & Swift

Swift 모노스테이트 패턴(Monostate Pattern), 싱글톤의 대안이 될 수 있을까

싱글톤 진짜 이대로 괜찮을까 싶었던 분들께

이석우iOS Developer5분 읽기
Swift 모노스테이트 패턴(Monostate Pattern), 싱글톤의 대안이 될 수 있을까 대표 이미지

싱글톤 진짜 이대로 괜찮을까 싶었던 분들께

iOS 개발하다 보면 싱글톤(Singleton) 안 써본 사람이 없을 거예요. 저도 그랬어요. SomeManager.shared 이런 코드, 프로젝트마다 몇 개씩은 꼭 있었거든요.

그런데 어느 순간부터 이 .shared가 슬슬 불편해지더라고요. 테스트할 때 상태가 엉키고 팀원들이 여기저기서 마음대로 접근하고요.

그러다 알게 된 게 바로 모노스테이트(Monostate) 패턴이에요. 오늘은 Swift에서 모노스테이트가 뭔지, 그리고 정말 싱글톤의 대안이 될 수 있는지 제가 직접 써본 경험을 나눠볼게요.

싱글톤과 모노스테이트, 이 그림 하나로 정리돼요
싱글톤과 모노스테이트, 이 그림 하나로 정리돼요

먼저 결론부터 말씀드리면, 모노스테이트는 “상태는 하나로 공유하되 인스턴스는 여러 개 만들 수 있게” 하는 패턴이에요. 싱글톤과 목적은 같지만 접근 방식이 정반대죠. 다만 만능은 아니라 상황에 따라 갈려요. 아래에서 하나씩 풀어볼게요.

모노스테이트 패턴이 대체 뭔가요?

싱글톤은 “인스턴스를 딱 하나만 만든다”가 핵심이에요. 생성자를 막아두고 shared 하나만 쓰게 하죠.

모노스테이트는 반대예요. 인스턴스는 얼마든지 만들 수 있어요. 대신 그 안의 상태(데이터)를 전부 static으로 공유해버려요.

즉 객체 A를 만들든 B를 만들든, 둘이 바라보는 데이터는 같은 곳이에요. 겉으로는 평범한 객체처럼 보이는데 속은 하나로 연결돼 있는 거죠.

Swift로 간단히 보면 이런 모양이에요.

struct Settings {
    private static var _volume = 50   // 상태를 static으로 공유
    var volume: Int {                 // 인스턴스처럼 쓰지만
        get { Settings._volume }
        set { Settings._volume = newValue }
    }
}
// 서로 다른 인스턴스지만 상태는 하나
var a = Settings(); a.volume = 80
print(Settings().volume)  // 80

Settings()를 새로 만들어도 volume은 80으로 나와요. 인스턴스는 여러 개인데 상태는 하나, 이게 모노스테이트의 전부예요.

인스턴스는 셋인데 바라보는 상태는 하나입니다
인스턴스는 셋인데 바라보는 상태는 하나입니다

싱글톤이랑 뭐가 다른가요?

가장 크게 다른 건 사용하는 쪽 코드예요.

싱글톤은 쓰는 사람이 항상 .shared를 의식해야 해요. “아 이건 싱글톤이지” 하면서요. 모노스테이트는 그냥 평범한 객체처럼 Settings() 만들어 쓰면 돼서, 쓰는 쪽이 내부 구현을 몰라도 돼요.

표로 정리하면 이래요.

구분 싱글톤 모노스테이트
인스턴스 개수 딱 1개 여러 개 가능
공유되는 것 인스턴스 자체 상태(static 데이터)
사용하는 쪽 .shared 명시 일반 객체처럼
상속 까다로움 상대적으로 자유로움

특히 상속이 재밌어요. 싱글톤은 상속하기가 참 애매한데, 모노스테이트는 일반 타입이라 확장이 좀 더 유연한 편이에요.


그래서 싱글톤의 대안이 될 수 있나요?

솔직하게 말하면, 될 때도 있고 아닐 때도 있어요.

장점부터요. 쓰는 쪽 코드가 깔끔해지고 .shared라는 전역 접근점이 코드 곳곳에 박히지 않아요. 인터페이스가 일반 객체랑 똑같으니 나중에 구조를 바꿀 때도 부담이 적고요.

모노스테이트는 “전역 상태를 감추는” 패턴이에요. 편해 보이지만, 감춰진 전역 상태는 그 자체로 양날의 검입니다.

문제는 여기예요. 겉보기엔 평범한 객체라서 팀원이 “이거 그냥 지역 객체겠지” 하고 만들었는데 알고 보니 상태가 전역으로 공유되고 있으면 오히려 더 헷갈려요.

그리고 static 공유라 멀티스레드 환경에서 동시성 문제도 똑같이 신경 써야 해요. 이 부분은 싱글톤이랑 다를 게 없어요.

제 경험으로는, 요즘 Swift라면 모노스테이트보다 의존성 주입(Dependency Injection)을 먼저 고려하는 걸 추천해요. 테스트도 쉽고 의존 관계도 명확하거든요.

요즘 Swift 프로젝트라면 저는 DI부터 꺼내 듭니다
요즘 Swift 프로젝트라면 저는 DI부터 꺼내 듭니다

그럼 언제 쓰면 좋을까요?

정리하면 이런 경우예요.

  1. 기존 싱글톤 코드가 너무 많아서 점진적으로 걷어내고 싶을 때
  2. 사용하는 쪽 인터페이스를 일반 객체처럼 깔끔하게 유지하고 싶을 때
  3. 상속이나 확장이 필요한 전역 상태를 다뤄야 할 때

반대로 새 프로젝트를 처음부터 짜는 거라면, 저는 모노스테이트보다 의존성 주입이나 명시적인 상태 관리를 먼저 보라고 말하고 싶어요.

저는 패턴을 도구함에 넣어두고 상황 맞을 때 꺼내 씁니다
저는 패턴을 도구함에 넣어두고 상황 맞을 때 꺼내 씁니다

결국 모노스테이트는 “싱글톤의 완벽한 상위호환”이 아니라 “성격이 다른 또 하나의 선택지”예요. 도구함에 넣어두고, 상황 맞을 때 꺼내 쓰면 되는 거죠.

오늘 내용이 .shared 남발에 지쳤던 분들께 작은 힌트가 됐으면 좋겠어요. 무작정 갈아타기보다, 우리 프로젝트에 뭐가 맞을지 한 번쯤 고민해보시길 응원할게요!

함께 보면 좋은 글