🤖 이 글은 Claude Code(AI)가 작성합니다. | 시리즈 목차 | 이전: [27편] FileManager와 파일 시스템
파일 저장의 한계와 데이터베이스
JSON 파일로 데이터를 저장하면 간단하지만, 데이터가 많아질수록 문제가 생깁니다.
- 10,000개의 메모를 불러오려면 전체 JSON 파일을 읽어야 합니다
- “2024년에 작성한 메모만 검색”하려면 모든 데이터를 메모리에 올려야 합니다
- 여러 스레드에서 동시에 파일에 쓰면 데이터가 깨질 수 있습니다
이런 문제를 해결하는 것이 데이터베이스이고, macOS/iOS 앱에서 가장 널리 쓰이는 내장 데이터베이스가 SQLite입니다.
SQLite란?
SQLite는 서버가 필요 없는 파일 기반 관계형 데이터베이스입니다. 전 세계에서 가장 많이 배포된 데이터베이스로, 스마트폰 앱, 브라우저, 항공기 소프트웨어까지 다양한 곳에서 사용됩니다.
- 단일
.db파일에 모든 데이터 저장 - SQL 쿼리로 빠른 검색·정렬·필터링
- 트랜잭션으로 데이터 일관성 보장
- macOS/iOS에
libsqlite3로 기본 내장
libsqlite3 — C API 직접 사용
Swift에서 SQLite를 사용하는 가장 기본적인 방법은 시스템에 내장된 C 라이브러리를 직접 호출하는 것입니다.
import Foundation
import SQLite3 // 또는 import CSQLite
class Database {
private var db: OpaquePointer?
init(path: String) throws {
// 데이터베이스 열기 (없으면 생성)
let result = sqlite3_open(path, &db)
if result != SQLITE_OK {
throw DatabaseError.openFailed(sqlite3_errmsg(db).map(String.init) ?? "알 수 없는 오류")
}
}
deinit {
sqlite3_close(db)
}
}
enum DatabaseError: Error {
case openFailed(String)
case queryFailed(String)
}
테이블 생성
extension Database {
func createTables() throws {
let sql = """
CREATE TABLE IF NOT EXISTS notes (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
body TEXT NOT NULL DEFAULT '',
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
CREATE INDEX IF NOT EXISTS notes_created_at ON notes(created_at DESC);
"""
var errorMsg: UnsafeMutablePointer<CChar>?
let result = sqlite3_exec(db, sql, nil, nil, &errorMsg)
if result != SQLITE_OK {
let msg = errorMsg.map(String.init) ?? "알 수 없는 오류"
sqlite3_free(errorMsg)
throw DatabaseError.queryFailed(msg)
}
}
}
데이터 삽입 — Prepared Statement
SQL 인젝션을 막고 성능을 높이기 위해 항상 prepared statement를 사용합니다.
struct Note {
let id: String
var title: String
var body: String
var createdAt: Date
var updatedAt: Date
}
extension Database {
func insert(_ note: Note) throws {
let sql = """
INSERT OR REPLACE INTO notes (id, title, body, created_at, updated_at)
VALUES (?, ?, ?, ?, ?);
"""
var stmt: OpaquePointer?
defer { sqlite3_finalize(stmt) } // 항상 해제
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
throw DatabaseError.queryFailed("prepare 실패")
}
// ? 자리에 값 바인딩 (1-indexed)
sqlite3_bind_text(stmt, 1, note.id, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 2, note.title, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 3, note.body, -1, SQLITE_TRANSIENT)
sqlite3_bind_double(stmt, 4, note.createdAt.timeIntervalSince1970)
sqlite3_bind_double(stmt, 5, note.updatedAt.timeIntervalSince1970)
guard sqlite3_step(stmt) == SQLITE_DONE else {
throw DatabaseError.queryFailed("insert 실패")
}
}
}
데이터 조회
extension Database {
func fetchAll() throws -> [Note] {
let sql = "SELECT id, title, body, created_at, updated_at FROM notes ORDER BY created_at DESC;"
var stmt: OpaquePointer?
defer { sqlite3_finalize(stmt) }
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else {
throw DatabaseError.queryFailed("prepare 실패")
}
var notes: [Note] = []
while sqlite3_step(stmt) == SQLITE_ROW {
let id = String(cString: sqlite3_column_text(stmt, 0))
let title = String(cString: sqlite3_column_text(stmt, 1))
let body = String(cString: sqlite3_column_text(stmt, 2))
let createdAt = Date(timeIntervalSince1970: sqlite3_column_double(stmt, 3))
let updatedAt = Date(timeIntervalSince1970: sqlite3_column_double(stmt, 4))
notes.append(Note(id: id, title: title, body: body,
createdAt: createdAt, updatedAt: updatedAt))
}
return notes
}
func search(query: String) throws -> [Note] {
let sql = "SELECT id, title, body, created_at, updated_at FROM notes WHERE title LIKE ? OR body LIKE ? ORDER BY updated_at DESC LIMIT 50;"
var stmt: OpaquePointer?
defer { sqlite3_finalize(stmt) }
sqlite3_prepare_v2(db, sql, -1, &stmt, nil)
let pattern = "%\(query)%"
sqlite3_bind_text(stmt, 1, pattern, -1, SQLITE_TRANSIENT)
sqlite3_bind_text(stmt, 2, pattern, -1, SQLITE_TRANSIENT)
var results: [Note] = []
while sqlite3_step(stmt) == SQLITE_ROW {
// 위와 동일하게 행 읽기...
let id = String(cString: sqlite3_column_text(stmt, 0))
let title = String(cString: sqlite3_column_text(stmt, 1))
let body = String(cString: sqlite3_column_text(stmt, 2))
let createdAt = Date(timeIntervalSince1970: sqlite3_column_double(stmt, 3))
let updatedAt = Date(timeIntervalSince1970: sqlite3_column_double(stmt, 4))
results.append(Note(id: id, title: title, body: body,
createdAt: createdAt, updatedAt: updatedAt))
}
return results
}
}
트랜잭션
여러 작업을 하나의 원자적 단위로 처리할 때 트랜잭션을 사용합니다. 중간에 실패하면 전부 취소(롤백)됩니다.
extension Database {
func transaction(_ block: () throws -> Void) throws {
sqlite3_exec(db, "BEGIN TRANSACTION;", nil, nil, nil)
do {
try block()
sqlite3_exec(db, "COMMIT;", nil, nil, nil)
} catch {
sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
throw error
}
}
}
// 사용
try db.transaction {
try db.insert(note1)
try db.insert(note2)
try db.insert(note3)
// 셋 중 하나라도 실패하면 전부 취소
}
WAL 모드 — 성능 향상
SQLite의 기본 저널 모드 대신 WAL(Write-Ahead Logging) 모드를 사용하면 읽기와 쓰기를 동시에 할 수 있어 성능이 크게 향상됩니다.
// 데이터베이스 열 때 한 번만 설정
sqlite3_exec(db, "PRAGMA journal_mode=WAL;", nil, nil, nil)
sqlite3_exec(db, "PRAGMA synchronous=NORMAL;", nil, nil, nil)
sqlite3_exec(db, "PRAGMA cache_size=-4000;", nil, nil, nil) // 4MB 캐시
SwiftData (iOS 17 / macOS 14+)
Apple이 2023년 발표한 SwiftData는 SQLite 위에 구축된 고수준 프레임워크로, Core Data를 대체합니다. 복잡한 SQL 없이 Swift 코드로 데이터를 모델링하고 저장할 수 있습니다.
import SwiftData
@Model // SwiftData 모델 선언
class Note {
var id: UUID
var title: String
var body: String
var createdAt: Date
init(title: String, body: String) {
self.id = UUID()
self.title = title
self.body = body
self.createdAt = .now
}
}
// SwiftUI와 통합
struct NoteApp: App {
var body: some Scene {
WindowGroup {
NoteListView()
}
.modelContainer(for: Note.self) // 컨테이너 설정 한 줄로 끝
}
}
struct NoteListView: View {
@Query(sort: \Note.createdAt, order: .reverse) var notes: [Note]
@Environment(\.modelContext) var context
var body: some View {
List(notes) { note in
Text(note.title)
}
.toolbar {
Button("추가") {
context.insert(Note(title: "새 메모", body: ""))
}
}
}
}
최신 macOS/iOS 앱을 새로 시작한다면 SwiftData를 사용하는 것이 권장됩니다. 구버전 지원이 필요하거나 성능을 세밀하게 제어해야 한다면 SQLite를 직접 사용합니다.
데이터 저장 방법 비교
| 방법 | 적합한 용도 | 용량 |
|---|---|---|
| UserDefaults | 앱 설정, 간단한 값 | 소량 |
| Codable + JSON 파일 | 중소규모 데이터, 내보내기 | 중간 |
| SQLite | 대량 데이터, 검색·정렬 필요 | 대용량 |
| SwiftData | 관계형 모델, Swift-native API | 대용량 |
| Core Data | 레거시 코드, 구버전 지원 | 대용량 |
핵심 요약
- SQLite: 서버 불필요한 파일 기반 관계형 DB. macOS/iOS에 기본 내장
- sqlite3_open: DB 열기/생성. sqlite3_close: 반드시 닫기
- Prepared statement: SQL 인젝션 방지 + 성능 향상.
?로 바인딩 - SQLITE_ROW / SQLITE_DONE: sqlite3_step 결과 코드
- 트랜잭션: BEGIN → 작업 → COMMIT/ROLLBACK으로 원자성 보장
- WAL 모드: 읽기·쓰기 동시 처리로 성능 향상
- SwiftData: iOS 17+/macOS 14+ 신규 앱에 권장되는 고수준 API
5부 Foundation과 데이터 저장이 완결됩니다. 다음 편부터는 6부 네트워킹과 IPC로, TCP 소켓, Unix Domain Socket, 프로세스 간 통신을 다룹니다.
🤖 Generated with Claude Code