Featured image of post SQLite의 관대함에 속지 마라! 동적 타입의 함정이란? ALTER TABLE은 왜 미완성인가? Node.js에서 방어적 프로그래밍으로 매끄러운 테이블 마이그레이션을 구현하는 방법

SQLite의 관대함에 속지 마라! 동적 타입의 함정이란? ALTER TABLE은 왜 미완성인가? Node.js에서 방어적 프로그래밍으로 매끄러운 테이블 마이그레이션을 구현하는 방법

SQLite는 동적 약타입 시스템을 채택하여 INTEGER 컬럼에 문자열을 삽입해도 에러를 발생시키지 않습니다. SQLite의 타입 어피니티(Type Affinity) 함정, 네이티브 Boolean 및 Date 타입의 부재가 미치는 영향, ALTER TABLE의 제한사항과 '4단계 재생성 및 마이그레이션'의 안전한 업그레이드 전략, TypeScript, Zod, Prisma 등의 도구를 통한 방어적 프로그래밍 아키텍처에 대해 알아봅니다.

재활용 수거함에 ‘페트병 전용’이라는 라벨을 붙여 놓았는데, 누군가 종이 조각을 집어넣어도 아무런 항의 없이 묵묵히 받아들이는 모습을 상상해 보세요.

이것이 바로 개발자가 SQLite 타입 시스템을 처음 만났을 때 겪게 되는 등골 오싹한 경험입니다.

타입이 맞지 않으면 즉시 입국을 거부하는 PostgreSQL 같은 엄격한 세관 스타일에 익숙하다면, SQLite 소탈함(또는 느슨함)에 인생을 의심하게 될지도 모릅니다.

더 끔찍한 것은, 테이블 구조를 변경하려고 할 때 그것이 당신에게 이렇게 말한다는 것입니다:

“테이블을 직접 수정할 수 없습니다. 새 집을 짓고, 가구를 옮긴 다음, 오래된 집을 폭파하세요.”

SQLite의 내부에는 5가지 스토리지 클래스만 존재한다

CREATE TABLE에서 아무리 화려한 타입 이름(VARCHAR(255), BIGINT, DECIMAL)을 선언하더라도, SQLite 내부적으로는 다음 5가지 스토리지 클래스만 인식합니다:

스토리지 클래스 설명
NULL 빈 값
INTEGER 정수 (값의 크기에 따라 자동으로 1~8 바이트 점유)
REAL 부동 소수점 수 (고정 8 바이트)
TEXT 문자열 (기본 UTF-8 인코딩)
BLOB 바이너리 데이터 (입력된 상태 그대로 저장)

컬럼에 설정한 타입은 SQLite에게 단지 ‘권장 사항’일 뿐이며, ‘강제 규칙’이 아닙니다.

이를 **“타입 어피니티(Type Affinity)”**라고 부릅니다. SQLite는 데이터를 권장 타입으로 변환하려고 시도하지만, 변환할 수 없는 경우에는 에러 없이 원본 데이터를 그대로 삽입합니다.

SQLite에서 age INTEGER 컬럼을 선언하고 문자열 '영원한 18세'를 삽입해도 아주 기쁘게 받아들여집니다.

가장 쉽게 빠지기 쉬운 3가지 타입 함정

함정 1: 네이티브 Boolean 부재

SQLite에는 Boolean 타입이 없습니다. TrueFalse는 정수 10으로만 표현할 수 있습니다.

Node.js를 사용해 SQLite에서 데이터를 조회할 때, truefalse가 아니라 숫자 1 또는 0이 반환됩니다.

if (user.is_admin === true)와 같은 조건식을 직접 사용하면 절대 참이 되지 않습니다.

함정 2: Date/Time 타입 부재

SQLite에는 날짜 시간 타입이 없습니다. 시간은 다음과 같이 저장할 수밖에 없습니다:

저장 방식 예시 장단점
TEXT (ISO-8601 문자열) '2026-05-19T18:00:00Z' 가장 추천. 가독성이 높고 향후 PostgreSQL 마이그레이션 시 매끄럽게 변환 가능
INTEGER (Unix 타임스탬프) 1747656000 공간을 적게 차지하지만 사람이 읽을 수 없음

날짜를 2026/5/19 오후 6시와 같이 자신만의 커스텀 형식으로 저장하지 마세요. 향후 데이터 마이그레이션 시 재앙이 일어날 것입니다.

함정 3: INTEGER 컬럼에 문자열을 삽입해도 에러가 나지 않음

PostgreSQL에서는 INTEGER 컬럼에 문자열을 삽입하면 즉시 에러가 발생합니다. 하지만 SQLite는 묵묵히 변환을 시도하고, 실패하면 원본 그대로 수락합니다.

이는 오염된 데이터가 조용히 데이터베이스에 스며들 수 있음을 의미합니다. 어느 날 프로그램이 예상치 못한 타입을 받아 크래시가 나기 전까지는 문제를 발견할 수 없습니다.

방어적 프로그래밍: 관대한 데이터베이스를 엄격한 태도로 다루기

SQLite의 느슨함에 직면했을 때, Node.js 개발에서는 엄격한 방어 메커니즘을 구축해야 합니다:

방어 단계 도구 역할
컴파일 타임 가드 TypeScript 코드 작성 단계에서 잘못된 타입을 캐치함
API 입구 검증 Zod 입력되는 데이터를 엄격하게 검증함 (age가 반드시 숫자임을 보장)
내부 타입 변환 Prisma / Drizzle ORM SQLite와 PostgreSQL 간의 타입 차이를 자동으로 처리함

**‘데이터 검증의 문지기’**를 데이터베이스 계층에서 Node.js 애플리케이션 계층으로 이동시키는 것이 SQLite의 개발 속도를 누리면서 향후 확장성을 확보하기 위한 핵심 전략입니다.

ORM을 사용할 경우 코드 내에서 type: 'boolean'으로 선언해 두면, ORM이 SQLite 저장 시 자동으로 1/0으로 변환하고, 조회 시 true/false로 자동 변환해 주어 기저의 타입 차이를 완벽히 가려줍니다.

ALTER TABLE은 미완성: 수정할 수 있는 것과 없는 것

SQLite의 테이블 구조 수정 지원은 매우 제한적입니다:

작업 지원 여부
컬럼 추가 (ADD COLUMN)
컬럼 이름 변경 (RENAME COLUMN)
컬럼 삭제 (DROP COLUMN) (최신 버전에서 지원)
테이블 이름 변경 (RENAME TO)
컬럼 타입 변경 아니오
UNIQUE, NOT NULL 제약 조건 추가/제거 아니오
기본키(Primary Key) 수정 아니오
외래키(Foreign Key) 수정 아니오

‘지원되지 않는’ 수정을 수행해야 할 때, SQLite‘재생성 및 마이그레이션’ 전략을 실행할 것을 요구합니다.

재생성 및 마이그레이션 4단계: SQLite식 테이블 업그레이드 방법

직접 변경이 불가능하므로 공식 문서가 권장하는 표준 방법은 새 집을 짓고, 가구를 옮기고, 이전 집을 날려버리고, 새 문패를 다는 것입니다:

단계 액션 설명
1 새 테이블 생성 올바른 구조로 CREATE TABLE users_new (...) 생성
2 데이터 복사 INSERT INTO users_new SELECT ... FROM users
3 이전 테이블 삭제 DROP TABLE users
4 테이블 이름 변경 ALTER TABLE users_new RENAME TO users

이 4개 단계는 일사천리로 실행되어야 합니다. 중간에 전원이 끊기거나 프로그램이 크래시 나면 데이터 유실로 이어집니다.

업그레이드 시 데이터 유실 방지: 두 가지 안전 방어선

방어선 1: 물리적 방어, 파일 직접 복사

SQLite는 본질적으로 하나의 파일입니다. 스키마 변경을 실행하기 전에 .db 파일을 복사해 백업본을 만드는 것으로 충분합니다.

const fs = require('fs');
fs.copyFileSync('my_project.db', 'my_project_backup.db');

만약 문제가 발생하더라도 파일을 덮어쓰는 것만으로 눈 깜짝할 사이에 복구할 수 있습니다. 이는 다른 대형 데이터베이스에서는 제공하지 않는 큰 장점입니다.

방어선 2: 트랜잭션 래퍼, 데이터베이스의 타임머신

이사 단계를 하나의 Transaction으로 감싸줍니다. 단계 중 하나라도 실패하면 전체 프로세스가 자동으로 롤백되어 아무 일도 없었던 것처럼 복원됩니다.

const Database = require('better-sqlite3');
const db = new Database('my_project.db');

const migrateData = db.transaction(() => {
  db.prepare(`
    CREATE TABLE users_new (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      name TEXT NOT NULL,
      age INTEGER NOT NULL DEFAULT 18
    )
  `).run();

  db.prepare(`
    INSERT INTO users_new (id, name, age)
    SELECT id, name, COALESCE(age, 18) FROM users
  `).run();

  db.prepare('DROP TABLE users').run();
  db.prepare('ALTER TABLE users_new RENAME TO users').run();
});

try {
  migrateData();
  console.log('테이블 업그레이드 완료');
} catch (error) {
  console.error('업그레이드 실패, 데이터가 안전하게 복원되었습니다:', error.message);
}

**“백업 파일 + 트랜잭션 바인딩”**이 데이터베이스 마이그레이션의 에어백 역할을 합니다.

엄격한 태도로 소탈한 SQLite 제어하기

소탈한 SQLite엄격한 애플리케이션 계층 아키텍처로 다룸으로써, 향후 기술 부채를 방지하면서 극도로 빠른 개발 속도의 장점을 안심하고 누릴 수 있습니다.

SQLite의 타입 시스템은 매우 유연하며, ALTER TABLE 역시 제약 조건이 많습니다.

하지만 Node.js 단에서 TypeScript 타입 검사 + Zod 검증 + ORM 추상화를 착실히 적용하고, 물리적 백업 + 트랜잭션 안전 전략을 결합하면 SQLite가 제공하는 뛰어난 개발 효율성을 마음껏 누릴 수 있으며 동시에 향후 PostgreSQL로의 순탄한 이전 경로를 닦아둘 수 있습니다.

Reference

All rights reserved,未經允許不得隨意轉載
Hugo로 만듦
JimmyStack 테마 사용 중