데이터베이스 연결 설정

Dart에서 데이터베이스를 사용하려면 먼저 데이터베이스와의 연결을 설정해야 한다. 일반적으로 Dart에서는 sqflite 패키지나 postgres 패키지와 같은 라이브러리를 사용하여 데이터베이스에 접근할 수 있다. SQLite는 로컬 데이터베이스로 많이 사용되고, PostgreSQL은 서버 기반 데이터베이스로 많이 사용된다.

SQLite 연결 예시

SQLite 데이터베이스를 사용하는 경우, 데이터베이스 파일을 지정하고 연결을 열어야 한다.

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

void main() async {
  var database = openDatabase(
    join(await getDatabasesPath(), 'my_database.db'),
    onCreate: (db, version) {
      return db.execute(
        'CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
      );
    },
    version: 1,
  );
}

여기서 openDatabase 함수는 데이터베이스 연결을 열고, 데이터베이스가 없으면 자동으로 생성한다.

데이터 삽입

데이터베이스에 데이터를 삽입하려면 INSERT 쿼리를 사용한다. Dart에서는 보통 insert 함수를 통해 데이터를 삽입한다.

Future<void> insertUser(User user) async {
  final db = await database;

  await db.insert(
    'users',
    user.toMap(),
    conflictAlgorithm: ConflictAlgorithm.replace,
  );
}

위 코드에서는 insertUser 함수를 통해 사용자의 데이터를 users 테이블에 삽입하고, 만약 같은 ID가 이미 존재한다면 덮어쓰기를 선택하는 방식이다.

데이터 검색

데이터를 검색할 때는 SELECT 쿼리를 사용한다. Dart에서 데이터 검색은 주로 query 함수를 통해 이루어진다.

Future<List<User>> users() async {
  final db = await database;
  final List<Map<String, dynamic>> maps = await db.query('users');

  return List.generate(maps.length, (i) {
    return User(
      id: maps[i]['id'],
      name: maps[i]['name'],
      age: maps[i]['age'],
    );
  });
}

위 함수는 users 테이블에서 모든 사용자의 데이터를 검색하여 User 객체 리스트로 변환한다.

데이터 업데이트

데이터베이스에 저장된 데이터를 업데이트하려면 UPDATE 쿼리를 사용한다. Dart에서는 update 함수를 통해 데이터를 업데이트할 수 있다.

Future<void> updateUser(User user) async {
  final db = await database;

  await db.update(
    'users',
    user.toMap(),
    where: 'id = ?',
    whereArgs: [user.id],
  );
}

위 코드에서는 특정 id 값을 가진 사용자의 데이터를 업데이트한다. wherewhereArgs는 업데이트할 데이터를 특정하는 데 사용된다.

데이터 삭제

데이터를 삭제하려면 DELETE 쿼리를 사용한다. Dart에서는 delete 함수를 통해 특정 데이터를 삭제할 수 있다.

Future<void> deleteUser(int id) async {
  final db = await database;

  await db.delete(
    'users',
    where: 'id = ?',
    whereArgs: [id],
  );
}

위 코드에서는 주어진 id 값을 가진 사용자의 데이터를 삭제한다.

데이터베이스 트랜잭션 관리

데이터베이스 트랜잭션은 여러 SQL 작업을 하나의 원자적 단위로 처리하여, 중간에 오류가 발생할 경우 모든 작업을 원래 상태로 롤백하는 기능을 제공한다. Dart에서 트랜잭션을 관리하려면 transaction 함수를 사용하여 여러 SQL 작업을 하나의 트랜잭션으로 묶을 수 있다.

Future<void> performTransaction() async {
  final db = await database;

  await db.transaction((txn) async {
    await txn.insert('users', {'id': 1, 'name': 'Alice', 'age': 25});
    await txn.update('users', {'name': 'Bob'}, where: 'id = ?', whereArgs: [1]);
    await txn.delete('users', where: 'id = ?', whereArgs: [1]);
  });
}

위 코드에서는 insert, update, delete 명령이 하나의 트랜잭션으로 묶여 있으며, 트랜잭션 중 하나라도 실패하면 모든 변경 사항이 롤백된다.

인덱스 최적화

데이터베이스에서 인덱스는 검색 성능을 크게 향상시키는 중요한 요소이다. 인덱스를 사용하면 테이블 내 특정 열에 대한 검색 속도를 높일 수 있다. SQLite에서는 CREATE INDEX 문을 사용하여 인덱스를 생성한다.

await db.execute('CREATE INDEX idx_user_name ON users(name)');

위 코드에서는 users 테이블의 name 열에 대한 인덱스를 생성하여, 해당 열을 검색할 때 성능을 개선한다.

데이터베이스 조인

여러 테이블에서 데이터를 동시에 가져오기 위해 JOIN을 사용할 수 있다. Dart에서는 query 함수에 JOIN 구문을 직접 사용할 수 있다.

Future<List<User>> fetchUsersWithOrders() async {
  final db = await database;

  final List<Map<String, dynamic>> result = await db.rawQuery(
    'SELECT users.name, orders.order_id FROM users '
    'INNER JOIN orders ON users.id = orders.user_id'
  );

  // 결과를 변환하여 사용
  return List.generate(result.length, (i) {
    return User(
      name: result[i]['name'],
      orderId: result[i]['order_id'],
    );
  });
}

이 코드는 users 테이블과 orders 테이블을 INNER JOIN으로 연결하여 두 테이블에서 데이터를 동시에 가져온다.

데이터베이스 최적화 기법

데이터베이스 최적화는 쿼리 성능을 높이는 중요한 기술이다. 주요 최적화 방법은 다음과 같다:

  1. 쿼리 최소화: 여러 데이터를 한 번에 가져올 수 있도록 쿼리를 줄이다. 예를 들어, 중복된 쿼리를 여러 번 호출하는 대신 한 번에 데이터를 가져온다.
  2. 인덱스 사용: 자주 검색되는 열에 인덱스를 추가하여 검색 속도를 향상시킨다.
  3. 캐시 활용: 데이터를 메모리에 캐싱하여, 데이터베이스에 대한 과도한 요청을 줄이다.

데이터베이스 보안

데이터베이스 보안은 데이터 무결성과 기밀성을 유지하는 데 중요한 요소이다. Dart 애플리케이션에서 데이터베이스 보안을 강화하기 위한 몇 가지 방법은 다음과 같다:

  1. SQL 인젝션 방지: 사용자 입력을 직접 SQL 쿼리에 포함하지 않고, whereArgs 매개변수를 활용하여 SQL 인젝션 공격을 방지한다.

dart await db.rawQuery('SELECT * FROM users WHERE name = ?', [userInput]);

  1. 암호화: 민감한 정보를 저장할 때 데이터베이스에 저장되기 전에 데이터를 암호화한다. Dart에서 encrypt 패키지를 사용하여 데이터를 암호화할 수 있다.

  2. 접근 제어: 데이터베이스 사용자 계정을 생성하고, 최소 권한 원칙을 적용하여 사용자에게 필요한 권한만 부여한다.

데이터베이스 백업 및 복구

데이터베이스의 데이터 손실을 방지하기 위해 정기적인 백업 및 복구 전략이 필요하다. Dart에서는 파일 시스템 API를 사용하여 데이터베이스 파일을 복사하여 백업할 수 있다.

import 'dart:io';

Future<void> backupDatabase() async {
  final dbPath = await getDatabasesPath();
  final databaseFile = File('$dbPath/my_database.db');
  final backupFile = File('$dbPath/my_database_backup.db');

  await databaseFile.copy(backupFile.path);
}

이 코드는 현재 데이터베이스 파일을 복사하여 백업을 생성하는 간단한 예시이다.

데이터베이스 성능 모니터링

성능 모니터링은 데이터베이스의 건강 상태를 유지하고 성능 문제를 미리 발견하는 데 중요하다. Dart에서는 성능을 모니터링하기 위해 몇 가지 방법을 사용할 수 있다:

  1. 쿼리 로그: 데이터베이스에서 실행된 쿼리 로그를 기록하여 성능이 낮은 쿼리를 식별한다.
  2. 데이터베이스 상태 확인: 특정 주기로 데이터베이스의 성능 지표를 확인하고, 이상 징후를 감지하여 사전 예방적으로 조치를 취한다.

예제 코드 통합

데이터베이스 쿼리 및 관리의 여러 기능을 통합한 전체적인 예제 코드는 다음과 같다.

import 'package:sqflite/sqflite.dart';
import 'package:path/path.dart';

class User {
  final int id;
  final String name;
  final int age;

  User({required this.id, required this.name, required this.age});

  Map<String, dynamic> toMap() {
    return {
      'id': id,
      'name': name,
      'age': age,
    };
  }
}

class DatabaseHelper {
  static final DatabaseHelper _instance = DatabaseHelper._internal();
  static Database? _database;

  factory DatabaseHelper() {
    return _instance;
  }

  DatabaseHelper._internal();

  Future<Database> get database async {
    if (_database != null) return _database!;
    _database = await _initDatabase();
    return _database!;
  }

  Future<Database> _initDatabase() async {
    return await openDatabase(
      join(await getDatabasesPath(), 'my_database.db'),
      onCreate: (db, version) {
        return db.execute(
          'CREATE TABLE users(id INTEGER PRIMARY KEY, name TEXT, age INTEGER)',
        );
      },
      version: 1,
    );
  }

  Future<void> insertUser(User user) async {
    final db = await database;
    await db.insert(
      'users',
      user.toMap(),
      conflictAlgorithm: ConflictAlgorithm.replace,
    );
  }

  Future<List<User>> fetchUsers() async {
    final db = await database;
    final List<Map<String, dynamic>> maps = await db.query('users');

    return List.generate(maps.length, (i) {
      return User(
        id: maps[i]['id'],
        name: maps[i]['name'],
        age: maps[i]['age'],
      );
    });
  }
}

이 코드에서는 User 클래스를 통해 사용자의 정보를 저장하고, DatabaseHelper 클래스를 통해 데이터베이스와의 상호작용을 관리한다. 삽입, 조회 등의 기능이 포함되어 있다.