From ecc
Swift의 actor를 사용한 스레드 안전 데이터 영속화 패턴입니다. 메모리 캐시와 파일 기반 저장을 결합해 데이터 레이스를 설계 단계에서 제거합니다.
npx claudepluginhub sam42-lab/everything-claude-code-krThis skill uses the workspace's default tool permissions.
Swift actor를 이용해 스레드 안전한 데이터 영속화 레이어를 만드는 패턴입니다. 메모리 캐시와 파일 기반 저장을 결합하고, actor 모델을 활용해 컴파일 단계에서 데이터 레이스를 없앱니다.
Mandates invoking relevant skills via tools before any response in coding sessions. Covers access, priorities, and adaptations for Claude Code, Copilot CLI, Gemini CLI.
Share bugs, ideas, or general feedback.
Swift actor를 이용해 스레드 안전한 데이터 영속화 레이어를 만드는 패턴입니다. 메모리 캐시와 파일 기반 저장을 결합하고, actor 모델을 활용해 컴파일 단계에서 데이터 레이스를 없앱니다.
actor 모델은 직렬화된 접근을 보장합니다. 데이터 레이스가 없고, 컴파일러가 이를 강제합니다.
public actor LocalRepository<T: Codable & Identifiable> where T.ID == String {
private var cache: [String: T] = [:]
private let fileURL: URL
public init(directory: URL = .documentsDirectory, filename: String = "data.json") {
self.fileURL = directory.appendingPathComponent(filename)
self.cache = Self.loadSynchronously(from: fileURL)
}
public func save(_ item: T) throws {
cache[item.id] = item
try persistToFile()
}
public func delete(_ id: String) throws {
cache[id] = nil
try persistToFile()
}
public func find(by id: String) -> T? {
cache[id]
}
public func loadAll() -> [T] {
Array(cache.values)
}
private func persistToFile() throws {
let data = try JSONEncoder().encode(Array(cache.values))
try data.write(to: fileURL, options: .atomic)
}
private static func loadSynchronously(from url: URL) -> [String: T] {
guard let data = try? Data(contentsOf: url),
let items = try? JSONDecoder().decode([T].self, from: data) else {
return [:]
}
return Dictionary(uniqueKeysWithValues: items.map { ($0.id, $0) })
}
}
actor 격리 때문에 모든 호출은 자동으로 async가 됩니다.
let repository = LocalRepository<Question>()
let question = await repository.find(by: "q-001")
let allQuestions = await repository.loadAll()
try await repository.save(newQuestion)
try await repository.delete("q-001")
@Observable ViewModel과 결합@Observable
final class QuestionListViewModel {
private(set) var questions: [Question] = []
private let repository: LocalRepository<Question>
init(repository: LocalRepository<Question> = LocalRepository()) {
self.repository = repository
}
func load() async {
questions = await repository.loadAll()
}
func add(_ question: Question) async throws {
try await repository.save(question)
questions = await repository.loadAll()
}
}
| 결정 사항 | 근거 |
|---|---|
| Actor 사용 | 컴파일러가 강제하는 스레드 안전성 |
| 메모리 캐시 + 파일 저장 | 빠른 읽기 + 디스크 내구성 |
동기 init 로딩 | async init의 복잡성 회피 |
| ID 키 딕셔너리 | O(1) 조회 성능 |
Codable & Identifiable 제네릭 | 여러 모델에 대한 재사용 가능성 |
.atomic 파일 쓰기 | 앱 중단 시 부분 쓰기(partial write) 방지 |
Sendable 타입을 사용합니다..atomic 쓰기를 사용합니다.init에서 동기 로딩을 선호합니다.@Observable ViewModel과 결합합니다.DispatchQueue나 NSLock 사용await가 필요하다는 점을 잊는 것nonisolated로 actor 격리를 우회하는 것DispatchQueue 기반의 스레드 안전 코드를 현대적인 Swift concurrency로 교체할 때