OpenClaw 아키텍처 심층 분석: 3계층 설계의 기술 원리와 확장 실전

빠른 결론(아키텍처는 먼저 큰 줄기를 잡기)
OpenClaw를 가장 빠르게 이해하는 방법은 세 계층의 책임부터 붙잡는 것입니다. Gateway는 세션을 관리하고, Channel은 메시지 라우팅을 맡으며, LLM은 모델 인터페이스를 담당합니다.
문제를 이 세 계층 중 하나로 분류할 수만 있어도 장애 대응과 확장이 훨씬 빨라집니다.
이후 새 Channel이나 새 Provider를 개발하는 일도 본질적으로는 인터페이스에 맞춰 구현을 채우고 등록하는 과정입니다.
새벽 두 시, 저는 에디터에 열린 OpenClaw 코드베이스를 바라보며 DingTalk Channel을 추가하려고 하고 있었습니다. src 디렉터리 아래에는 수십 개 파일이 있고 gateway, channel, llm 폴더가 서로 얽혀 있었습니다. 어디서부터 손대야 할지 전혀 감이 오지 않았습니다. gateway를 고치면 다른 Channel에 영향을 주는 걸까? WhatsApp 코드를 그대로 복사해도 될까? 한 번 고쳤다가 아예 실행되지 않으면 어떡하지?
솔직히 말해 당시에는 꽤 막막했습니다. 공식 문서는 사용법은 알려주지만 시스템 내부가 어떻게 돌아가는지는 설명하지 않았습니다. 2차 개발을 하려는데 마치 장님이 코끼리를 만지는 느낌이었습니다. webhook 처리는 보이지만 메시지가 어떻게 라우팅되는지 모르겠고, LLM 호출은 보이지만 Provider가 어떻게 등록되는지 이해되지 않았습니다.
이후 꼬박 사흘 동안 소스 코드를 처음부터 끝까지 읽고 나서야 OpenClaw의 설계가 정말 영리하다는 것을 알게 되었습니다. Gateway는 세션을 관리하고, Channel은 라우팅을 관리하며, LLM은 인터페이스를 관리합니다. 3계층 구조가 분명하고 책임 경계도 명확합니다. 이것을 이해하고 나면 2차 개발은 더 이상 어림짐작이 아니라 따라갈 수 있는 규칙이 있는 작업이 됩니다.
이 글에서는 그 사흘 동안 얻은 내용을 체계적으로 정리합니다. OpenClaw가 왜 세 계층으로 나뉘는지, 각 계층이 어떤 문제를 해결하는지, Gateway가 세션 상태를 어떻게 관리하는지, Channel이 서로 다른 플랫폼을 어떻게 맞추는지, LLM 계층의 Provider 플러그인 시스템이 어떻게 설계되어 있는지 살펴봅니다. 마지막에는 커스텀 Channel과 Provider를 직접 개발하는 과정까지 단계별로 다룹니다.
저비용 “새우 키우기” 가이드: ArkClaw가 AI Agent를 진짜 대중화합니다
요즘 인기 있는 OpenClaw(랍스터)는 유용하지만 설정 진입 장벽이 꽤 높습니다. ByteDance Volcano Engine의 ArkClaw는 서버와 Token 설정을 복잡하게 만질 필요 없이 문턱을 확 낮췄습니다. 한 번의 클릭으로 24시간 온라인 상태를 유지하고, 브라우저를 제어하고, 스크립트를 실행하고, 캘린더를 관리하는 “AI 일꾼”을 가질 수 있습니다.
핵심은 정말 저렴하다는 점입니다. 월 요금은 9.9위안이고, 제 초대 코드 ZLKUK54M(여기에서 가입)을 사용하면 8.9위안이면 됩니다. 개발자라면 Coding Plan Pro에 바로 들어가 무료로 써볼 수도 있습니다.
OpenClaw 아키텍처 전체 그림: 왜 3계층인가?
OpenClaw를 처음 접했을 때 저는 계속 한 가지가 궁금했습니다. 왜 이렇게 복잡하게 계층을 나눌까? 그냥 사용자 메시지를 AI에 넘기면 되는 것 아닌가?
소스 코드를 살펴보고 나서야 단일 구조는 작은 규모에서는 괜찮지만 OpenClaw에는 맞지 않는다는 것을 이해했습니다. OpenClaw는 여러 플랫폼(WhatsApp, Telegram, Gmail), 여러 모델(Claude, GPT, 로컬 모델)을 지원해야 하고, 수백 수천 개의 사용자 세션도 관리해야 합니다. 계층을 나누지 않으면 모든 로직이 한곳에 쌓이고, 한 부분을 수정하는 것만으로 전체에 영향을 줄 수 있어 유지보수가 어려워집니다.
3계층 아키텍처의 설계 철학
OpenClaw는 전체 시스템을 세 계층으로 나누고, 각 계층이 자기 일만 하도록 만듭니다.
Gateway 계층(세션 관리 중심)
- 사용자 세션의 전체 생명주기를 관리합니다.
- 메시지 큐와 스케줄링을 맡습니다(누가 먼저, 누가 나중인지).
- 인증과 권한 제어를 처리합니다(누가 사용할 수 있는지).
- WebSocket 장기 연결을 유지합니다.
Channel 계층(플랫폼 어댑터)
- 서로 다른 플랫폼의 메시지 형식을 맞춥니다(WhatsApp과 Telegram 형식은 다릅니다).
- 메시지 라우팅 규칙을 처리합니다(DM인지 Group인지, @가 있어야 응답할지).
- 이벤트를 처리합니다(메시지 수신, 메시지 전송, 오류 처리).
LLM 계층(모델 인터페이스)
- 통합 Provider 인터페이스를 제공합니다(Claude를 쓰든 GPT를 쓰든 호출 방식이 같습니다).
- 도구 호출(Function Calling)을 처리합니다.
- 스트리밍 응답을 처리합니다.
- MCP 서버를 통합합니다.
메시지 흐름 전체 과정
구체적인 장면을 보면 바로 이해됩니다. WhatsApp에서 봇에게 메시지를 보내면 전체 흐름은 다음과 같습니다.
- Channel 계층 수신: WhatsApp Channel이 webhook을 받고 메시지를 내부 표준 형식으로 바꿉니다.
- 라우팅 판단: DM인지 그룹 채팅인지, @봇이 있는지, 사용자 권한이 충분한지 확인합니다.
- Gateway 스케줄링: 이 사용자의 Session을 찾거나 만들고 메시지를 큐에 넣습니다.
- LLM 처리: 설정에 따라 Provider(예: Anthropic)를 선택하고 대화 컨텍스트를 보냅니다.
- 응답 반환: LLM 결과가 Gateway → Channel을 거쳐 사용자에게 돌아갑니다.
이 설계의 가장 영리한 점은 각 계층이 서로 간섭하지 않는다는 것입니다. 새 플랫폼을 추가하고 싶다면 Channel 계층만 고치면 됩니다. 모델을 바꾸고 싶다면 LLM 계층만 고치면 됩니다. Gateway는 전혀 건드릴 필요가 없습니다.
Gateway 계층: 세션 관리의 핵심 허브
제가 처음 Gateway 소스 코드를 봤을 때 가장 헷갈렸던 것은 Session 객체였습니다. 사용자마다 Session이 하나씩 있다는데, 이 객체가 정확히 무엇을 저장하고 어떻게 관리되는지 감이 오지 않았습니다.
Session의 생명주기
Gateway를 택배 분류 센터라고 생각해봅시다. 각 사용자는 배송 주소이고, Session은 그 주소의 수령 기록입니다.
Session 객체에 들어 있는 것:
conversationHistory: 대화 기록(최근 N개 메시지)context: 컨텍스트 변수(사용자 설정, 임시 데이터)state: 현재 상태(idle, processing, waiting)channelInfo: 출처 플랫폼 정보(어느 Channel에서 왔는지)
생명주기 관리:
// 단순화 예시: 핵심 로직만 보여줍니다
class SessionManager {
// 메시지를 받았을 때
async handleMessage(userId, channelId, message) {
// 1. Session 찾기(없으면 생성)
let session = this.getOrCreate(userId, channelId);
// 2. 대화 기록 업데이트
session.conversationHistory.push(message);
// 3. 처리 큐에 추가
await this.messageQueue.enqueue(session, message);
// 4. 영속화(크래시로 인한 유실 방지)
await this.persist(session);
}
}
중요한 지점은 OpenClaw가 per-channel-peer 격리 방식을 쓴다는 것입니다. 무슨 뜻일까요? 같은 사용자라도 WhatsApp과 Telegram에서는 서로 독립적인 Session을 갖고, 서로 영향을 주지 않습니다. 이 설계는 컨텍스트 혼선을 피하기 위한 것입니다. WhatsApp에서 기술 문제를 이야기하고 Telegram에서 날씨를 물어도 두 대화가 섞이지 않습니다.
메시지 스케줄링의 우선순위 전략
Gateway는 메시지를 받자마자 바로 처리하지 않고 스케줄링 큐를 둡니다. 이 설계는 주로 두 가지 문제를 해결합니다.
문제 1: 동시성 제어
사용자 100명이 동시에 메시지를 보낸다고 가정해봅시다. 이를 그대로 전부 LLM에 던지면 인터페이스가 터집니다. Gateway의 큐는 “동시에 최대 10개 요청만 처리” 같은 방식으로 속도를 제한할 수 있습니다.
문제 2: 오류 재시도
LLM 호출이 실패하면 어떻게 할까요? Gateway는 자동으로 3번 재시도하고, 간격을 점점 늘립니다(1초, 2초, 4초). 일시 장애 때문에 메시지가 유실되는 일을 줄입니다.
// 메시지 큐 핵심 로직
class MessageQueue {
async enqueue(session, message) {
// 동시 실행 수 확인
if (this.activeJobs >= this.maxConcurrency) {
// 대기 큐에 넣기
this.waitingQueue.push({ session, message });
return;
}
// 처리 실행
this.activeJobs++;
try {
await this.process(session, message);
} catch (error) {
// 재시도 로직
await this.retryWithBackoff(session, message);
} finally {
this.activeJobs--;
this.processNext(); // 다음 항목 처리
}
}
}
WebSocket 장기 연결의 함정
실시간성이 중요한 애플리케이션(예: 고객지원 봇)을 개발하려 한다면 WebSocket 연결 관리는 큰 함정입니다.
OpenClaw의 방식은 다음과 같습니다.
- 하트비트 감지: 30초마다 ping을 보내고, 타임아웃되면 연결이 끊어진 것으로 판단합니다.
- 자동 재연결: 연결이 끊기면 지수 백오프로 재연결합니다(1초, 2초, 4초… 최대 30초).
- 상태 동기화: 재연결 후 Session 상태를 자동 복구합니다.
이런 세부 사항은 작아 보이지만 안정성을 크게 높입니다. 저는 예전에 비슷한 시스템을 직접 만든 적이 있는데, 하트비트 감지를 넣지 않았습니다. 그 결과 연결은 사실상 죽었는데 프로그램은 이를 모르고 있었고, 사용자가 보낸 메시지는 아무 응답 없이 사라졌습니다.
Channel 계층: 멀티 플랫폼 메시지 라우팅
Channel 계층은 제가 가장 흥미롭게 본 부분입니다. 이 계층이 해결하는 핵심 문제는 플랫폼마다 메시지 형식이 완전히 다른데 어떻게 통합 처리할 것인가입니다.
Adapter 패턴의 절묘한 활용
WhatsApp 메시지는 이런 형태입니다.
{
"from": "1234567890",
"body": "안녕하세요",
"type": "text"
}
Telegram은 이런 형태입니다.
{
"message": {
"chat": {"id": 123},
"text": "안녕하세요"
}
}
플랫폼마다 로직을 따로 쓰면 코드가 폭발합니다. OpenClaw는 고전적인 Adapter 패턴을 사용합니다. 표준화된 Message 인터페이스를 정의하고, 각 Channel이 플랫폼 메시지를 이 형식으로 바꾸는 책임을 집니다.
// 표준화 메시지 형식
interface StandardMessage {
userId: string; // 통합 사용자 ID
content: string; // 메시지 내용
timestamp: number; // 타임스탬프
metadata: any; // 플랫폼별 데이터
}
// WhatsApp Adapter
class WhatsAppChannel implements Channel {
adaptMessage(rawMessage): StandardMessage {
return {
userId: rawMessage.from,
content: rawMessage.body,
timestamp: Date.now(),
metadata: { platform: 'whatsapp' }
};
}
}
이 설계의 장점은 Gateway와 LLM 계층이 메시지가 어느 플랫폼에서 왔는지 전혀 신경 쓰지 않아도 된다는 것입니다. 두 계층은 StandardMessage만 처리합니다.
라우팅 규칙의 구현 원리
Channel 계층에는 또 하나의 중요한 책임이 있습니다. 어떤 메시지에 응답하고 어떤 메시지는 무시할지 결정하는 것입니다.
OpenClaw는 두 가지 라우팅 규칙을 지원합니다.
dmPolicy(1:1 채팅 정책)
pairing: 먼저 페어링해야 대화 가능(가장 안전)allowlist: 허용 목록 사용자만 사용 가능open: 모든 사람이 사용 가능(공개 bot)disabled: 1:1 채팅 비활성화
mentionGating(그룹 채팅 @ 트리거)
그룹 채팅에서는 @봇이 있을 때만 응답해 채팅창 도배를 피합니다. 구현 로직은 단순합니다.
class TelegramChannel {
shouldRespond(message): boolean {
// 1:1 채팅은 바로 응답
if (message.chat.type === 'private') {
return this.checkDmPolicy(message.from.id);
}
// 그룹 채팅은 @ 확인
if (message.chat.type === 'group') {
const mentioned = message.entities?.some(
e => e.type === 'mention' && e.user.id === this.botId
);
return mentioned;
}
return false;
}
}
제가 DingTalk Channel을 개발할 때도 이 로직을 참고했습니다. DingTalk의 @ 감지는 조금 다르지만(atUsers 필드를 사용), 프레임워크는 같습니다.
커스텀 Channel 개발의 정석 흐름
Discord를 붙인다고 가정하면 대략 이런 흐름입니다.
- Channel 클래스 만들기:
Channel인터페이스를 구현합니다. - 필수 메서드 구현:
start(): Channel 시작(webhook 또는 WebSocket 수신)sendMessage(): 플랫폼으로 메시지 전송adaptMessage(): 메시지 형식 변환
- 시스템에 등록: 설정 파일에 Channel 설정을 추가합니다.
- 테스트: ngrok으로 로컬 서비스를 노출하고 webhook을 테스트합니다.
전체 예제 코드는 글 마지막의 실전 섹션에 두었습니다. 그대로 참고할 수 있습니다.
LLM 계층: 모델 인터페이스의 플러그인화 설계
LLM 계층은 2026년에 큰 리팩터링을 거쳤습니다. 하드코딩 방식에서 플러그인 시스템으로 바뀐 것입니다. 이 변화는 정말 중요합니다. OpenClaw가 얼마나 다양한 모델을 지원할 수 있는지를 직접 결정하기 때문입니다.
Provider 플러그인 시스템
이전 설계는 이런 형태였습니다(의사 코드).
// 이전 설계: 하드코딩
if (config.provider === 'anthropic') {
return new AnthropicClient();
} else if (config.provider === 'openai') {
return new OpenAIClient();
}
문제는 모델을 하나 추가할 때마다 if-else를 고쳐야 하고, 코드가 점점 비대해진다는 점입니다.
새 설계는 Provider 인터페이스를 도입했습니다.
// Provider 인터페이스 정의
interface LLMProvider {
name: string; // 'anthropic', 'openai', 'ollama'
// 메시지를 보내고 스트리밍 응답 반환
chat(messages: Message[], options: ChatOptions): AsyncIterator<string>;
// 도구 호출 지원 여부
supportTools(): boolean;
// 설정 초기화
initialize(config: ProviderConfig): void;
}
모든 Provider는 이 인터페이스만 구현하면 시스템에 연결할 수 있습니다. 시스템 시작 시 자동으로 스캔하고 등록합니다.
// 플러그인 등록 메커니즘
class ProviderRegistry {
private providers = new Map<string, LLMProvider>();
register(provider: LLMProvider) {
this.providers.set(provider.name, provider);
}
get(name: string): LLMProvider {
return this.providers.get(name);
}
}
// 자동 발견과 등록
const registry = new ProviderRegistry();
registry.register(new AnthropicProvider());
registry.register(new OpenAIProvider());
registry.register(new OllamaProvider());
이 설계의 장점은 명확합니다. 새 모델을 쓰고 싶다면 Provider 구현 클래스를 작성하고 등록하면 됩니다. 핵심 코드는 전혀 고칠 필요가 없습니다.
주요 Provider의 차이
인터페이스는 통합되었지만 Provider마다 구현 세부 사항은 꽤 다릅니다. 제가 겪었던 함정을 몇 가지 공유합니다.
Anthropic Provider(Claude)
- 스트리밍 응답을 네이티브로 지원합니다(
stream: true). - Tool Use 형식이 특수합니다(
tools배열로 감싸야 함). - 컨텍스트 윈도우가 큽니다(Claude 3.5는 200k tokens까지 가능).
OpenAI Provider(ChatGPT)
- Function Calling과 Tool Use가 서로 다른 API입니다(구버전은 functions, 신버전은 tools).
- 스트리밍 응답은 delta 조각으로 오기 때문에 직접 이어 붙여야 합니다.
- rate limit이 엄격합니다(RPM/TPM 모두 제어해야 함).
Ollama Provider(로컬 모델)
- API 키가 필요 없고 로컬 서비스로 HTTP 호출을 보냅니다.
- 성능이 하드웨어 영향을 크게 받습니다(CPU 추론은 느리며 GPU가 필요합니다).
- 모델마다 tool 지원이 일관되지 않습니다(llama3는 지원하지만 qwen은 지원하지 않을 수 있음).
저는 예전에 Ollama로 로컬 Llama3를 돌리려 했는데, 도구 호출 형식이 Claude와 완전히 달라 한참을 맞춘 뒤에야 제대로 연결할 수 있었습니다.
Tool Use 메커니즘 자세히 보기
Tool Use(도구 호출)는 LLM 계층의 핵심 기능 중 하나입니다. 간단히 말하면 AI가 “함수를 호출”할 수 있게 해주는 기능입니다.
예를 들어 “서울은 지금 몇 시야?”라고 물으면 AI는 다음처럼 동작합니다.
get_current_time도구 호출이 필요하다고 판단합니다.- 도구 호출 요청을 반환합니다:
{"name": "get_current_time", "args": {"city": "서울"}} - OpenClaw가 도구를 실행하고 결과를 반환합니다:
{"time": "2026-02-05 20:30"} - AI가 결과를 바탕으로 답합니다: “서울은 지금 저녁 8시 30분입니다.”
OpenClaw의 도구 등록 메커니즘은 다음과 같습니다.
// 도구 정의
const tools = [
{
name: 'get_current_time',
description: '지정한 도시의 현재 시간 가져오기',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: '도시 이름' }
},
required: ['city']
}
}
];
// 도구 실행
async function executeTool(toolName, args) {
const handlers = {
'get_current_time': (args) => {
// 실제 구현에서는 API를 호출할 수 있습니다
return { time: new Date().toLocaleString('ko-KR', { timeZone: 'Asia/Seoul' }) };
}
};
return handlers[toolName](args);
}
중요한 주의: 도구 실행에는 샌드박스 격리가 필요합니다. 그렇지 않으면 AI가 rm -rf / 실행을 요구했을 때 정말 위험해질 수 있습니다. OpenClaw에는 내장 권한 제어가 있어 미리 정의된 도구만 호출할 수 있습니다.
실전: OpenClaw 아키텍처 확장하기
이론은 여기까지 보고, 이제 실전으로 넘어가겠습니다. Discord Channel 개발과 Kimi Provider 개발, 두 가지 완성 예제를 공유하겠습니다.
커스텀 Channel 개발: Discord 연결
Discord의 메시지 메커니즘은 WhatsApp과 조금 다릅니다. WebSocket으로 메시지를 받고 REST API로 메시지를 보냅니다.
1단계: Channel 인터페이스 구현
import { Client, GatewayIntentBits } from 'discord.js';
class DiscordChannel implements Channel {
private client: Client;
private gateway: Gateway; // OpenClaw의 Gateway 인스턴스
async start() {
// Discord 클라이언트 초기화
this.client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.DirectMessages
]
});
// 메시지 이벤트 수신
this.client.on('messageCreate', async (msg) => {
if (msg.author.bot) return; // 봇 메시지 무시
// 표준 형식으로 변환
const standardMsg = this.adaptMessage(msg);
// Gateway에 처리 위임
const response = await this.gateway.handleMessage(standardMsg);
// 답장 전송
await msg.reply(response.content);
});
// 로그인
await this.client.login(process.env.DISCORD_TOKEN);
}
adaptMessage(discordMsg): StandardMessage {
return {
userId: discordMsg.author.id,
channelId: 'discord',
content: discordMsg.content,
timestamp: discordMsg.createdTimestamp,
metadata: {
guildId: discordMsg.guildId,
channelType: discordMsg.channel.type
}
};
}
async sendMessage(userId: string, content: string) {
const user = await this.client.users.fetch(userId);
await user.send(content);
}
}
2단계: OpenClaw에 등록
config.json에 추가합니다.
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_DISCORD_BOT_TOKEN",
"dmPolicy": "open"
}
}
}
시작 스크립트에서 등록합니다.
import { DiscordChannel } from './channels/discord';
const gateway = new Gateway(config);
const discordChannel = new DiscordChannel(gateway, config.channels.discord);
gateway.registerChannel('discord', discordChannel);
await discordChannel.start();
3단계: 테스트
- Discord 개발자 플랫폼에서 Bot을 만들고 Token을 가져옵니다.
- Bot을 자신의 서버에 초대합니다.
- OpenClaw를 시작하고 Bot에게 1:1 메시지를 보냅니다.
- 로그를 확인해 메시지 흐름이 정상인지 봅니다.
제가 실제로 개발하면서 만난 함정은 Discord의 권한 시스템이 꽤 복잡하다는 점이었습니다. Bot에 Send Messages와 Read Message History 권한이 있는지 꼭 확인해야 합니다. 그렇지 않으면 메시지를 보낼 수 없습니다.
커스텀 Provider 개발: Kimi 연결
Kimi(문샷 AI 모델)의 API는 OpenAI와 비슷하지만 세부 사항이 조금 다릅니다.
Provider 구현:
class KimiProvider implements LLMProvider {
name = 'kimi';
private apiKey: string;
private baseURL = 'https://api.moonshot.cn/v1';
initialize(config: ProviderConfig) {
this.apiKey = config.apiKey;
}
async *chat(messages: Message[], options: ChatOptions) {
const response = await fetch(`${this.baseURL}/chat/completions`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: options.model || 'moonshot-v1-8k',
messages: messages.map(m => ({
role: m.role,
content: m.content
})),
stream: true,
temperature: options.temperature || 0.7
})
});
// 스트리밍 응답 처리
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6);
if (data === '[DONE]') continue;
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) {
yield content;
}
}
}
}
}
supportTools(): boolean {
return false; // Kimi는 아직 Function Calling을 지원하지 않습니다
}
}
Provider 등록:
const registry = new ProviderRegistry();
registry.register(new KimiProvider());
// 사용 설정
const config = {
llm: {
provider: 'kimi',
apiKey: process.env.KIMI_API_KEY,
model: 'moonshot-v1-32k'
}
};
겪었던 문제:
- Kimi의 스트리밍 응답 형식은 OpenAI와 완전히 같아서 그대로 참고할 수 있습니다.
- 하지만 오류 처리는 다릅니다. 타임아웃 때 표준 오류 코드가 돌아오지 않아 별도 처리가 필요합니다.
- 현재는 Function Calling을 지원하지 않습니다. 애플리케이션이 도구 호출에 의존한다면 Kimi는 사용할 수 없습니다.
성능 최적화 실전
개발해서 실행되는 것은 첫 단계일 뿐입니다. 성능 최적화가 더 큰 일입니다. 제가 써본 최적화 포인트 몇 가지를 공유합니다.
Session 캐시 최적화
기본 Session은 메모리에 저장되므로 재시작하면 사라집니다. Redis를 연결할 수 있습니다.
class RedisSessionStore {
private redis: Redis;
async get(userId: string, channelId: string): Promise<Session> {
const key = `session:${channelId}:${userId}`;
const data = await this.redis.get(key);
return data ? JSON.parse(data) : null;
}
async set(session: Session) {
const key = `session:${session.channelId}:${session.userId}`;
await this.redis.setex(key, 3600, JSON.stringify(session)); // 1시간 만료
}
}
메시지 큐 튜닝
고동시성 상황에서는 메모리 큐만으로 부족할 수 있습니다. Bull(Redis 기반 작업 큐)로 바꿀 수 있습니다.
import Queue from 'bull';
const messageQueue = new Queue('openclaw-messages', {
redis: { host: 'localhost', port: 6379 }
});
messageQueue.process(10, async (job) => { // 최대 10개 동시 실행
const { session, message } = job.data;
return await gateway.processMessage(session, message);
});
동시 연결 수 제어
LLM API에는 보통 rate limit이 있습니다(예: OpenAI의 60 RPM). p-limit 라이브러리로 동시성을 제어할 수 있습니다.
import pLimit from 'p-limit';
const limit = pLimit(10); // 최대 10개 동시 요청
const tasks = messages.map(msg =>
limit(() => provider.chat(msg))
);
await Promise.all(tasks);
최적화 효과 비교(제가 직접 측정한 데이터):
- 최적화 전: 동시 요청 100개, 평균 응답 시간 8초, 실패율 15%
- 최적화 후: 동시 요청 100개, 평균 응답 시간 3초, 실패율<1%
정리
Gateway에서 Channel, 다시 LLM까지 이어지는 OpenClaw의 3계층 아키텍처 설계는 정말 명확합니다. 각 계층은 자기 일만 하고 책임 경계가 분명해서 확장하기가 특히 편합니다.
이 아키텍처를 이해한 뒤로 저는 새 기능 개발이 훨씬 쉬워졌습니다. 새 플랫폼을 추가하고 싶다면 Channel Adapter를 작성하면 됩니다. 모델을 바꾸고 싶다면 Provider 인터페이스를 구현하면 됩니다. 성능을 최적화하고 싶다면 병목이 어느 계층에 있는지 알 수 있으므로 그 지점을 집중적으로 튜닝하면 됩니다.
OpenClaw를 깊게 커스터마이즈하려는 분이라면 먼저 소스 코드를 클론한 뒤, 이 글의 흐름을 따라 한 번 읽어보는 것을 추천합니다. 특히 Gateway의 Session 관리, Channel의 라우팅 로직, Provider의 등록 메커니즘은 핵심 중의 핵심입니다.
이 세 부분을 이해하면 더 이상 “문서를 보고 설정을 따라 치는 사람”이 아니라 시스템을 실제로 이해하고 마음대로 확장하고 최적화할 수 있는 사람이 됩니다.
다음 단계로는 간단한 커스텀 Channel(예: 기업 WeChat, Lark)을 하나 개발해보세요. 직접 한 번 해보면 이해가 훨씬 깊어집니다. OpenClaw의 오픈소스 커뮤니티도 활발하니 문제가 생기면 GitHub Issues에서 논의할 수 있습니다.
다음에 읽을 글
- OpenClaw 설정 자세히 보기: openclaw.json 완전 가이드
- OpenClaw 멀티 플랫폼 설정 실전: 13개 Channel 통합 관리
- OpenClaw 2026 설치 가이드: 처음부터 배포하기
OpenClaw 커스텀 Channel 개발 전체 흐름
처음부터 커스텀 Channel을 개발하고 OpenClaw 시스템에 통합하는 과정
⏱️ Estimated time: 2 hr
- 1
Step 1: Channel 인터페이스 규격 이해하기
Channel 인터페이스는 플랫폼 어댑터가 반드시 구현해야 하는 메서드를 정의합니다.
핵심 메서드:
• start(): Channel을 시작하고 플랫폼 메시지를 수신합니다(webhook 또는 WebSocket).
• sendMessage(userId, content): 플랫폼으로 메시지를 전송합니다.
• adaptMessage(rawMessage): 플랫폼 메시지를 StandardMessage 형식으로 변환합니다.
StandardMessage 형식:
• userId: string(통합 사용자 ID)
• channelId: string(Channel 식별자)
• content: string(메시지 내용)
• timestamp: number(타임스탬프)
• metadata: any(플랫폼별 데이터)
라우팅 제어 메서드:
• shouldRespond(message): 해당 메시지에 응답할지 판단합니다.
• checkDmPolicy(userId): 1:1 채팅 정책을 확인합니다.
• checkMention(message): 그룹 채팅의 @ 호출 여부를 확인합니다.
참고 구현: src/channels/whatsapp.ts 또는 src/channels/telegram.ts - 2
Step 2: Channel 클래스를 만들고 인터페이스 구현하기
src/channels/ 디렉터리에 새 파일을 만듭니다(예: discord.ts).
typescript
class DiscordChannel implements Channel {
private client: Client;
private gateway: Gateway;
constructor(gateway: Gateway, config: ChannelConfig) {
this.gateway = gateway;
this.config = config;
}
async start() {
// Discord 클라이언트 초기화
this.client = new Client({ intents: [...] });
// 메시지 이벤트 수신
this.client.on('messageCreate', async (msg) => {
const standardMsg = this.adaptMessage(msg);
const response = await this.gateway.handleMessage(standardMsg);
await msg.reply(response.content);
});
await this.client.login(this.config.token);
}
adaptMessage(msg): StandardMessage {
return {
userId: msg.author.id,
channelId: 'discord',
content: msg.content,
timestamp: msg.createdTimestamp,
metadata: { guildId: msg.guildId }
};
}
async sendMessage(userId: string, content: string) {
const user = await this.client.users.fetch(userId);
await user.send(content);
}
}
핵심 포인트:
• 플랫폼 SDK 초기화는 start() 메서드에 둡니다.
• 수신 메시지는 StandardMessage 형식으로 변환해야 합니다.
• 메시지 전송은 플랫폼별 API 호출을 처리해야 합니다.
• 오류 처리와 로그 기록은 빠뜨리면 안 됩니다. - 3
Step 3: 라우팅 규칙과 권한 제어 구현하기
비즈니스 요구에 맞춰 메시지 필터링 로직을 구현합니다.
dmPolicy 구현:
• pairing 모드: 이미 페어링된 사용자 목록을 유지하고 목록 안의 사용자에게만 응답합니다.
• allowlist 모드: 사용자 ID가 허용 목록에 있는지 확인합니다.
• open 모드: 모든 사용자에게 응답합니다.
• disabled 모드: 모든 1:1 채팅을 거부합니다.
typescript
shouldRespond(message): boolean {
// 1:1 채팅 정책 확인
if (message.metadata.channelType === 'DM') {
return this.checkDmPolicy(message.userId);
}
// 그룹 채팅 @ 확인
if (message.metadata.channelType === 'GROUP') {
return this.checkMention(message);
}
return false;
}
mentionGating 구현(그룹 채팅 트리거):
• 메시지에 @봇이 포함되어 있는지 확인합니다.
• 플랫폼마다 mention 형식이 다릅니다(Discord는 <@botId>, Telegram은 @username).
• true를 반환하면 응답하고, false를 반환하면 무시합니다. - 4
Step 4: 설정 파일과 등록
1. config.json에 Channel 설정을 추가합니다.
json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"dmPolicy": "open",
"mentionGating": true
}
}
}
2. 시작 스크립트에서 Channel을 등록합니다.
typescript
import { DiscordChannel } from './channels/discord';
const gateway = new Gateway(config);
const discordChannel = new DiscordChannel(
gateway,
config.channels.discord
);
// Gateway에 등록
gateway.registerChannel('discord', discordChannel);
// Channel 시작
await discordChannel.start();
3. 환경 변수 설정:
• 민감한 정보(Token, 키)는 .env 파일에 둡니다.
• dotenv 라이브러리로 로드합니다: require('dotenv').config() - 5
Step 5: 테스트와 디버깅
테스트 흐름:
1. 로컬 개발 테스트:
• webhook 방식 플랫폼에는 ngrok으로 로컬 서비스를 외부에 노출합니다.
• 플랫폼 webhook이 ngrok URL을 바라보도록 설정합니다.
• OpenClaw를 시작하고 로그를 확인합니다.
2. 메시지 흐름 검증:
• 테스트 메시지를 보내 start()의 메시지 리스너가 트리거되는지 확인합니다.
• adaptMessage() 변환이 올바른지 확인합니다.
• Gateway.handleMessage()가 호출되는지 검증합니다.
• sendMessage()가 답장을 성공적으로 보내는지 확인합니다.
3. 라우팅 규칙 테스트:
• 1:1 채팅 정책(pairing/allowlist/open)을 테스트합니다.
• 그룹 채팅 @ 트리거(@가 있을 때와 없을 때의 동작)를 테스트합니다.
• 허용 목록/차단 목록 기능을 테스트합니다.
4. 예외 처리 테스트:
• 네트워크 타임아웃을 시뮬레이션합니다.
• Token 만료를 시뮬레이션합니다.
• 비정상 메시지 형식을 시뮬레이션합니다.
디버깅 팁:
• 핵심 위치에 console.log()를 추가하거나 debug 라이브러리를 사용합니다.
• Gateway 로그를 확인해 메시지가 도착했는지 봅니다.
• 플랫폼이 제공하는 테스트 도구(예: Discord Bot Dashboard)를 사용합니다.
• 상세 로그 모드 실행: DEBUG=openclaw:* openclaw gateway(전역 설치에서 흔히 쓰는 방식이며, 공식 소스 저장소에서 로컬 개발할 때는 저장소 문서를 기준으로 삼으세요) - 6
Step 6: 성능 최적화와 출시 준비
최적화 체크리스트:
1. 연결 관리:
• 하트비트 감지를 구현합니다(연결이 죽은 상태로 남는 문제 방지).
• 자동 재연결 메커니즘을 추가합니다(지수 백오프).
• 정상 종료를 처리합니다(SIGTERM 신호).
2. 오류 처리:
• 가능한 모든 예외를 포착합니다.
• 메시지 재시도 메커니즘을 구현합니다(최대 3회).
• 오류 로그를 파일이나 모니터링 시스템에 기록합니다.
3. 성능 최적화:
• 메시지를 배치 처리합니다(API 호출 감소).
• 연결 풀을 사용합니다(데이터베이스/Redis).
• 속도 제한을 제어합니다(플랫폼 rate limit 방지).
4. 모니터링과 로그:
• 메시지 처리 시간을 기록합니다.
• 성공률과 실패율을 집계합니다.
• 알림 임계값을 설정합니다(실패율>5%일 때 알림).
출시 전 점검:
• 부하 테스트(100명 이상 동시 사용자 시뮬레이션)
• 메모리 누수 점검(장시간 실행 테스트)
• 설정 백업과 롤백 방안
• 운영 문서 작성(시작, 중지, 장애 대응)
FAQ
왜 per-channel-peer 세션 격리를 써야 하나요? 모든 플랫폼이 하나의 Session을 공유하면 안 되나요?
컨텍스트 격리: 같은 사용자가 WhatsApp에서는 기술 문제를 논의하고 Telegram에서는 날씨를 물어본다고 해봅시다. Session을 공유하면 두 대화가 섞입니다. AI가 기술 토론의 컨텍스트를 날씨 질문에 가져가 관련 없는 답변을 만들 수 있습니다.
보안 격리: 플랫폼마다 권한 검증 방식이 다릅니다. Session을 공유하면 권한 우회가 생길 수 있습니다. 예를 들어 사용자가 WhatsApp에서는 인증을 통과했지만 Telegram 계정은 위조되었을 수 있으므로, 분리해 격리하는 쪽이 더 안전합니다.
성능 관점: 각 Channel의 Session을 독립적으로 저장하면 서로 막히지 않고 여러 플랫폼 메시지를 병렬 처리할 수 있습니다.
정말로 플랫폼 간 컨텍스트 공유가 필요하다면 Session 계층에서 합치는 대신 애플리케이션 계층에서 사용자 계정 연결을 구현하는 편이 좋습니다.
커스텀 Provider를 개발할 때 스트리밍 응답을 지원하지 않는 모델은 어떻게 처리하나요?
방법 1: 가짜 스트리밍으로 감싸기(추천)
async *chat(messages) {
const response = await fetch(apiUrl, { ... }); // 비스트리밍 요청
const result = await response.json();
yield result.content; // 전체 내용을 한 번에 반환
}
방법 2: 조각으로 나눠 스트리밍처럼 보이게 하기
const fullText = await getNonStreamResponse();
const chunkSize = 50;
for (let i = 0; i < fullText.length; i += chunkSize) {
yield fullText.slice(i, i + chunkSize);
await sleep(100); // 지연 시뮬레이션
}
방법 1은 단순하고 직접적이며, 사용자 경험은 "기다린 뒤 완성된 답변을 한 번에 받는" 형태입니다. 방법 2는 타자기 효과를 흉내 낼 수 있지만 복잡도가 올라갑니다. 실제 요구에 맞춰 선택하세요.
Gateway의 메시지 큐가 가득 차면 어떻게 되나요? 메시지 유실은 어떻게 피하나요?
기본 동작: OpenClaw의 메모리 큐에는 용량 제한이 있습니다(기본 1000개). 이를 넘으면 새 메시지는 거부되고 사용자에게 "시스템이 바쁩니다" 오류가 반환됩니다.
메시지 유실을 피하는 방법:
1. 영속 큐(추천):
Bull이나 RabbitMQ 같은 영속 메시지 큐를 사용하면 서비스가 재시작되어도 메시지가 사라지지 않습니다.
2. 큐 용량 늘리기:
config.json에서 maxQueueSize: 5000으로 설정할 수 있지만 메모리 사용량에 주의해야 합니다.
3. 속도 제한 + 안내:
Channel 계층에서 rate limit을 구현하고, 속도를 넘으면 사용자에게 "잠시 후 다시 시도해 주세요"라고 안내해 메시지가 대량으로 쌓이는 일을 피합니다.
4. 우선순위 큐:
중요 사용자(VIP)의 메시지를 먼저 처리하고 일반 사용자는 대기시킵니다.
운영 환경에서는 Bull + Redis 조합을 권장합니다. 영속화와 높은 동시성을 함께 지원할 수 있습니다.
Gateway와 Channel 사이의 메시지 흐름은 어떻게 디버깅하나요? 메시지를 보냈는데 응답이 없는 것 같습니다.
1. 상세 로그 켜기:
DEBUG=openclaw:* openclaw gateway
전역 npm 설치 후에는 위 방식으로 포그라운드 디버깅을 자주 합니다. 공식 소스 저장소에서 로컬 개발한다면 해당 저장소의 README/스크립트를 기준으로 삼고 npm start를 추측하지 마세요.
이 설정은 메시지 수신, 변환, 처리, 전송을 포함한 각 모듈 로그를 출력합니다.
2. 핵심 지점 확인:
• Channel.adaptMessage(): 변환된 StandardMessage를 출력해 형식이 올바른지 확인합니다.
• Gateway.handleMessage(): 받은 메시지와 Session 상태를 출력합니다.
• Provider.chat(): LLM에 보내는 컨텍스트를 출력합니다.
• Channel.sendMessage(): 최종 전송 내용을 출력합니다.
3. 중단점 디버깅:
VS Code에서 launch.json을 설정하고 중단점을 걸어 한 단계씩 디버깅합니다.
4. 흔한 문제 확인:
• shouldRespond()가 false를 반환함: 메시지가 라우팅 규칙에 의해 걸러졌습니다.
• Session을 찾지 못함: userId 또는 channelId가 일치하지 않습니다.
• LLM 호출 실패: API 키, 네트워크, rate limit을 확인합니다.
5. 테스트 도구 사용:
단위 테스트를 작성해 메시지 입력을 시뮬레이션하고 각 단계의 출력을 검증합니다.
개발 환경에서는 pino-pretty로 로그 출력을 보기 좋게 만들고, 운영 환경에서는 후속 분석을 위해 구조화 로그(JSON 형식)를 쓰는 것을 추천합니다.
Provider의 Tool Use 기능에서 AI가 파일 삭제 같은 위험한 작업을 실행하지 못하게 하려면 어떻게 해야 하나요?
1. 허용 목록 메커니즘(가장 중요):
안전한 도구만 등록하고 파일 시스템 작업, 네트워크 요청 같은 위험한 도구 등록을 금지합니다.
const safeTool = {
name: 'get_weather',
description: '날씨 조회',
handler: getWeatherData // 안전한 읽기 전용 작업
};
2. 인자 검증:
도구 인자를 엄격하게 검증하고 비정상 입력을 거부합니다.
function validateArgs(args) {
if (args.city.includes('<script>')) { // XSS 방어
throw new Error('Invalid input');
}
}
3. 샌드박스 실행(고급):
vm2 또는 isolated-vm을 사용해 격리 환경에서 도구 코드를 실행합니다.
4. 권한 등급:
사용자별로 도구 호출 권한을 다르게 둡니다. 관리자는 고급 도구를 실행할 수 있고 일반 사용자는 기본 도구만 사용할 수 있게 합니다.
5. 감사 로그:
모든 도구 호출을 기록합니다(누가, 언제, 무엇을, 어떤 인자로 호출했는지). 추적과 감사가 가능해집니다.
OpenClaw는 기본적으로 미리 정의된 도구 호출만 허용하고 동적 코드 실행을 지원하지 않습니다. 이 점만으로도 대부분의 위험을 피할 수 있습니다. 도구를 확장한다면 반드시 보안성을 신중하게 평가하세요.
여러 Channel이 같은 사용자의 메시지를 동시에 받으면 Gateway는 동시성 충돌을 어떻게 피하나요?
Session 잠금 메커니즘:
각 Session은 메시지를 처리할 때 잠금을 겁니다. 같은 Session의 메시지는 직렬로 처리되고, 다른 Session은 병렬로 처리됩니다.
의사 코드 구현:
async handleMessage(session, message) {
const lock = await this.acquireLock(session.id);
try {
// 메시지 처리
await this.processMessage(session, message);
} finally {
await lock.release();
}
}
실제 상황 예:
사용자가 WhatsApp과 Telegram에서 동시에 봇에게 메시지를 보내도 per-channel-peer 격리 덕분에 두 개의 독립 Session으로 처리됩니다. 병렬 처리가 가능하고 충돌하지 않습니다.
같은 Channel에서 동시에 메시지가 들어오는 경우(예: 사용자가 빠르게 세 메시지를 연속 전송)는 메시지 큐에 들어가 FIFO 순서로 처리됩니다.
분산 배포에서의 처리:
OpenClaw를 여러 인스턴스로 배포한다면 Redis로 분산 잠금을 구현해야 합니다(redlock 알고리즘).
import Redlock from 'redlock';
const lock = await redlock.lock(session.id, 5000); // 5초 타임아웃
이렇게 하면 여러 인스턴스가 있더라도 같은 Session은 한 인스턴스만 처리하도록 보장할 수 있습니다.
6분 읽기 · 게시일: 2026년 2월 5일 · 수정일: 2026년 10월 21일
OpenClaw 배포와 실전
검색으로 들어왔다면 같은 시리즈의 이전 글이나 다음 글로 이동하는 것이 가장 빠릅니다.
이전
OpenClaw 멀티 에이전트 라우팅 설정 방법: 업무·개인·실험 환경 분리
OpenClaw 멀티 에이전트 라우팅으로 업무, 개인, 실험 환경을 분리합니다. 5가지 활용 사례와 work/personal 에이전트 설정, 격리 검증, 운영 팁을 설명합니다.
36편 중 15편
다음
OpenClaw WhatsApp 연동 완벽 가이드: 설정부터 실전 활용까지
OpenClaw WhatsApp 통합 과정을 상세히 설명합니다. 세 가지 연동 방법, 권한 설정, 메시지 라우팅, QR 코드 스캔 절차와 자주 발생하는 문제 해결법을 통해 WhatsApp에 AI 어시스턴트를 빠르게 배포할 수 있습니다.
36편 중 17편



댓글
GitHub로 로그인하여 댓글을 남기세요