단일 진실 공급원(Single Source Of Truth)
데이터가 만들어지고 수정되는 작업이 여러 곳에서 일어나는것이 아니라 한 곳에서 발생하면 좋다고 합니다
만약 여러 곳에서 일어나게되면 디버깅할 때 값의 흐름을 추적하는 것이 귀찮고 어려운 작업이 되면서 유지보수할 때 많이 힘들어집니다
정보시스템, 데이터관리 및 소프트웨어 개발에서 사용되는 개념이며 조직 내 모든 사람들이 동일한 데이터를 기반으로 결정을 내리도록 보장하는 것입니다
SSOT(Single Source Of Truth) / SPOT(Single Point Of Truth)
모든 데이터 요소가 편집(마스터) 되도록 정보 및 모델 관련 데이터 스키마를 구성하는 관행입니다
한 곳에서만 정규 형식 데이터를 제공합니다
SSOT 요소
데이터 일관성
동일한 데이터를 사용하도록 하여 불일치를 방지
데이터 정확성
업데이트 및 유지 관리할 소스가 하나뿐이므로 오류를 줄임
데이터 무결성
조직 내에서 데이터가 흐르는 동안 신뢰성 높임
효율성
데이터 관리가 단순해지고 중복된 데이터 저장 및 처리가 줄어듦
SSOT장점
의사결정 향상
일관되고 정확한 데이터로 전반에 사용이 가능
운영 효율성
데이터 관리 프로세스를 간소화하여 데이터 조정에 소요되는 시간과 노력을 줄임
위협 감소
상충되는 데이터 소스로 인한 오류의 위험 최소화
협업 강화
동일한 데이터 세트를 사용하여 부서 간 협업 촉진
SSOT 예시
ERP system
MDM(마스터 데이터 관리)
클라우드 플랫폼
등
우선 swift에서 SSOT를 생각해보고자 하면
싱글톤 패턴으로 통하여 인스턴스가 하나만 생성하고 어디스든 이 인스턴스에 접근할 수 있도록 하는 디자인패턴은
단일 소스를 유지하는데 유용하다고 할 수 있습니다
class DataManager {
static let shared = DataManager()
private init() {}
var data: [String] = []
}
DataManager.Shared.data.apped("hello")
상태관리
어플리케이션 상태를 단일 소스에서 관리해서 데이터 일관성을 유지할 수 있습니다
SwiftUI에서는 @State, @ObservedObject @EnvironmentObject 등 속성을 사용해서 상태 관리를 할 수 있습니다
import SwiftUI
class AppState: ObservableObject {
@Published var counter: Int = 0
}
struct ContentView: View {
@EnvironmentObject var appState: AppState
var body: some View {
VStack {
Text("Counter: \(appState.counter)")
Button("Increment") {
appState.counter += 1
}
}
}
}
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView().environmentObject(AppState())
}
}
}
UIKit로 설명하자면
import Foundation
import Combine
class AppState {
var counter = CurrentValueSubject<Int, Never>(0)
}
import UIKit
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appState = AppState()
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let viewController = ViewController()
viewController.appState = appState
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController(rootViewController: viewController)
window?.makeKeyAndVisible()
return true
}
}
import UIKit
import Combine
class ViewController: UIViewController {
var appState: AppState!
private var cancellables = Set<AnyCancellable>()
private let counterLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 24)
return label
}()
private let incrementButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Increment", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 24)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupViews()
setupConstraints()
setupBindings()
}
private func setupViews() {
view.addSubview(counterLabel)
view.addSubview(incrementButton)
incrementButton.addTarget(self, action: #selector(incrementCounter), for: .touchUpInside)
}
private func setupConstraints() {
counterLabel.translatesAutoresizingMaskIntoConstraints = false
incrementButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
counterLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
counterLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
incrementButton.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: 20),
incrementButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
private func setupBindings() {
appState.$counter
.receive(on: RunLoop.main)
.sink { [weak self] newValue in
self?.counterLabel.text = "Counter: \(newValue)"
}
.store(in: &cancellables)
}
@objc private func incrementCounter() {
appState.counter += 1
}
}
그리고 Core Data / Swift Data로도 할 수 있습니다
import SwiftData
@Model
class Item {
@Attribute(.primaryKey) var id: UUID
var name: String
var timestamp: Date
init(id: UUID = UUID(), name: String, timestamp: Date = Date()) {
self.id = id
self.name = name
self.timestamp = timestamp
}
}
import UIKit
import SwiftData
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let container = PersistentContainer(name: "Model")
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
container.loadPersistentStores { description, error in
if let error = error {
fatalError("Unable to load persistent stores: \(error)")
}
}
let viewController = ViewController()
viewController.viewContext = container.viewContext
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController(rootViewController: viewController)
window?.makeKeyAndVisible()
return true
}
}
import UIKit
import SwiftData
class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
var viewContext: NSManagedObjectContext!
var items: [Item] = []
let tableView = UITableView()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
title = "Items"
tableView.frame = view.bounds
tableView.dataSource = self
tableView.delegate = self
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "Cell")
view.addSubview(tableView)
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addItem))
fetchItems()
}
func fetchItems() {
let fetchRequest: NSFetchRequest<Item> = Item.fetchRequest()
let sortDescriptor = NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
do {
items = try viewContext.fetch(fetchRequest)
tableView.reloadData()
} catch {
print("Failed to fetch items: \(error)")
}
}
@objc func addItem() {
let newItem = Item(context: viewContext)
newItem.name = "New Item"
newItem.timestamp = Date()
do {
try viewContext.save()
items.append(newItem)
tableView.reloadData()
} catch {
print("Failed to save item: \(error)")
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) ->
위 예제에서는 Item을 정의하고 앱델리게이트에서 PresistentController를 설정한 다음 ViewController에서 UITableView를 사용하여 데이터를 표시 및 관리하는 방법을 보여주고 있습니다
+
그 외에도 combine을 사용하여 상태를 관리하고 UI update 하는 방법
import UIKit
import Combine
// AppState 클래스는 앱의 상태를 관리하는 역할을 합니다.
// 여기서는 counter라는 상태를 CurrentValueSubject를 사용하여 정의합니다.
// CurrentValueSubject는 현재 값을 저장하고, 그 값이 변경될 때마다 모든 구독자에게 알림을 보냅니다.
class AppState {
var counter = CurrentValueSubject<Int, Never>(0)
}
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
var appState = AppState()
// 애플리케이션이 시작될 때 호출되는 메서드입니다.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let viewController = ViewController()
viewController.appState = appState
// UIWindow를 설정하고, UINavigationController를 통해 ViewController를 루트 뷰 컨트롤러로 설정합니다.
window = UIWindow(frame: UIScreen.main.bounds)
window?.rootViewController = UINavigationController(rootViewController: viewController)
window?.makeKeyAndVisible() //창을 설정하고 표시하는데 사용
// 사용하지 않는경우 창에 표시되지 않음
return true
}
}
class ViewController: UIViewController {
var appState: AppState!
private var cancellables = Set<AnyCancellable>() // Combine 구독을 관리하는 Set입니다.
// UILabel을 초기화하여 counter 값을 표시합니다.
private let counterLabel: UILabel = {
let label = UILabel()
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 24)
return label
}()
// UIButton을 초기화하여 counter 값을 증가시키는 역할을 합니다.
private let incrementButton: UIButton = {
let button = UIButton(type: .system)
button.setTitle("Increment", for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 24)
return button
}()
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .white
setupViews() // UI 요소를 뷰에 추가합니다.
setupConstraints() // UI 요소의 제약 조건을 설정합니다.
setupBindings() // Combine을 사용하여 상태 변경을 관찰하고 UI를 업데이트합니다.
}
private func setupViews() {
view.addSubview(counterLabel) // UILabel을 뷰에 추가합니다.
view.addSubview(incrementButton) // UIButton을 뷰에 추가합니다.
// UIButton에 액션을 추가하여 버튼이 눌릴 때 counter 값을 증가시키는 메서드를 호출합니다.
incrementButton.addTarget(self, action: #selector(incrementCounter), for: .touchUpInside)
}
private func setupConstraints() {
// Auto Layout을 사용하여 UI 요소의 제약 조건을 설정합니다.
counterLabel.translatesAutoresizingMaskIntoConstraints = false
incrementButton.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
counterLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
counterLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
incrementButton.topAnchor.constraint(equalTo: counterLabel.bottomAnchor, constant: 20),
incrementButton.centerXAnchor.constraint(equalTo: view.centerXAnchor)
])
}
private func setupBindings() {
// Combine을 사용하여 counter 값의 변경을 관찰하고 UILabel의 텍스트를 업데이트합니다.
appState.counter
.receive(on: RunLoop.main) // 메인 스레드에서 값을 수신하도록 설정합니다.
.sink { [weak self] newValue in
self?.counterLabel.text = "Counter: \(newValue)" // counter 값이 변경될 때마다 UILabel의 텍스트를 업데이트합니다.
}
.store(in: &cancellables) // 구독을 Set에 저장하여 메모리 관리를 합니다.
}
@objc private func incrementCounter() {
// UIButton이 눌릴 때 호출되는 메서드입니다. counter 값을 1 증가시킵니다.
appState.counter.value += 1
}
}
https://en.wikipedia.org/wiki/Single_source_of_truth