본문 바로가기
공부/프로그래밍

Flutter 앱 개발 시리즈: SQLite 데이터베이스 다루기 + 버전관리

by demonic_ 2024. 11. 28.
반응형

모바일 앱 개발에서 로컬데이터베이스 활용을 하려는데 SQLite를 쓰기로 했다. 항상 서버사이드에서 다루다가 앱에서 다루려니까 미숙한 부분이 있는데, 테이블을 직접 만들어줘야 하고, 그 DDL을 앱 설정에 포함되어야 한다는 점이다.

 

우선 SQLite를 쓰면서 순서대로 살펴보도록 하자.

 

 

SQLite 테이블 설계시 주의사항

SQLite는 다른 RDBMS와 달리 동적 타입 시스템을 사용한다. 이게 어떤의미냐면 VARCHAR나 INT를 지정할떄 자릿수 등을 명확히 정해야하는 그런 것을 하지 않아도 된다는 점이다.

 

그로인해 유연한 데이터 저장은 가능하겠지만, 단점은 타입 불일치로 인한 버그 발생 가능성이 높아진다.

 

또한 서버사이드에서 다루는 데이터베이스인 Mysql이나 PostgreSQL등은 테이블을 미리 만들어두고 확인해가며 볼 수 있는데 반해 여기서는 바로 눈으로 확인할 수 없고 DB파일을 다운로드 받아 다른 툴로 확인해야 하는 번거로움이 있다.

 

아래는 이번에 만든 테이블 중 하나다.

CREATE TABLE my_option (
  id INTEGER PRIMARY KEY,
  time_term REAL,        // 실수값 저장
  is_show_clock INTEGER, // Boolean 대신 INTEGER
  is_music_play INTEGER
)

 

 

 

처음에는 Boolean 이 필요해서 Boolean 타입을 지정했는데, SQLite 에서는 Boolean 타입을 지원하지 않아 Integer로 대신했다.

  • INTEGER를 사용하여 0(false), 1(true)로 저장

 

그리고 실수값을 지정하는데, Double을 저장하기 위해 Real 을 쓰기로 했다.

 

만약 테이블을 잘못 만들었다면, 다음과 같은 쿼리를 넣어 테이블을 삭제하고 다시 생성하곤 했다.

Future<void> dropTable(String tableName) async {
  try {
    final db = await DatabaseHelper.instance.database;
    await db.execute('DROP TABLE IF EXISTS $tableName');
    print('$tableName 테이블이 삭제되었습니다.');
  } catch (e) {
    print('테이블 삭제 중 에러 발생: $e');
  }
}

 

 

 

버전관리

개발과정에서 테이블이 자주 변경될 수 있는데, 처음에는 이걸 어떻게 관리해야 할지 몰라 막막했었다. 그러다가 openDatabase 함수에 버전과 onUpdate 를 활용해서 변경하는걸 알게되었다. 그리고 데이터베이스도 Create 가 아니라 CREATE TABLE IF NOT EXISTS 써야했다. 그러지 않으면 이미 테이블이 있어 에러가 나곤 했다.

 

그리고 어떤 이유로 인해 테이블이나 컬럼이 바뀌면 에러가 날 수 있었다. 예를들어

  1. 새로운 컬럼 추가되거나 기존 컬럼 타입 변경 시
  2. 새로운 테이블 추가

 

이렇게 되었을때, 없는 테이블을 참고하거나, 컬럼이 없거나 다른 데이터타입이라 불러오는데 에러가 발생하는 등이 그것이다.

 

변경이 된다면 기존 사용자의 앱에서 안전하게 적용되어야 한다. 그러기 위해서는 버전별로 어떻게 변경되었고, 무엇이 추가되었는지를 코드로 관리되어야 했다.

 

항상 백엔드에서 데이터베이스를 관리했기 때문에 버전별 관리에 대해 좀 소홀했었는데, 이번에 앱을 개발하게 되면서 데이터베이스 관리가 얼마나 중요한지 새삼 깨닫게 되었다. 처음 SQLite를 쓸때는 이런 생각이 없었다.

 

아래는 SQLite를 처음 불러올때의 코드이다. 버전과 onUpgrade 를 추가했다.

class DatabaseHelper {
  static final DatabaseHelper instance = DatabaseHelper._init();
  
  Future<Database> _initDB() async {
    String path = join(await getDatabasesPath(), 'myapp.db');
    return await openDatabase(
      path,
      version: 1,
      onCreate: _onCreate,
      onUpgrade: _onUpgrade,
    );
  }
}

 

Upgrade에는 어떤 항목이 변하는지를 등록해줘야 한다.

Future<void> _onUpgrade(Database db, int oldVersion, int newVersion) async {
  if (oldVersion < newVersion) {
    // 버전 1에서 2로 업그레이드
    await db.execute('''
      ALTER TABLE my_option 
      ADD COLUMN is_show_clock INTEGER DEFAULT 0
    ''');
  }
}

 

 

별 대수롭지 않게 업그레이드 메서드를 만들었는데, 나중에 코드를 복기해보니 여기에는 치명적인 문제가 있었다. 바로 버전을 몇단계 건너띄게 되었을때, 히스토리가 없다는 점이다. 즉, 버전별로 단계별 업그레이드가 되어야 테이블이 깨지지 않는데, 1 -> n단계로 한번에 가는 경우 중간에 추가된 특정 컬럼이나 테이블이 존재하지 않을 수 있겠단 생각이 들었다.

 

버전별로 어떻게 변경되었는지, 무엇이 추가되었는지 등 명확하게 명시되어야 한다. 그래서 이 부분을 변경하기로 했다

Future<void> dbUpdate(Database db, int oldVersion) async {
  if (oldVersion < 2) {
    await db.execute('''
                 CREATE TABLE IF NOT EXISTS ${tableMyAlbum} (
                   id INTEGER PRIMARY KEY AUTOINCREMENT,
                   album_id TEXT NOT NULL
                 )
               ''');
  }
  if (oldVersion < 3) {
    await db.execute('''
                 CREATE TABLE ${tableMyOption} (
                   id INTEGER PRIMARY KEY,
                   time_term REAL, 
                   is_show_clock INTEGER, 
                   is_music_play INTEGER
                 )
              ''');
  }
}

 

가능하면 다음과 같이 내용도 추가하면 좋을듯 하다

/*
 * Database Version History
 * v1: 기본 테이블 생성
 * v2: is_show_clock 컬럼 추가
 * v3: is_music_play 컬럼 추가
 */

 

 

어쨋든 테이블이 잘못 설정된다면 앱을 지웠다 다시 설치해야 할 수 있기 때문에 이부분은 조심히 다뤄야 할거 같다.

 

 

여담이지만 사실 SQLite 외에 데이터베이스 종류가 더 있다.

  • SharedPreferences
  • Hive
  • SQLite
  • Secure Storage

 

이중 관계형을 쓰려면 SQLite 가 좋다고 하는데, 딱히 관계형을 위해 썼다기 보다는 SharedPreferences와

Secure Storage는 목록 저장이 잘 될거같지 않아서 SQLite를 쓰게 되었다. 그리고 가능하면 복잡한 SQL은 쓰지않고 간단한 것으로 관리하는게 좋다는 생각이 들었다.

 

 

SQLite 내용확인 및 DB 파일 찾기

 

참고로 내부 SQLite 의 정보를 확인하려면 파일을 다운받은 다음에 DB Browser for SQLite 등으로 확인할 수 있다. 파일은 운영체제에 따라 다르며, 다음의 정보로 확인할 수 있다

 

Flutter 로 데이터베이스 경로 확인

 

void printDbPath() async {
  String dbPath = await getDatabasesPath();
  print('데이터베이스 경로: $dbPath');
}

 

 

OS의 경우

  • Finder 에서 CMD + Shift + G 입력
  • 다음 경로 입력
~/Library/Developer/CoreSimulator/Devices
  • 디바이스 ID 폴더 찾기(최근 날짜 확인)
  • data/Containers/Data/Application/[앱ID]/Documents 경로로 이동
  • .db 파일 찾기

 

Android의 경우

  • 안드로이드 스튜디오에서 View > Tool Windows > Device File Explorer 열기
  • data/data/[패키지명]/databases 경로로 이동
  • DB 파일 우클릭 후 'Save As' 선택

 

 

 

 

반응형

댓글