Moltbot으로 대량 콘텐츠 관리하기: SQLite 연동 전략

Moltbot은 24시간 돌아가는 AI 비서로 꽤 유용하다. 간단한 작업 자동화나 주기적인 알림 정도는 문제없이 해낸다. 그런데 한 달치 블로그 포스트를 미리 작성해두고 매주 배포하는 건 어떨까? 아니면 트위터, 링크드인, 블로그, 뉴스레터에 각각 맞춤형 콘텐츠를 동시에 배포하는 건? 이런 생각을 하다 보면 금방 한계에 부딪힌다.

Moltbot은 MEMORY.md 파일과 벡터 인덱스로 기억을 관리하는데, 콘텐츠가 수백 개를 넘어가면 컨텍스트 윈도우가 감당하기 힘들어진다. "지난달에 초안 작성했던 그 글, 아직 배포 안 한 거 있어?"라고 물어봤을 때 정확히 답하기 어려워지는 것이다. 이건 Moltbot이 못나서가 아니라, 메모리 기반 관리의 구조적 한계다.

AI 에이전트의 한계와 전통적인 DB의 만남

사람들은 AI가 만능이라고 생각하기 쉽지만, 사실 AI 에이전트는 실행자로서 훌륭하다. 대신 상태 관리는 잘 못한다. 반대로 데이터베이스는 상태 관리에 특화되어 있지만, 스스로 판단하고 실행하지는 못한다.

그렇다면 둘을 결합하면 어떨까? Moltbot은 실행자 역할만 하고, 콘텐츠의 상태와 메타데이터는 SQLite 같은 외부 DB에 저장하는 것이다. 이렇게 하면 Moltbot은 "오늘 배포할 콘텐츠 뭐 있어?"라는 질문에 DB를 조회해서 정확히 답할 수 있고, 배포 후에는 상태를 업데이트할 수도 있다.

SQLite를 선택한 이유

여러 채널에 동시 배포하고, 초안 작성 후 검토 워크플로우를 거치려면 제법 복잡한 상태 관리가 필요하다. 그렇다고 PostgreSQL이나 MySQL 같은 서버를 띄우긴 과하다. 개인 프로젝트나 소규모 콘텐츠 관리라면 SQLite가 딱 적당하다.

SQLite는 파일 기반이라 설치나 설정이 거의 필요 없고, Moltbot이 파일 경로만 알면 바로 접근할 수 있다. 게다가 트랜잭션, 인덱스, 외래키 같은 기본적인 DB 기능을 다 지원하니 안정적이다.

콘텐츠 관리를 위한 DB 구조

대량 콘텐츠를 체계적으로 관리하려면 단순히 제목과 내용만 저장하면 안 된다. 어떤 상태인지, 어디에 배포할 건지, 언제 배포할 건지 같은 정보가 필요하다. 이를 위해 다음과 같은 테이블 구조를 설계할 수 있다.

-- 메인 콘텐츠 테이블
CREATE TABLE content (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  title TEXT NOT NULL,
  topic TEXT,
  category TEXT,
  tags TEXT,  -- JSON array로 저장
  idea TEXT,  -- 초기 아이디어
  status TEXT DEFAULT 'idea',  -- idea, draft, review, approved, published
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 콘텐츠 버전 관리
CREATE TABLE content_version (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content_id INTEGER NOT NULL,
  version_number INTEGER NOT NULL,
  body TEXT NOT NULL,  -- 실제 콘텐츠
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (content_id) REFERENCES content(id)
);

-- 배포 채널
CREATE TABLE channel (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE,  -- blog, twitter, linkedin, newsletter
  config TEXT  -- JSON으로 채널별 설정 저장
);

-- 배포 정보
CREATE TABLE publication (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content_id INTEGER NOT NULL,
  channel_id INTEGER NOT NULL,
  scheduled_at DATETIME,
  published_at DATETIME,
  status TEXT DEFAULT 'scheduled',  -- scheduled, published, failed
  metadata TEXT,  -- JSON으로 배포 결과 저장
  FOREIGN KEY (content_id) REFERENCES content(id),
  FOREIGN KEY (channel_id) REFERENCES channel(id)
);

이 구조는 콘텐츠의 전체 생명주기를 관리한다. 처음 아이디어가 떠올랐을 때는 idea 상태로 저장하고, 초안을 작성하면 draft, 검토를 거쳐 approved 가 되면 각 채널별로 publication 레코드를 만들어서 스케줄링한다. 배포가 완료되면 published 상태로 업데이트하는 식이다.

Moltbot과 SQLite 연동하기

Moltbot은 기본적으로 파일 시스템 접근이 가능하고, 터미널 명령어도 실행할 수 있다. SQLite는 파일 기반 DB라서 경로만 알면 바로 접근 가능하다. 간단한 조회는 Moltbot이 직접 sqlite3 CLI로 실행하면 된다.

예를 들어 "오늘 배포할 콘텐츠 있어?"라고 물으면 Moltbot은 이렇게 쿼리를 실행한다.

sqlite3 /path/to/content.db \
  "SELECT c.title, ch.name, p.scheduled_at
   FROM publication p
   JOIN content c ON p.content_id = c.id
   JOIN channel ch ON p.channel_id = ch.id
   WHERE DATE(p.scheduled_at) = DATE('now')
   AND p.status = 'scheduled';"

하지만 복잡하거나 위험한 작업은 스크립트로 만들어두는 게 안전하다. 예를 들어 배포 작업은 여러 단계를 거치고, 실패 시 롤백도 필요할 수 있다. 이런 건 Python이나 Node.js로 스크립트를 작성해두고, Moltbot은 그걸 실행만 하는 식으로 구성하면 된다.

# publish_content.py
import sqlite3
import sys

def publish_content(content_id, channel_id):
    conn = sqlite3.connect('/path/to/content.db')
    cursor = conn.cursor()

    try:
        # 콘텐츠와 채널 정보 조회
        cursor.execute("""
            SELECT c.title, cv.body, ch.name, ch.config
            FROM content c
            JOIN content_version cv ON c.id = cv.content_id
            JOIN channel ch ON ch.id = ?
            WHERE c.id = ?
            ORDER BY cv.version_number DESC
            LIMIT 1
        """, (channel_id, content_id))

        result = cursor.fetchone()
        if not result:
            raise Exception("Content not found")

        title, body, channel_name, config = result

        # 실제 배포 로직 (생략)
        # publish_to_channel(channel_name, title, body, config)

        # 배포 완료 상태 업데이트
        cursor.execute("""
            UPDATE publication
            SET status = 'published',
                published_at = CURRENT_TIMESTAMP
            WHERE content_id = ? AND channel_id = ?
        """, (content_id, channel_id))

        conn.commit()
        print(f"Successfully published: {title} to {channel_name}")

    except Exception as e:
        conn.rollback()
        print(f"Failed to publish: {e}")
        sys.exit(1)
    finally:
        conn.close()

if __name__ == "__main__":
    publish_content(int(sys.argv[1]), int(sys.argv[2]))

Moltbot은 이 스크립트를 python publish_content.py 123 1 같은 식으로 실행하기만 하면 된다.

이미지 콘텐츠 처리하기

텍스트만 다루면 간단한데, 이미지가 포함된 콘텐츠는 어떻게 할까? Moltbot이 이미지를 이해하려면 결국 Claude API를 통해야 하는데, 여기서 주의할 점이 있다.

Claude API는 파일 경로를 직접 이해하지 못한다. /Users/namhoon/image.png 같은 경로를 보내면 그냥 텍스트로 인식할 뿐이다. Claude API 서버는 내 컴퓨터에 접근할 수 없기 때문이다.

이미지를 전달하는 방법은 두 가지다.

Base64 인코딩

파일을 읽어서 base64 문자열로 변환한 뒤 전송하는 방식이다.

import anthropic
import base64

def analyze_image(image_path, prompt):
    with open(image_path, "rb") as f:
        image_data = base64.standard_b64encode(f.read()).decode("utf-8")

    # 확장자로 media_type 결정
    ext = image_path.split('.')[-1].lower()
    media_types = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg', 'gif': 'image/gif', 'webp': 'image/webp'}
    media_type = media_types.get(ext, 'image/png')

    client = anthropic.Anthropic()
    response = client.messages.create(
        model="claude-sonnet-4-20250514",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": [
                {
                    "type": "image",
                    "source": {
                        "type": "base64",
                        "media_type": media_type,
                        "data": image_data,
                    },
                },
                {"type": "text", "text": prompt}
            ],
        }],
    )
    return response.content[0].text

공개 URL

이미지가 웹에 공개되어 있다면 URL을 직접 전달할 수도 있다.

{
    "type": "image",
    "source": {
        "type": "url",
        "url": "https://example.com/image.png",
    },
}

다만 URL 방식은 몇 가지 조건이 있다. 인증 없이 접근 가능해야 하고, 리다이렉트가 없어야 하며, 지원 포맷(JPEG, PNG, GIF, WebP)이어야 한다. 크기는 최대 20MB까지 가능하지만 5MB 이하를 권장한다.

실무에서는 base64 방식이 더 안정적이다. URL은 외부 서버 상태에 의존하고, 만료되거나 변경될 수 있기 때문이다.

이미지 사이즈 제한

Claude API에는 이미지 크기 제한이 있다. API를 통해 보낼 때는 5MB 가 상한이고, claude.ai 웹에서는 10MB까지 된다. 해상도는 8000×8000 픽셀까지 지원하지만, 한 번에 20개 이상의 이미지를 보내면 2000×2000 픽셀로 제한된다.

권장 사항은 최소 1000×1000 픽셀 이상이면 분석 정확도가 좋고, 파일 크기는 5MB 이하로 유지하는 게 안전하다. 제한을 넘기면 API 에러가 나고, 컨텍스트가 오염되는 버그도 보고되어 있다.

이해 능력은 동일하다

base64든 URL이든 동일한 비전 모델을 사용한다. 전송 방식만 다를 뿐, Claude가 이미지를 이해하는 능력은 똑같다. 이미지가 Claude 서버에 도착한 이후에는 같은 멀티모달 모델이 처리하기 때문이다.

이미지 리사이즈 유틸리티

이미지가 크면 API 제한에 걸리기 전에 리사이즈하는 게 좋다.

from PIL import Image
import io
import base64

def prepare_image_for_api(path, max_size_mb=4, max_dimension=4000):
    """API 제한에 맞게 이미지 리사이즈 후 base64 반환"""
    img = Image.open(path)

    # 해상도 줄이기
    if max(img.size) > max_dimension:
        ratio = max_dimension / max(img.size)
        new_size = (int(img.size[0] * ratio), int(img.size[1] * ratio))
        img = img.resize(new_size, Image.LANCZOS)

    # 품질 조정하며 크기 맞추기
    quality = 95
    while quality > 20:
        buffer = io.BytesIO()
        img.save(buffer, format='JPEG', quality=quality)
        size_mb = buffer.tell() / (1024 * 1024)
        if size_mb <= max_size_mb:
            buffer.seek(0)
            return base64.standard_b64encode(buffer.read()).decode('utf-8')
        quality -= 10

    raise ValueError("이미지를 충분히 압축할 수 없습니다")

이 함수를 사용하면 어떤 크기의 이미지든 API 제한에 맞게 자동으로 조정된다.

콘텐츠 DB에 이미지 정보 저장하기

이미지가 포함된 콘텐츠를 관리한다면, DB 구조를 약간 확장하면 된다.

-- 콘텐츠에 연결된 이미지
CREATE TABLE content_image (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  content_id INTEGER NOT NULL,
  file_path TEXT NOT NULL,        -- 로컬 파일 경로
  alt_text TEXT,                  -- 대체 텍스트
  description TEXT,               -- AI가 분석한 이미지 설명
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  FOREIGN KEY (content_id) REFERENCES content(id)
);

이미지를 저장할 때 Claude에게 분석을 요청해서 description 필드에 저장해두면, 나중에 이미지 내용으로 검색하거나 자동으로 alt text를 생성하는 데 활용할 수 있다.

실제 워크플로우 예시

이제 전체적인 흐름을 그려보자.

1. 아이디어 수집

트위터를 보다가 좋은 아이디어가 떠올랐다. Moltbot에게 메시지를 보낸다.

"블로그 아이디어 추가: Moltbot으로 콘텐츠 관리하기. 카테고리는 AI-ML, 태그는 moltbot, automation"

Moltbot은 이 정보를 파싱해서 DB에 저장한다.

INSERT INTO content (title, topic, category, tags, status)
VALUES (
  'Moltbot으로 콘텐츠 관리하기',
  'AI 에이전트와 DB 연동',
  'AI-ML',
  '["moltbot", "automation"]',
  'idea'
);

2. 초안 작성

주말에 시간이 나서 초안을 작성한다. Moltbot에게 물어본다.

"초안 작성할 아이디어 있어?"

Moltbot은 status = 'idea' 인 콘텐츠를 조회해서 보여준다. 그중 하나를 선택하면 Claude나 ChatGPT로 초안을 작성하고, 그 결과를 content_version 테이블에 저장한다.

INSERT INTO content_version (content_id, version_number, body)
VALUES (1, 1, '초안 내용...');

UPDATE content SET status = 'draft' WHERE id = 1;

3. 검토 및 수정

며칠 뒤 다시 읽어보니 수정할 부분이 보인다. Moltbot에게 요청한다.

"ID 1번 콘텐츠 수정해줘: 제목을 더 구체적으로 바꾸고, SQLite 연동 부분 추가"

수정된 내용은 새로운 버전으로 저장된다.

INSERT INTO content_version (content_id, version_number, body)
VALUES (1, 2, '수정된 내용...');

4. 배포 스케줄링

만족스러우면 배포를 예약한다.

"ID 1번 콘텐츠, 블로그랑 트위터에 이번 주 금요일 오전 10시에 배포해줘"

UPDATE content SET status = 'approved' WHERE id = 1;

INSERT INTO publication (content_id, channel_id, scheduled_at, status)
VALUES
  (1, 1, '2026-01-31 10:00:00', 'scheduled'),
  (1, 2, '2026-01-31 10:00:00', 'scheduled');

5. 자동 배포

Moltbot의 Cron 기능으로 매일 오전 10시에 배포 작업을 확인한다.

# Moltbot Cron 설정
0 10 * * * python /path/to/check_and_publish.py

스크립트는 scheduled_at 이 현재 시각보다 이전이고 status = 'scheduled' 인 항목을 찾아서 배포를 실행한다. 배포가 완료되면 상태를 업데이트한다.

한계와 개선 방향

이 방식도 완벽하지는 않다. SQLite는 동시 쓰기에 약하기 때문에 여러 프로세스가 동시에 DB를 업데이트하면 락 충돌이 발생할 수 있다. 하지만 개인 프로젝트나 소규모 콘텐츠 관리라면 충분히 감당 가능한 수준이다.

만약 규모가 커진다면 PostgreSQL로 마이그레이션하거나, Notion API를 활용하는 방법도 있다. Notion을 DB처럼 사용하면 UI도 편하고, 팀원들과 협업하기도 쉽다. 다만 API 호출 횟수 제한이 있으니 대량 작업에는 주의가 필요하다.

마무리

Moltbot은 훌륭한 AI 비서지만, 메모리만으로 수백 개의 콘텐츠를 관리하긴 어렵다. 대신 SQLite 같은 전통적인 DB를 붙여서 상태 관리를 맡기면, Moltbot은 실행자로서 제 역할을 충분히 해낸다.

AI 에이전트는 실행자로, 상태는 DB로 분리하는 것. 이 원칙만 지키면 훨씬 확장 가능하고 안정적인 자동화 시스템을 만들 수 있다. 이제 당신의 Moltbot에게 콘텐츠 관리를 맡겨보는 건 어떨까?

참고