텔레그램 봇

SQLite로 상태와 대화 기록 영구화하기

WAL 모드 활성화, aiosqlite 비동기 연결, 유저 세션 영구 보존 및 무중단 온라인 백업 완벽 가이드.

업데이트: 2026-10-02

텔레그램 봇을 운영할 때 사용자의 대화 기록, 포인트 잔액, 구독 설정, 일일 이용량 카운터를 메모리 변수(dict, list)에만 보관하면, 서버가 재배포되거나 재시작되는 순간 모든 데이터가 완전히 증발합니다.

외부 클라우드 데이터베이스(AWS RDS, Supabase 등)를 매달 수만 원씩 들여 구축할 필요 없이, DashCloud의 영구 디스크 볼륨과 내장 SQLite를 조합하여 비용 0원에 초저지연 로컬 데이터 저장소를 운영하는 방법을 상세히 다룹니다.


왜 DashCloud + SQLite 조합이 최고일까요?

  1. 외부 DB 고정비 0원: 매달 1~3만 원씩 청구되는 관리형 PostgreSQL/MySQL 인스턴스 비용이 전혀 발생하지 않습니다.
  2. 마이크로초(µs) 단위 초고속 쿼리: 네트워크를 거치지 않고 DashCloud 고성능 NVMe SSD에서 직접 읽고 쓰므로 0.1ms 미만의 초고속 응답을 보장합니다.
  3. 영구 볼륨 디렉터리(/app/data): 새 코드를 커밋하고 재배포해도 물리 디스크에 보존되어 데이터 유실이 원천 차단됩니다.
  4. 단일 파일로 손쉬운 백업: bot.db 파일 하나만 복사하면 어디서든 로컬 PC나 다른 환경으로 데이터를 100% 복원할 수 있습니다.

동시성 잠금 방지: WAL 모드 필수 설정

비동기 텔레그램 봇에서 여러 사용자가 동시에 메시지를 보낼 때 가장 흔하게 발생하는 에러가 sqlite3.OperationalError: database is locked입니다.

기본 저널 DELETE 모드는 쓰기 트랜잭션이 진행되는 동안 다른 모든 읽기 쿼리를 차단하기 때문입니다. 이를 해결하기 위해 반드시 WAL(Write-Ahead Logging) 모드를 활성화해야 합니다.

SQLite 필수 PRAGMA 설정
-- 1. WAL 모드 활성화 (읽기와 쓰기가 서로를 블로킹하지 않음)
PRAGMA journal_mode = WAL;
 
-- 2. 동기화 수준 최적화 (성능과 내구성의 최적 균형)
PRAGMA synchronous = NORMAL;
 
-- 3. 메모리 캐시 크기 증설 (약 64MB 할당)
PRAGMA cache_size = -64000;
 
-- 4. 외래 키 제약 조건 활성화
PRAGMA foreign_keys = ON;

완성형 비동기 SQLite 매니저 보일러플레이트

파이썬 비동기 프레임워크와 궁합이 가장 뛰어난 aiosqlite 라이브러리를 활용한 프로덕션급 데이터베이스 관리자 모듈입니다.

requirements.txt에 추가
aiosqlite>=0.20.0
database.py
import os
import logging
from pathlib import Path
import aiosqlite
 
logger = logging.getLogger("Database")
 
# DashCloud 영구 볼륨 디렉터리 경로 설정
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR / "data"
DATA_DIR.mkdir(parents=True, exist_ok=True)
DB_PATH = DATA_DIR / "bot_database.db"
 
class DatabaseManager:
    def __init__(self, db_path: Path = DB_PATH):
        self.db_path = db_path
 
    async def get_connection(self) -> aiosqlite.Connection:
        """연결 풀 생성 및 WAL 모드 자동 주입"""
        conn = await aiosqlite.connect(self.db_path, timeout=30.0)
        conn.row_factory = aiosqlite.Row  # 딕셔너리처럼 컬럼명으로 접근 가능
        await conn.execute("PRAGMA journal_mode = WAL;")
        await conn.execute("PRAGMA synchronous = NORMAL;")
        return conn
 
    async def init_tables(self):
        """테이블 스키마 생성 및 초기화"""
        async with await self.get_connection() as conn:
            # 1. 사용자 정보 테이블
            await conn.execute("""
                CREATE TABLE IF NOT EXISTS users (
                    user_id INTEGER PRIMARY KEY,
                    username TEXT,
                    first_name TEXT,
                    points INTEGER DEFAULT 100,
                    is_admin INTEGER DEFAULT 0,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP
                );
            """)
 
            # 2. 대화 기록 테이블 (대화형 AI 챗봇 메모리용)
            await conn.execute("""
                CREATE TABLE IF NOT EXISTS chat_history (
                    id INTEGER PRIMARY KEY AUTOINCREMENT,
                    user_id INTEGER,
                    role TEXT CHECK(role IN ('user', 'assistant')),
                    content TEXT NOT NULL,
                    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
                    FOREIGN KEY(user_id) REFERENCES users(user_id) ON DELETE CASCADE
                );
            """)
            await conn.commit()
            logger.info("SQLite 테이블 초기화 완료 (WAL 모드 활성화)")
 
    async def upsert_user(self, user_id: int, username: str, first_name: str):
        """유저 접속 시 자동 등록 또는 방문 일시 갱신"""
        async with await self.get_connection() as conn:
            await conn.execute("""
                INSERT INTO users (user_id, username, first_name, last_seen)
                VALUES (?, ?, ?, CURRENT_TIMESTAMP)
                ON CONFLICT(user_id) DO UPDATE SET
                    username = excluded.username,
                    first_name = excluded.first_name,
                    last_seen = CURRENT_TIMESTAMP;
            """, (user_id, username, first_name))
            await conn.commit()
 
    async def get_user(self, user_id: int):
        """특정 유저 정보 조회"""
        async with await self.get_connection() as conn:
            async with conn.execute("SELECT * FROM users WHERE user_id = ?", (user_id,)) as cursor:
                return await cursor.fetchone()
 
# 싱글톤 인스턴스 생성
db = DatabaseManager()

텔레그램 봇 핸들러 연동 예시

생성한 데이터베이스 매니저를 main.py의 명령어 핸들러와 결합합니다.

main.py
import os
import sys
import logging
from telegram import Update
from telegram.ext import Application, CommandHandler, ContextTypes
from database import db
 
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
 
async def start_handler(update: Update, context: ContextTypes.DEFAULT_TYPE):
    user = update.effective_user
    # 1. DB에 유저 정보 자동 기록 (영구 저장)
    await db.upsert_user(
        user_id=user.id,
        username=user.username or "",
        first_name=user.first_name
    )
    
    # 2. 유저 정보 조회
    user_data = await db.get_user(user.id)
    points = user_data["points"]
 
    await update.message.reply_text(
        f"반갑습니다 {user.first_name}님!\n"
        f"현재 보유 포인트: {points}P\n"
        f"회원 정보가 영구 디스크(/app/data)에 안전하게 보존됩니다."
    )
 
async def post_init(application: Application):
    """봇 기동 시 비동기 DB 테이블 초기화"""
    await db.init_tables()
 
def main():
    token = os.environ.get("TELEGRAM_BOT_TOKEN")
    if not token:
        logger.critical("TELEGRAM_BOT_TOKEN이 누락되었습니다.")
        sys.exit(1)
 
    app = (
        Application.builder()
        .token(token)
        .post_init(post_init)  # 테이블 자동 생성 훅 바인딩
        .build()
    )
 
    app.add_handler(CommandHandler("start", start_handler))
    app.run_polling(drop_pending_updates=True)
 
if __name__ == "__main__":
    main()

무중단 온라인 백업과 웹 파일 다운로드

1. 파이썬 스케줄러로 무중단 온라인 백업 (sqlite3.backup)

SQLite는 가동 중인 데이터베이스를 서비스 중단 없이 안전하게 다른 파일로 복사하는 backup() API를 제공합니다.

backup_cron.py
import sqlite3
import shutil
from datetime import datetime
from pathlib import Path
 
def create_online_backup():
    source_db = "/app/data/bot_database.db"
    backup_dir = Path("/app/data/backups")
    backup_dir.mkdir(parents=True, exist_ok=True)
    
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    target_file = backup_dir / f"backup_{timestamp}.db"
 
    # 잠금 없이 온라인 상태에서 안전하게 복제
    with sqlite3.connect(source_db) as src, sqlite3.connect(target_file) as dst:
        src.backup(dst)
    
    print(f"백업 완료: {target_file}")

2. 대시보드 웹 파일 관리자에서 DB 다운로드

  • DashCloud 대시보드의 프로젝트 상세 ➔ 파일 메뉴로 이동합니다.
  • /app/data/bot_database.db 파일을 클릭하면, 브라우저에서 즉시 내 PC로 다운로드하여 DB Browser for SQLite 같은 무료 도구로 테이블과 데이터를 시각적으로 열람할 수 있습니다.

다음 추천 문서