Flutter Drift vs SQFlite: Repository 패턴 적용과 마이그레이션
결론
Drift를 추천합니다. Repository 패턴 적용과 마이그레이션 모두에서 Drift가 더 우수합니다.
- Repository 패턴 적용: Drift는 타입 세이프한 DAO를 자동 생성하여 Repository 패턴과 자연스럽게 통합됩니다. Flutter databases overview (opens in a new tab)
- 마이그레이션 용이성: Drift는 공식 마이그레이션 도구(
drift_dev)를 제공하여 스키마 변경을 자동으로 추적하고 마이그레이션 코드를 생성합니다. Drift Migrations Guide (opens in a new tab) - 커뮤니티 평가: 2025년 기준 Drift는 "default choice and the right one for most projects"로 평가받고 있습니다. Best Local Database for Flutter Apps (opens in a new tab)
Flutter databases overview에서 "Drift has emerged as the go-to choice for type-safe SQL database access. It's actively maintained and has excellent documentation and community support"라고 명시되어 있습니다.
Repository 패턴 적용 비교
Drift의 장점
- 자동 추상화 레이어: 코드 생성으로 DAO가 자동 생성되어 Repository처럼 동작합니다
- 타입 안정성: 컴파일 타임에 쿼리 오류를 검출합니다
- 보일러플레이트 최소화: 수동으로 작성할 코드가 적습니다
// Drift - 테이블 정의만으로 DAO 자동 생성
class Todos extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text()();
BoolColumn get completed => boolean().withDefault(const Constant(false))();
}
// 자동 생성된 DAO 사용
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 1;
// DAO 메서드가 자동으로 생성됨
Future<List<Todo>> getAllTodos() => select(todos).get();
Future<int> insertTodo(TodosCompanion todo) => into(todos).insert(todo);
}
// Repository는 단순히 Database를 래핑
class TodoRepository {
final AppDatabase _db;
TodoRepository(this._db);
Future<List<Todo>> getTodos() => _db.getAllTodos();
Future<void> addTodo(String title) =>
_db.insertTodo(TodosCompanion.insert(title: title));
}Drift 공식 문서 (opens in a new tab)에서 "Drift generates type-safe Dart code from your SQL tables"라고 명시되어 있습니다.
SQFlite의 특징
- 직접 제어: SQL을 직접 작성하여 세밀한 제어 가능
- 수동 추상화: 모든 추상화 레이어를 직접 작성해야 함
- 유연성: 필요한 만큼만 추상화 가능
// SQFlite - 모든 코드를 수동 작성
class TodoDao {
final Database db;
TodoDao(this.db);
Future<List<Map<String, dynamic>>> getAllTodos() async {
return await db.query('todos');
}
Future<int> insertTodo(Map<String, dynamic> todo) async {
return await db.insert('todos', todo);
}
// fromMap, toMap 메서드도 수동 작성 필요
Todo fromMap(Map<String, dynamic> map) {
return Todo(
id: map['id'],
title: map['title'],
completed: map['completed'] == 1,
);
}
Map<String, dynamic> toMap(Todo todo) {
return {
'id': todo.id,
'title': todo.title,
'completed': todo.completed ? 1 : 0,
};
}
}
// Repository 구현
class TodoRepository {
final TodoDao _dao;
TodoRepository(this._dao);
Future<List<Todo>> getTodos() async {
final maps = await _dao.getAllTodos();
return maps.map((m) => _dao.fromMap(m)).toList();
}
Future<void> addTodo(Todo todo) async {
await _dao.insertTodo(_dao.toMap(todo));
}
}Flutter databases comparison (opens in a new tab)에서 "The main drawback of sqflite is you have to write raw SQL, which is something most people don't like"라고 언급되어 있습니다.
비교 요약
| 항목 | Drift | SQFlite |
|---|---|---|
| 코드 생성 | 자동 | 수동 |
| 타입 안정성 | 컴파일 타임 | 런타임 |
| 보일러플레이트 | 최소 | 많음 |
| 학습 곡선 | 중간 | 낮음 |
| Repository 통합 | 쉬움 | 보통 |
| 유연성 | 높음 | 매우 높음 |
Drift 마이그레이션 방법
1. 초기 설정
# pubspec.yaml
dependencies:
drift: ^2.14.0
sqlite3_flutter_libs: ^0.5.0
path_provider: ^2.1.0
path: ^1.8.0
dev_dependencies:
drift_dev: ^2.14.0
build_runner: ^2.4.02. 스키마 변경 및 마이그레이션 프로세스
// STEP 1: schemaVersion 증가
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 2; // 1에서 2로 증가
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (migrator, from, to) async {
if (from == 1) {
// 마이그레이션 로직
await migrator.addColumn(todos, todos.dueDate);
}
},
);
}3. 자동 마이그레이션 도구 사용
# STEP 1: 스키마 덤프 (각 버전마다 실행)
dart run drift_dev schema dump lib/database/app_database.dart db_schemas/
# STEP 2: 마이그레이션 코드 생성
dart run drift_dev schema steps db_schemas/ lib/database/migration.dart생성된 migration.dart 파일:
// 자동 생성됨
import 'package:drift/drift.dart';
import 'package:drift_dev/api/migrations.dart';
class GeneratedMigrations extends GeneratedHelper {
@override
int get schemaVersion => 2;
@override
Future<void> onCreate(Migrator m) async {
await m.createTable(todos);
}
@override
Future<void> onUpgrade(Migrator m, int from, int to) async {
if (from == 1) {
await m.addColumn(todos, todos.dueDate);
}
}
}4. 생성된 마이그레이션 적용
@DriftDatabase(tables: [Todos])
class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 2;
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (migrator, from, to) async {
// 생성된 마이그레이션 사용
await GeneratedMigrations().onUpgrade(migrator, from, to);
},
);
}Drift Migrations Documentation (opens in a new tab)에서 "After changing your database schema and running the make-migrations command, Drift generates a database.steps.dart file"이라고 설명되어 있습니다.
5. 복잡한 마이그레이션 예시
@override
MigrationStrategy get migration => MigrationStrategy(
onUpgrade: (migrator, from, to) async {
if (from < 2) {
// 컬럼 추가
await migrator.addColumn(todos, todos.dueDate);
}
if (from < 3) {
// 새 테이블 생성
await migrator.createTable(categories);
// 데이터 이전
await customStatement(
'INSERT INTO categories (name) SELECT DISTINCT category FROM todos'
);
}
if (from < 4) {
// 컬럼 이름 변경 (SQLite는 직접 지원 안함)
await migrator.alterTable(
TableMigration(
todos,
columnTransformer: {
todos.title: todos.title.dartCast<String>(),
todos.description: const CustomExpression('old_desc'),
},
),
);
}
},
);6. 마이그레이션 테스트
import 'package:drift_dev/api/migrations.dart';
import 'package:test/test.dart';
void main() {
late SchemaVerifier verifier;
setUpAll(() {
verifier = SchemaVerifier(GeneratedHelper());
});
test('schema v1 to v2 migration', () async {
final connection = await verifier.startAt(1);
final db = AppDatabase(connection);
// v1에서 데이터 삽입
await db.into(db.todos).insert(
TodosCompanion.insert(title: 'Test'),
);
// v2로 마이그레이션
final migratedDb = AppDatabase(
await verifier.migrateAndValidate(db, 2),
);
// 데이터 검증
final todos = await migratedDb.select(migratedDb.todos).get();
expect(todos.length, 1);
expect(todos.first.title, 'Test');
});
}Drift Migration Guide (opens in a new tab)에서 "Drift provides tooling and APIs to safely change database schemas, including command-line tools and testing utilities"라고 명시되어 있습니다.
SQFlite 마이그레이션 방법
1. 초기 설정
# pubspec.yaml
dependencies:
sqflite: ^2.3.0
path_provider: ^2.1.0
path: ^1.8.02. 기본 마이그레이션 구조
class DatabaseProvider {
static final DatabaseProvider instance = DatabaseProvider._();
static Database? _database;
DatabaseProvider._();
Future<Database> get database async {
if (_database != null) return _database!;
_database = await _initDatabase();
return _database!;
}
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'app_database.db');
return await openDatabase(
path,
version: 3, // 현재 버전
onCreate: _onCreate,
onUpgrade: _onUpgrade,
);
}
Future<void> _onCreate(Database db, int version) async {
// 최신 스키마로 생성
await db.execute('''
CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
completed INTEGER NOT NULL DEFAULT 0,
due_date TEXT,
category_id INTEGER,
FOREIGN KEY (category_id) REFERENCES categories(id)
)
''');
await db.execute('''
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
''');
}
Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
// 버전별 마이그레이션
if (oldVersion < 2) {
await db.execute('ALTER TABLE todos ADD COLUMN due_date TEXT');
}
if (oldVersion < 3) {
await db.execute('''
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE
)
''');
await db.execute(
'ALTER TABLE todos ADD COLUMN category_id INTEGER'
);
// 데이터 이전
await db.execute('''
INSERT INTO categories (name)
SELECT DISTINCT category FROM todos WHERE category IS NOT NULL
''');
}
}
}SQFlite migration example (opens in a new tab)에서 공식 마이그레이션 패턴을 제공합니다.
3. sqflite_migration_plan 패키지 사용
dependencies:
sqflite_migration_plan: ^3.0.0import 'package:sqflite_migration_plan/sqflite_migration_plan.dart';
class DatabaseProvider {
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'app_database.db');
return await openDatabaseWithMigration(
path,
MigrationConfig(
initializationScript: [
'''CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL
)'''
],
migrationScripts: [
// 버전 2 마이그레이션
'''ALTER TABLE todos ADD COLUMN completed INTEGER NOT NULL DEFAULT 0''',
// 버전 3 마이그레이션
'''CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
)''',
'''ALTER TABLE todos ADD COLUMN category_id INTEGER''',
],
),
);
}
}sqflite_migration_plan (opens in a new tab)은 "flexible migrations (upgrade and downgrade) for Flutter sqflite databases"를 제공합니다.
4. 파일 기반 마이그레이션 (sqflite_migrate)
dependencies:
sqflite_migrate: ^1.0.0assets/
migrations/
001_initial.sql
002_add_due_date.sql
003_add_categories.sql-- assets/migrations/001_initial.sql
-- UP
CREATE TABLE todos (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL
);
-- DOWN
DROP TABLE todos;-- assets/migrations/002_add_due_date.sql
-- UP
ALTER TABLE todos ADD COLUMN due_date TEXT;
-- DOWN
ALTER TABLE todos DROP COLUMN due_date;import 'package:sqflite_migrate/sqflite_migrate.dart';
class DatabaseProvider {
Future<Database> _initDatabase() async {
final dbPath = await getDatabasesPath();
final path = join(dbPath, 'app_database.db');
final db = await openDatabase(path);
await migrate(
db,
migrationsPath: 'assets/migrations',
);
return db;
}
}sqflite_migrate (opens in a new tab)는 "file-based migrations with UP and DOWN sections"을 지원합니다.
5. Repository 패턴과 통합
// DAO
class TodoDao {
final Database db;
TodoDao(this.db);
Future<List<Todo>> getAll() async {
final List<Map<String, dynamic>> maps = await db.query('todos');
return List.generate(maps.length, (i) => Todo.fromMap(maps[i]));
}
Future<int> insert(Todo todo) async {
return await db.insert('todos', todo.toMap());
}
Future<int> update(Todo todo) async {
return await db.update(
'todos',
todo.toMap(),
where: 'id = ?',
whereArgs: [todo.id],
);
}
Future<int> delete(int id) async {
return await db.delete(
'todos',
where: 'id = ?',
whereArgs: [id],
);
}
}
// Repository
abstract class TodoRepository {
Future<List<Todo>> getTodos();
Future<void> addTodo(Todo todo);
Future<void> updateTodo(Todo todo);
Future<void> deleteTodo(int id);
}
class TodoRepositoryImpl implements TodoRepository {
final TodoDao _dao;
TodoRepositoryImpl(this._dao);
@override
Future<List<Todo>> getTodos() => _dao.getAll();
@override
Future<void> addTodo(Todo todo) async {
await _dao.insert(todo);
}
@override
Future<void> updateTodo(Todo todo) async {
await _dao.update(todo);
}
@override
Future<void> deleteTodo(int id) async {
await _dao.delete(id);
}
}마이그레이션 Best Practices
공통 원칙
- 버전 관리: 항상 마이그레이션을 순차적으로 적용
- 데이터 백업: 중요한 데이터는 마이그레이션 전 백업
- 테스트: 각 마이그레이션 단계를 테스트
- 롤백 계획: 문제 발생 시 대응 방안 준비
SQFlite best practices (opens in a new tab)에서 "Always add new migrations to the end of the list, never in the middle"라고 강조합니다.
Drift 권장사항
- 스키마 변경 시
drift_dev schema dump실행하여 스키마 히스토리 유지 - 자동 생성된 마이그레이션 코드 검토 후 사용
- 복잡한 마이그레이션은
customStatement활용 - 테스트 코드로 마이그레이션 검증
SQFlite 권장사항
- 마이그레이션 SQL을 별도 파일로 관리
- 버전별로 명확한 주석 작성
onUpgrade에서 모든 버전 케이스 처리- 앱 재시작 필요 (Hot Reload 작동 안함)
SQFlite migration guide (opens in a new tab)에서 "You will have to restart your app when you change your application schema; Flutter Hot-reload won't work"이라고 명시되어 있습니다.
성능 고려사항
- Drift는 약간의 ORM 오버헤드가 있지만 대부분의 앱에서 무시할 수 있는 수준입니다
- 한 개발자가 SQFlite에서 Drift로 마이그레이션 후 약 0.1초의 성능 저하를 보고했으나, 타입 안정성과 개발 생산성이 더 중요한 고려사항입니다
Performance discussion (opens in a new tab)에서 실제 마이그레이션 경험이 공유되었습니다.
언제 SQFlite를 선택할까?
다음 경우에는 SQFlite를 고려할 수 있습니다:
- 극한의 성능 최적화: 밀리초 단위 최적화가 필요한 경우
- 복잡한 SQL 쿼리: Drift가 지원하지 않는 고급 SQL 기능 사용
- 코드 생성 회피: 빌드 프로세스에 코드 생성을 추가하고 싶지 않은 경우
- 기존 SQFlite 프로젝트: 이미 잘 구축된 SQFlite 프로젝트가 있는 경우
출처
- Flutter databases overview - updated 2025 (opens in a new tab)
- Best Local Database for Flutter Apps: A Complete Guide (opens in a new tab)
- Flutter databases - Hive, ObjectBox, sqflite, Isar and Moor (opens in a new tab)
- Drift Official Documentation - Migrations (opens in a new tab)
- Drift Migration Step-by-Step Guide (opens in a new tab)
- Database migration Drift flutter dart - Medium (opens in a new tab)
- Drift Local Database For Flutter (Part 1) - Medium (opens in a new tab)
- SQFlite Migration Example - GitHub (opens in a new tab)
- sqflite_migration_plan - GitHub (opens in a new tab)
- sqflite_migrate - Pub.dev (opens in a new tab)
- Flutter: Database Migrations with sqflite_migrations - Medium (opens in a new tab)
- How database migration works with the flutter Sqflite (opens in a new tab)
- Performance question - GitHub Issue (opens in a new tab)