SSOT란?
단일 진실 공급원이라고 부르며 모든 데이터 요소를 한 곳에서만 제어 또는 편집하도록 조직하는 방법론이다.
즉, "진실은 단 하나의 장소에만 있어야 한다" 같은 데이터를 보고 같은 곳에서 관리해야 한다라는 원칙을 말한다.
단어의 원천
SSOT라는 용어는 특정 분야에 국한되어 나타난 용어는 아니지만 정보 관리와 데이터 일관성이 필요한 ERP, DB 설계에서 데이터 일관성의 중요성이 대두되며 "하나의 시스템, 소스만이 데이터 원천이 되어야 한다."라는 방법론이 나타났다.
iOS 개발에서의 필요성
- 상태가 여러 화면과 기능에서 공유된다. 즉, 한 곳에서 관리되지 않으면 데이터 불일치와 UI의 이상이 발생할 수 있다.
- 캐시, 네트워크, 로컬 데이터가 동시에 존재한다. 즉, SSOT가 없으면 어떤 값이 최신인지 판단할 수 없다.
iOS 개발을 하며 SSOT를 지키지 않아 생겼던 문제
상황
SwiftUI로 개발을 하면서 View에서만 사용될 것이라고 판단하는 상태는 ViewModel에 공통으로 사용될 것이라고 판단되는 상태는 EnvironmentObject로 나눠 관리를 진행한다.
만약 EnvironmentObject에 상태를 가지고 있지만 EnvironmentObject 내부에 코드를 넣기에는 코드양이 너무 많이 질 것이라고 생각되고 ViewModel에 들어가야 한다라고 생각이 들 때가 있다.
(실제로 UserEnvironmentObject라는 식의 객체에는 정말 많은 코드가 들어갈 여지를 주게 된다.)
그렇다면 EnvironmentObject에 상태값이 있고 그걸 ViewModel에서 받아서 처리를 해야하는데
이 상태를 전달할 수 있는 위치는 보통 View가 될 것이다.
.onChange(of: userEO.age, perform: { value in
profileViewModel.age = value
})
다음과 같이 전달을 하게 되었다.
하지만 이렇게 전달하게 되면 문제는 이 onChange가 위치는 View의 위치가 AView인지 ProfileView인지 MainView인지 명확한 기준이 없다면 다시 찾아서 처리를 하는데 어려움을 겪게 된다는 것이다.
특히, 다른 개발자 찾기가 정말 쉽지 않을 것이다.
iOS 개발을 하면 SSOT가 지켜지지 않았던 이유
1) 상태값을 추가로 만드는 경우

Clean Architecture 구조에 MVVM 구조라고 가정을 하고 설명을 진행하도록 하겠다.
- A ViewModel에서 사용하는 상태값을 B ViewModel에서도 사용되어야 하는 경우, 값을 공통으로 빼서 EnvironmentObject로 만들어야 하는 것이 원칙이지만 그대로 만들어서 사용하게 되는 경우가 있다.
struct Profile {
let name
let age
let phoneNumber
}
@Published var profile: Profile?
@Published var myAge: Int?
개발을 하다보면 쉽게 처리를 하고 싶은 욕구가 생기고 그렇게 되면 단일 책임 원칙을 가진 데이터 이외의 값들을 추가적으로 만드는 경우가 생긴다. 이렇게 되면 myAge를 변경할 때 히스토리가 없는 개발자는 myAge 값을 변경하게 되고 profile을 바라보고 있는 View의 age는 바뀌지 않아 이슈가 생길 수 있다.
때문에 이를 방지하기 위해서는 파생된 값을 만들지 않는 노력이 필요하다.
SSOT를 지키기 위한 방법
뷰가 모델과 직접 통신할 수 있도록 만든 패턴
SwiftUI에서 View와 ViewModel을 따로 구분하지 않고 View 안에 있는 @State 프로퍼티 래퍼를 바인딩할 수 있는 ViewModel로 보는 방식

생각해봤을 때 개념으로 봤을 때는 SSOT를 잘 지킬 수 있는 방법이라고 생각이 든다. 하지만 앱의 규모가 커진다면 Model이 엄청 많이질 것이고 View에서 바라보는 Model의 양이 엄청 많아져 결국 혼란을 일으킬 수 있다고 생각한다.
public final class AppStore {
static let contentStore = ContentStore()
static let profileStore = ProfileStore()
}
public final class ProfileStore {
public init() {
}
var info: infoEntity?
var friendsList: FriendListEntity?
}
public final class ContentStore {
var contentBookmark: AvatarTalkBookmarkEntity?
}
다음과 같이 DataSource로부터 불러온 데이터를 AppStore라는 장소에 저장한다.
그리고 다시 ContentBookmarks를 호출하고자 할 때 AppStore를 SSOT로 여기고 이곳에서 데이터를 가져오는 것이다.
public func getContentBookmarks() -> AnyPublisher<ContentBookmarkEntity, APIError> {
if let cached = AppStore.avatarTalkStore.avatarTalkBookmarks {
return Just(cached)
.setFailureType(to: APIError.self)
.eraseToAnyPublisher()
} else {
return dataSource.getAvatarTalkBookmarks()
.map({ avatarTalkBookmarkDTO in
let avatarTalkBookmarkEntity = avatarTalkBookmarkDTO.toEntity()
return avatarTalkBookmarkEntity
})
.handleEvents(receiveOutput: { entity in
AppStore.avatarTalkStore.avatarTalkBookmarks = entity // ✅ 캐시에 저장
})
.eraseToAnyPublisher()
}
}
이렇게 Repository 레이어에 AppStore라는 저장소를 만듦으로써 SSOT를 지키면서 개발을 할 수 있었다.
기존에 문제였던 View에서 onChange를 통해 값을 가져오지 않아도 되고 AppStore가 SSOT라고 정의되어 있기 때문에 ViewModel에서 값을 파생해서 다른 상태를 만들지 않고 가져다 쓸 수 있는 구조가 되었다.
하지만 바인딩이 되어있지 않아 View에서 onAppear가 되는 순간마다 값을 다시 가져하기 때문에 어떤값은 실제로 서버에서 가져와야 하고 어떤값은 캐싱으로 가지고 있어도 된다라는 한 가지 고민을 더 해야하는 불편함은 있다고 느껴졌다.
'개발지식' 카테고리의 다른 글
| single source 아키텍처로 가는 길(2) - Flux (0) | 2025.08.30 |
|---|---|
| UIKit) 최상단을 계속 유지하는 토스트 메세지 만들기 (0) | 2025.02.17 |
| 클린 아키텍처(CleanArchitecture) 의존성 역전(DIP)에 대해 (2) | 2023.11.13 |
| swift) git 공부하기5 github 연결 git remote add, git push (0) | 2022.08.13 |