본문 바로가기

학습 기록 (Learning Logs)/CS Study

SQLite

 

 


공통 질문

 

 


SQLite

파일 기반의 경량 관계형 데이터베이스 관리 시스템(RDBMS)

자체적으로 실행 가능한 "임베디드(Embedded) 데이터베이스"**입니다.

서버가 필요 없으며, 애플리케이션에 직접 통합할 수 있습니다.


특징

1. 파일 기반 데이터베이스

  • 데이터는 하나의 단일 파일(.sqlite, .db 등)에 저장됨.
  • 파일 이동만으로도 데이터베이스 백업 및 복사가 가능함.

2. 서버가 필요 없는 임베디드 데이터베이스

  • PostgreSQL, MySQL과 달리 클라이언트-서버 아키텍처가 아님.
  • 애플리케이션 내부에서 직접 데이터베이스를 실행할 수 있음.
  • 별도의 설정 없이 실행 가능(Zero Configuration).

3. 경량(Lightweight)

  • 바이너리 크기가 매우 작음 (약 500KB ~ 1MB)
  • 메모리 사용량이 적음 → 모바일 앱, IoT 기기, 브라우저 등에 적합.

4. ACID(원자성, 일관성, 독립성, 지속성) 지원

  • 트랜잭션을 안전하게 처리하여 데이터 무결성을 유지.
  • WAL(Write-Ahead Logging) 모드를 사용하면 고속 동시 읽기/쓰기 가능.

5. 크로스 플랫폼 지원

  • Windows, macOS, Linux, Android, iOS 등 거의 모든 플랫폼에서 동작 가능.
  • 데이터베이스 파일을 운영체제 간 쉽게 이동 가능.

6. SQL 표준 지원

  • ANSI SQL-92를 대부분 지원.
  • JOIN, 트랜잭션, 인덱스, 뷰, 트리거, 서브쿼리 등 지원.

 


장점

설치 및 설정이 필요 없음

  • SQLite는 실행 파일(sqlite3)만 있으면 바로 사용 가능.

빠른 속도 (단일 사용자 환경)

  • 읽기 성능이 매우 빠름 (전체 데이터가 파일로 관리됨).
  • 작은 데이터베이스에서 MySQL, PostgreSQL보다 성능이 더 좋을 수 있음.

트랜잭션 지원

  • BEGIN TRANSACTION, COMMIT, ROLLBACK 지원.

모바일과 임베디드 환경에 최적화

  • Android, iOS, IoT, 브라우저(LocalStorage)에서 자주 사용됨.

하나의 파일로 데이터베이스를 관리

  • .db, .sqlite 같은 파일 하나에 데이터베이스 저장.
  • 파일을 직접 복사하거나 전송 가능.

 

 


단점

 

 

 

SQLite에서 읽기/쓰기 동시 수행이 불가능함

  • SQLite의 기본 설정(PRAGMA journal_mode=DELETE)에서는 읽기와 쓰기를 동시에 수행할 수 없음.
    • 하나의 트랜잭션이 쓰기(UPDATE, INSERT, DELETE)를 실행하면, 다른 모든 SELECT 작업이 차단됨.
    • 따라서 웹 애플리케이션처럼 여러 클라이언트가 동시에 접근하는 환경에서는 성능이 저하될 수 있음.
  • 해결방법: WAL 사용
    • WAL(Write-Ahead Logging) 모드를 활성화하면 읽기/쓰기 동시 수행이 가능.
    • WAL 모드를 사용하면 쓰기 작업이 wal 파일에 먼저 기록되며, 이를 통해 읽기 작업과 쓰기 작업을 동시에 수행할 수 있음.

 

 

  • 읽기 작업 (SELECT)
    • 여러 개의 읽기(SELECT) 작업은 동시에 실행 가능.
    • 하지만 기본적으로 읽기 중에 쓰기(INSERT, UPDATE, DELETE)가 들어오면 충돌(lock) 발생.
  • 쓰기 작업 (INSERT, UPDATE, DELETE)
    • SQLite에서는 한 번에 하나의 쓰기 작업만 허용.
    • 쓰기 작업이 진행 중이면 다른 쓰기 작업은 대기(blocking) 또는 실패(locked error)
  • 읽기 + 쓰기 동시 작업
    • 기본적으로 읽기 중에도 쓰기 작업이 불가능 (읽기 중에는 데이터베이스를 "SHARED LOCK" 상태로 유지).
    • 쓰기 작업이 시작되면, 모든 읽기 작업이 차단됨.

 

동시 쓰기 성능이 낮음 (멀티 유저 환경 비효율적)

  • SQLite는 하나의 프로세스만 동시에 쓰기 가능.
  • 여러 사용자가 동시 접속하는 환경(예: 웹 애플리케이션)에서는 성능이 떨어짐.
  • WAL(Write-Ahead Logging) 모드를 사용하면 동시 읽기는 가능하지만, 쓰기 동시성은 MySQL/PostgreSQL보다 낮음.

대규모 데이터베이스 지원 부족

  • GB 단위 이상의 대용량 데이터에는 적합하지 않음.
  • 데이터가 많아질수록 성능 저하 발생 가능.

고급 기능 부족

  • Stored Procedure(저장 프로시저) 미지원.
  • 권한 관리 기능이 없음 (사용자 계정 개념이 없음).
  • 네트워크 액세스 기능 없음 (클라이언트-서버 모델이 아님).

비관적락, 낙관적락 지원 안함


비관적 락(Pessimistic Lock)이란?

 

  • 트랜잭션이 특정 행(Row)을 수정하는 동안 다른 트랜잭션이 동일한 행을 읽거나 변경하지 못하도록 차단하는 방식.
  • SELECT ... FOR UPDATE 같은 명령어로 구현됨.

 

SQLite는 FOR UPDATE를 지원하지 않음.
비관적 락을 사용할 수 없기 때문에, 같은 데이터를 여러 트랜잭션이 동시에 수정하는 경우 데이터 충돌이 발생할 수 있음.

대체 방법: 트랜잭션을 활용한 Locking

SQLite에서는 비관적 락을 사용할 수 없으므로, 트랜잭션을 활용하여 동시성을 최소한으로 제어할 수 있음.

await this.repository.manager.transaction(async (entityManager) => {
  const group = await entityManager.findOne(GroupDto, { where: { id: 1 } });

  if (!group) throw new Error("Group not found");

  // 데이터 업데이트 (다른 트랜잭션이 이 데이터를 변경하기 전에 수행)
  group.name = "Updated Group";
  await entityManager.save(group);
});

트랜잭션 내에서 데이터를 읽고 즉시 업데이트하면 어느 정도의 동시성 충돌을 줄일 수 있음.
❌ 하지만, 다른 트랜잭션이 동시에 실행될 경우 완전히 차단되지 않음(비관적 락과 동일한 효과를 내지 못함).

 


낙관적 락(Optimistic Lock)이란?

 

  • 트랜잭션을 수행하는 동안 다른 트랜잭션이 데이터를 변경할 가능성을 허용하고, 최종적으로 충돌을 감지하는 방식.
  • 버전 필드(version), 수정 시간(updatedAt)을 비교하여 충돌을 감지.

SQLite는 낙관적 락을 직접 지원하지 않지만, 타임스탬프 필드(updatedAt) 또는 버전 필드(version)를 활용하여 수동으로 구현 가능.

 

✅ updatedAt을 사용한 낙관적 락

async function updateGroup(groupId: number, newName: string) {
  return await this.repository.manager.transaction(async (entityManager) => {
    const group = await entityManager.findOne(GroupDto, {
      where: { id: groupId },
    });

    if (!group) throw new Error("Group not found");

    // 기존 데이터의 updatedAt 필드를 가져옴
    const previousUpdatedAt = group.updatedAt;

    // 업데이트 수행
    const result = await entityManager.update(
      GroupDto,
      { id: groupId, updatedAt: previousUpdatedAt }, // 이전 상태를 조건으로 포함
      { name: newName, updatedAt: new Date() }
    );

    if (result.affected === 0) {
      throw new Error("Data conflict detected! Please retry.");
    }

    return result;
  });
}

두 개의 트랜잭션이 같은 데이터를 수정하려고 하면, updatedAt 값이 변경된 경우 충돌을 감지할 수 있음.
❌ 하지만, MySQL의 ON UPDATE 또는 PostgreSQL의 version 필드처럼 자동화된 지원은 없음.


lock 경험

// 그룹 순서 변경
  async changeGroupOrder(
    groupDtoCurrent: GroupDto,
    groupBefore: GroupDto | null,
    groupNext: GroupDto | null,
  ): Promise<string> {
    // 현재 그룹을 업데이트
    await this.repository.save({
      ...groupDtoCurrent,
      beforeGroupId: groupBefore?.id ?? null,
      nextGroupId: groupNext?.id ?? null,
    });

    // 다음 그룹이 존재하면 업데이트
    if (groupNext) {
      await this.repository.save({
        ...groupNext,
        beforeGroupId: groupDtoCurrent.id,
      });
    }

    // 이전 그룹이 존재하면 업데이트
    if (groupBefore) {
      await this.repository.save({
        ...groupBefore,
        nextGroupId: groupDtoCurrent.id,
      });
    }

    return groupDtoCurrent.id;
  }

 

문제점

  • 트랜잭션을 걸지 않으면 데이터 정합성이 깨질 수 있음.
  • 기본적으로 쓰기 작업이 하나만 가능 → save()를 여러 번 호출하면 database is locked 오류 발생 가능.
  • 해결 방법: WAL 모드 사용 + 트랜잭션 적용.

 

TypeORM에서 트랜잭션 적용

데이터 정합성을 보장하려면 트랜잭션을 사용하여 save()를 하나의 원자적 작업으로 묶어야 함.

async changeGroupOrder(
  groupDtoCurrent: GroupDto,
  groupBefore: GroupDto | null,
  groupNext: GroupDto | null,
): Promise<string> {
  return await this.repository.manager.transaction(async (entityManager) => {
    // 현재 그룹 업데이트
    await entityManager.save(GroupDto, {
      ...groupDtoCurrent,
      beforeGroupId: groupBefore?.id ?? null,
      nextGroupId: groupNext?.id ?? null,
    });

    // 다음 그룹 업데이트
    if (groupNext) {
      await entityManager.save(GroupDto, {
        ...groupNext,
        beforeGroupId: groupDtoCurrent.id,
      });
    }

    // 이전 그룹 업데이트
    if (groupBefore) {
      await entityManager.save(GroupDto, {
        ...groupBefore,
        nextGroupId: groupDtoCurrent.id,
      });
    }

    return groupDtoCurrent.id;
  });
}
 

WAL 모드의 장점과 단점

✅ WAL 모드의 장점

읽기/쓰기 동시 가능

SELECT(읽기) 중에도 INSERT, UPDATE, DELETE(쓰기)가 가능.

 

쓰기 속도 향상

데이터가 직접 데이터베이스 파일에 기록되지 않고, wal 로그 파일에 저장되므로 I/O 성능 향상.

 

Crash Recovery 성능 향상

장애 발생 시 wal 파일을 이용하여 더 빠르게 복구 가능.

❌ WAL 모드의 단점

동시에 여러 개의 쓰기는 여전히 불가능

한 번에 하나의 쓰기 작업만 가능 (다른 DBMS처럼 다중 트랜잭션 쓰기 지원 X).


DELETE, UPDATE가 많은 경우 성능 저하 가능

wal 파일이 일정 크기로 커지면 checkpoint를 실행하여 데이터 파일로 병합해야 함.

PRAGMA wal_checkpoint(TRUNCATE); 명령을 주기적으로 실행하여 최적화 필요.

 

 


SQLite vs MySQL vs PostgreSQL 비교

 

 

 


SQLite가 사용되는 분야

SQLite는 주로 소규모 데이터베이스에 적합하며, 다음과 같은 분야에서 많이 사용됩니다.

📌 1. 모바일 애플리케이션

  • Android → Android 앱 개발 시 SQLite가 기본 내장됨.
  • iOS → Core Data에서 SQLite를 백엔드 저장소로 활용.

📌 2. 웹 브라우저 및 로컬 스토리지

  • Google Chrome, Firefox → 쿠키, 설정 정보 저장에 사용.
  • Electron 앱 (VS Code 등) → SQLite를 데이터 저장소로 사용.

📌 3. 임베디드 시스템 및 IoT

  • 스마트 TV, 셋톱박스, 자동차 내비게이션 → 경량 데이터 저장 용도로 활용.

📌 4. 테스트 및 프로토타이핑

  • 대규모 DBMS를 사용하기 전에 간단한 데이터 모델 테스트 용도로 SQLite 사용.

 


SQLite의 WAL(Write-Ahead Logging) 모드

SQLite는 기본적으로 Rollback Journal 모드를 사용하지만, WAL 모드를 사용하면 동시 읽기 성능이 향상됨.

PRAGMA journal_mode=WAL;

 

WAL 모드 장점:

  • 여러 개의 프로세스가 동시에 읽기 가능 (쓰기 성능은 여전히 낮음).
  • 트랜잭션 속도 향상 (쓰기 성능이 약간 개선됨).

 

 


SQLite를 사용하면 좋은 경우 vs 사용하면 안 되는 경우

✅ SQLite를 사용하면 좋은 경우

✔️ 단순한 데이터 저장(모바일 앱, 설정 저장, 쿠키 등).
✔️ 읽기(Read) 작업이 많고 쓰기(Write) 작업이 적은 경우.
✔️ 서버 없이 간단한 DB가 필요한 경우.
✔️ 파일 하나로 데이터를 저장해야 하는 경우.

❌ SQLite를 사용하면 안 되는 경우

웹 애플리케이션의 메인 데이터베이스 (MySQL, PostgreSQL 사용 추천).
대량의 동시 쓰기 작업이 필요한 경우.
복잡한 권한 관리, 멀티 유저 기능이 필요한 경우.

'학습 기록 (Learning Logs) > CS Study' 카테고리의 다른 글

transaction  (0) 2025.02.03
isolation level  (0) 2025.02.03
Tree  (0) 2025.02.03
WAL(Write-Ahead Logging)  (0) 2025.02.03
kafka  (0) 2025.01.23