Flutter Drift로 SQLite 데이터베이스 생성부터 관리까지 5단계

자신이 개발한 앱의 활용성을 높이기 위해서는 사용자의 데이터를 관리해야 합니다. 보통 앱이 실행될 때 입력값을 변수로 관리하다가 앱이 종료되면서 그 값을 잃어버리게 되는데 데이터베이스를 이용하게 되면 사용자의 다양한 활동을 저장하여 기억할 수 있기 때문에 데이터베이스를 이용하면 더욱 활용성이 높은 어플리케이션을 만들 수 있게 됩니다. 이 글에서는 drift로 SQLite 데이터베이스 생성부터 관리까지 방법에 대해서 알아보도록 하겠습니다.

이 글에서 소개하려는 라이브러리 패키지는 drift라는 패키지입니다. 우선 이 패키지를 사용하는 가장 큰 목적은 범용성입니다. Android, iOS 뿐만 아니라 Desktop 및 제한적으로 Web에서도 사용이 가능하다고 명시되어 있습니다. 현재 개발하는 프로그램이 데스크탑 기반의 프로그램이기 때문에 drift를 대체할 패키지가 없었습니다.

두번째로 Flutter Favorite 마크를 획득한 패키지라는 점입니다. Flutter 개발팀은 모든 라이브러리를 자체 개발하지 않고 외부 개발자가 개발한 패키지 중 잘 만들어진 패키지에 ‘Flutter Favorite’라는 마크를 달아줍니다. 이 마크가 있으면 구글에서도 인정한 패키지구나 하고 일단 믿고 사용할 수 있습니다.

Drift 패키지를 이용하면 SQLite를 이용하는데 SQL 쿼리는 전혀 사용할 필요가 없습니다. 물론 SQL에 대한 기본지식을 갖는 것은 중요합니다. 하지만 Drift 자체가 데이터베이스의 구조와 객체를 연결(매핑)해주는 기술인 객체-관계 매핑(ORM – Object Relational Mapping)을 지원하기 때문에 테이블을 클래스로 표현하고 쿼리를 Dart 언어로 작성하여 프로그램의 유지보수를 더욱 용이하게 만들어줍니다.

Drift로 SQLite 데이터베이스 생성부터 관리까지

자신의 프로젝트에 Drift 패키지를 이식하는 과정은 다음과 같습니다.

  • 테이블 정의하기
  • 데이터베이스 생성하기 app_db.dart
    • 데이터베이스 관리 파일 생성 app_db.g.dart
  • CRUD 구성하기
  • 쿼리 결과 데이터를 List로 변환 및 사용
  • 복잡한 구조의 SQL 구현하기(JOIN, WHERE, ORDER BY 등)

데이터베이스 새로 생성을 하던지 아니면 외부로부터 가져오던지 간에 데이터베이스를 객체로써 접근할 수 있어야 합니다. 데이터베이스를 새롭게 생성하는 방법과 더불어 외부의 데이터베이스를 가져오는 방법에 대해서도 함께 설명하도록 하겠습니다.

데이터베이스를 생성하는 과정중에서 테이블 구조와 데이터베이스를 연결하는 app_db.g.dart 파일을 자동으로 생성하게 됩니다. 이 파일에는 기본적으로 데이터베이스를 관리하기 위한 다양한 형태의 메서드가 정의됩니다.

우리가 만들 어플은?

학생목록을 데이터베이스로 관리하는 어플을 예시로 만들어 보도록 하겠습니다. 기본적으로 메인페이지에서는 학생 목록을 볼 수 있으며, 학생을 추가할 수 있는 페이지와 학생 정보를 수정할 수 있는 페이지의 두 개의 추가 페이지로 구성되어 있습니다. 메인페이지에서 학생목록에서 삭제를 할 수 있는 기능도 함께 제공합니다.

데이터베이스는 학생정보, 반정보, 학생-반 매칭정보를 저장한 세 개의 테이블로 구성됩니다. 테이블을 세 개로 구성하는 이유는 향후 유지보수의 용이성 때문입니다. 학년이 올라가게 되어 새로운 반이 구성이 된다면 기존의 내용을 업데이트 해 주어야 합니다. 이 때 테이블이 나눠져 있다면 학생 – 반 매칭정보만 수정함으로써 내용을 업데이트 할 수 있게 됩니다. 훨씬 적은 노력으로 원하는 결과를 가져올 수 있습니다. 하나의 테이블로부터 데이터를 읽는 것이 아니라 고수준의 데이터베이스 관리를 위한 JOIN, ORDER BY, WHERE 기능을 추가한 앱을 만들어 보도록 하겠습니다.

모델 구현하기

Drift에서 이야기하는 모델을 구현한다는 것은 데이터베이스의 테이블을 생성하는 것과 동일하게 생각하시면 됩니다. SQL로 구현되는 내용이 어떻게 Drift의 모델로 정의되는지 비교해 보도록 하겠습니다. 먼저 저희가 만들 학생 정보 모델 정보는 다음과 같습니다.

파일경로 : lib/data/local/entity/student_infos.dart

import 'package:drift/drift.dart';

class StudentInfos extends Table {
  @override
  // 1
  String get tableName => 'student_infos';
  // 2
  IntColumn get id => integer().autoIncrement()();
  // 3
  TextColumn get studentId => text().named('student_id')();
  TextColumn get name => text()();
  // 4
  TextColumn get gender => text().nullable();
}
  1. 이후에 코드 자동생성 기능을 이용해서 모델을 이용할 수 있도록 코드가 생성되는데 이 때 테이블 이름이나 객체 데이터 등에 대해서 자동으로 정의가 됩니다. 클래스명은 기본적으로 camel case로 작성하며 테이블이름은 자동으로 snake case로 변경됩니다. 만약 다른 이름으로 작성하고 싶다면 다음과 같이 별도로 지정할 수 있습니다.
  2. 기본적으로 게터를 이용하여 테이블의 열을 정의합니다. 가장 처음에는 각 열의 타입을 지정합니다. 대표적으로 IntColumn, TextColumn, DateTimeColumn 등이 사용됩니다.
  3. 각 열의 속성정보는 함수를 이어서 작성하면 됩니다. 모델의 열의 이름은 camel case로 작성되며, 이후 코드가 생성되면 이 역시 snake case로 변환되어 활용하게 됩니다. 다른 이름으로 사용하고 싶을 경우에 named 함수를 사용합니다.
  4. 기본적으로 열을 정의하면 null을 허용하지 않습니다. NOT NULL 속성을 부여하기 위해서는 nullable 함수를 정의해줍니다.

이를 SQL을 이용하여 테이블 생성하는 코드와 비교하면 다음과 같습니다. 참고용으로 알아봅시다.

CREATE TABLE student_infos {
  id INTEGER PRIMARY KEY AUTOINCREMENT, 
  student_id VARCHAR NOT NULL, 
  name VARCHAR NOT NULL, 
  gender VARCHAR
}

데이터베이스 관리 도구를 이식하기 전 코드입니다. 현재는 List 형식의 더미 데이터가 들어있으며, 향후 데이터베이스를 통해서 조회한 데이터를 기반으로 대체할 예정입니다. 오른쪽 하단의 Floating Action Button을 클릭하면 학생등록 페이지로 이동하며, 목록의 학생목록을 클릭하면 학생정보 수정 페이지로 이동합니다. 삭제기능은 이후 데이터베이스 적용 후 별도로 구현해보도록 하겠습니다.

같은 위치에 반정보를 담을 모델과 학생과 반 매칭정보를 담을 모델을 각각 만들어줍니다. 이렇게 3개의 테이블을 구분해서 만들어주는 이유는 코드의 유연성을 높이고 데이터베이스의 효율적 관리를 위함입니다. 매년 학년이 올라가면서 학생의 반 정보가 바뀌게 되는데 이 때 데이터베이스 업데이트를 통해서 반 정보를 변경하게되면 학생의 이전 기록이 남아있지 않게 됩니다. 이렇게 테이블을 분리하여 관리하면 학생의 과거 반 정보 변경이력을 모두 확인할 수 있다는 장점이 생깁니다.

파일경로 : lib/local/entity/system_codes.dart

import 'package:drift/drift.dart';

class SystemCodes extends Table {
  @override
  String get tableName => 'system_codes';
  IntColumn get id => integer().autoIncrement()();
  TextColumn get type => text()();
  TextColumn get code => text()();
  TextColumn get codeName => text()();
}

반 정보는 거의 변하지 않는 정보입니다. 그래서 시스템 코드로 분류하여 기타 다른 정보와 함께 저장합니다.

파일경로 : lib/local/entity/student_class_infos.dart

import 'package:drift/drift.dart';

class StudentClassInfos extends Table {
  @override
  String get tableName => 'student_class_infos';
  IntColumn get id => integer().autoIncrement()();
  IntColumn get year => integer()();
  TextColumn get studentId => text()();
  TextColumn get classId => text()();
  TextColumn get validYn => text()();
  DateTimeColumn get logDate => dateTime()();
}

학생-반 정보 매칭정보는 student_class_infos 테이블에 저장됩니다. validYn 값을 저장하여 현재 데이터와 과거 데이터를 분리합니다.

데이터베이스 파일 생성하기

앞선 모델은 단순히 테이블의 구조만 정의한 클래스이며, 앞으로 이를 기반으로 데이터를 관리할 수 있도록 테이블 관리 코드라던지 데이터베이스 생성 등을 위한 코드를 생성하도록 하겠습니다. app_db.dart 라는 파일에 한번에 구현됩니다.

테이블 관리 코드

사실 Drift의 가장 큰 장점은 사용자가 테이블 관리에 필요한 코드를 일일이 작성할 필요가 없다는데 있습니다. 코드 자동생성 기능을 이용해서 g.dart 파일을 생성하게 되면 테이블 관리에 필요한 모든 코드가 생성이 됩니다. 아래의 코드를 살펴보겠습니다.

파일경로 : lib/data/local/db/app_db.dart

import 'dart:io';

import 'package:drift/drift.dart';
import '../entity/student_class_infos.dart';
import '../entity/student_infos.dart';
import '../entity/system_codes.dart';

part 'app_db.g.dart';

LazyDatabase _openConnection() {
  return LazyDatabase(() async {
    // 1
    final dbFolder = await getApplicationDocumentsDirectory();
    // 2
    final file = File(p.join(dbFolder.path, 'srn_db_imported_2.sqlite'));

    // 3
    if (!await file.exists()) {
      // Extract the pre-populated database file from assets
      final blob = await rootBundle.load('assets/srn_db_2.sqlite');
      final buffer = blob.buffer;
      await file.writeAsBytes(
          buffer.asUint8List(blob.offsetInBytes, blob.lengthInBytes));
    }
    // 4
    return NativeDatabase.createInBackground(file);
  });
}

@DriftDatabase(tables: [
  StudentInfos,
  StudentClassInfos,
  SystemCodes,
])
class AppDb extends _$AppDb {}

LazyDatabase정의

  1. LazyDatabase에서는 db파일이 저장될 폴더 위치를 지정해줍니다. dbFolder라는 변수 이름으로 지정해주며 이는 getApplicationDocumentsDirectory()라는 함수를 통해 경로를 설정합니다. 이 함수는 서로 플랫폼이 달라지더라도 시스템 폴더의 위치를 기억하고 있어 범용적으로 폴더 위치를 사용할 수 있도록 도와줍니다.
  2. File 클래스를 이용하여 지정한 경로상에 파일을 생성합니다.
  3. 이 부분은 기존에 있던 데이터베이스의 구조 및 정보를 가져올 때 사용됩니다. 예시 코드에서는 assets 폴더에 srn_db_2.sqlite 이름의 데이터베이스를 임포트합니다. 코드를 가져올 때 pubspec.yaml 파일에서 다음과 같이 추가해 주어야 합니다.
  4. 리턴타입은 NativeDatabase 형태로 주어지는데 NativeDatabase 클래스는 File 형태의 인자(file)를 전달받으며, 실행 결과를 file에 저장하는 데이터베이스를 생성합니다. 메소드 중 createInBackground를 부여할 수 있습니다. 기본적으로 NativeDatabase와 동일한 기능을 제공하지만 한가지 차이점은 데이터베이스 생성 시 백그라운드에서 별개로 실행된다는 점에 차이가 있습니다.

파일경로 : pubspec.yaml

assets:
    - assets/srn_db_2.sqlite

dart.io File Class 자세히 보기

class AppDb

  1. Drift 관련한 쿼리작성 및 데이터베이스 관리를 위한 클래스를 정의합니다. 클래스의 이름은 자신이 원하는 이름으로 설정하시면 됩니다. 나중에 코드 자동생성을 하게 되면 앞에 _$이 추가된 이름의 클래스가 생성이 되고 해당 클래스를 상속받는 형태로 코드를 작성해주시면 됩니다.
  2. @DrfitDatabase에는 앞서 정의한 테이블들을 목록으로 작성해 주시면됩니다.

g.dart 코드 자동생성하기

이제 기본적인 준비는 끝이 났습니다. 데이터베이스에 대한 기본적인 뼈대를 잡았으니 이제 drift의 코드 자동생성 기능을 사용해 보도록 하겠습니다. 터미널에서 해당 프로젝트 폴더로 이동한 뒤 아래의 명령어를 입력합니다.

flutter pub run build_runner build

이 때 주의해야 할 사항은 앞서 작성한 app_db.dart 파일 안에 part 'app_db.g.dart'; 구문이 들어가 있어야 한다는 것입니다. 자동 코드 생성을 실행하게 되면 app_db.dart 파일과 동일한 경로에 app_db.g.dart 파일이 생성됩니다. 이 파일 안에서 앞서 설명한 _$AppDb 클래스도 확인하실 수 있습니다. 이로써 Drift를 본격적으로 사용하기 위한 준비를 마쳤습니다. 다음에는 테이블 구조에 따라서 직접 쿼리를 작성해 보도록 하겠습니다.

Leave a Comment