본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 8장: 프로덕션 듀얼 레이어 아키텍처
2026년 3월 12일·AI / ML·

8장: 프로덕션 듀얼 레이어 아키텍처

Hot Path와 Cold Path를 결합한 듀얼 레이어 메모리 아키텍처의 설계, 하이브리드 검색, 메모리 라우팅, 비용-지연시간 최적화 전략을 다룹니다.

16분709자10개 섹션
aillmai-agent
공유
agent-memory8 / 10
12345678910
이전7장: 메모리 프레임워크 비교와 선택다음9장: 프로젝트 메모리와 코딩 에이전트

학습 목표

  • 듀얼 레이어 아키텍처의 구조와 필요성을 이해한다
  • Hot Path와 Cold Path의 역할을 구분하고 설계할 수 있다
  • 하이브리드 검색과 메모리 라우팅을 구현할 수 있다
  • 비용과 지연시간 최적화를 위한 캐싱 전략을 적용할 수 있다

듀얼 레이어 아키텍처란

지금까지 다룬 단기 메모리, 장기 메모리, 에피소딕 메모리, 지식 그래프를 하나의 시스템으로 통합하는 것이 듀얼 레이어 아키텍처(Dual-Layer Architecture)입니다.

두 개의 경로로 구성됩니다.

  • Hot Path: 즉시 사용 가능한 컨텍스트. 현재 대화의 최근 메시지, 요약, 핵심 사용자 정보
  • Cold Path: 외부 저장소에서 필요 시 검색하는 메모리. 벡터 DB, 지식 그래프, 에피소딕 메모리

Hot Path 설계

Hot Path는 매 턴마다 항상 컨텍스트에 포함되는 정보입니다. 빠른 접근이 보장되어야 하며, 토큰 예산을 효율적으로 사용해야 합니다.

구성 요소

hot-path.ts
typescript
interface HotPathContext {
  systemPrompt: string;          // 에이전트 지시사항
  userProfile: UserProfileCore;  // 사용자 핵심 정보
  conversationSummary: string;   // 현재까지 대화 요약
  recentMessages: Message[];     // 최근 N개 메시지
  activeGoals: string[];         // 현재 진행 중인 목표
}
 
interface UserProfileCore {
  name: string;
  preferences: string[];    // 핵심 선호도 (최대 5개)
  expertise: string[];      // 전문 분야
  communicationStyle: string;
}
 
function buildHotPath(
  session: Session,
  userProfile: UserProfileCore,
  tokenBudget: number
): HotPathContext {
  const systemPromptTokens = countTokens(session.systemPrompt);
  const profileTokens = countTokens(JSON.stringify(userProfile));
  
  let remainingBudget = tokenBudget - systemPromptTokens - profileTokens;
 
  // 대화 요약에 20% 할당
  const summaryBudget = Math.floor(remainingBudget * 0.2);
  const summary = truncateToTokens(
    session.conversationSummary, summaryBudget
  );
  remainingBudget -= countTokens(summary);
 
  // 나머지를 최근 메시지에 할당
  const recentMessages = selectRecentMessages(
    session.messages, remainingBudget
  );
 
  return {
    systemPrompt: session.systemPrompt,
    userProfile,
    conversationSummary: summary,
    recentMessages,
    activeGoals: session.activeGoals,
  };
}

Hot Path의 토큰 예산

128K 토큰 모델 기준으로 Hot Path의 예산 배분 예시입니다.

hot-path-budget.txt
text
전체 컨텍스트: 128,000 토큰
 
Hot Path (50%):  64,000 토큰
  - 시스템 프롬프트:    8,000 (6.25%)
  - 사용자 프로필:      2,000 (1.56%)
  - 대화 요약:         12,000 (9.38%)
  - 최근 메시지:       38,000 (29.69%)
  - 활성 목표:          4,000 (3.13%)
 
Cold Path (25%):  32,000 토큰
  - 벡터 검색 결과:    15,000
  - 그래프 탐색 결과:  10,000
  - 에피소딕 참조:      7,000
 
출력 예약 (25%):  32,000 토큰

Cold Path 설계

Cold Path는 필요할 때만 검색하여 컨텍스트에 추가하는 메모리입니다. 매 턴마다 검색하는 것이 아니라, 메모리 라우터가 필요하다고 판단할 때만 실행됩니다.

검색 트리거

cold-path-trigger.ts
typescript
interface ColdPathTrigger {
  type: "explicit" | "implicit" | "periodic";
  condition: string;
}
 
function shouldSearchColdPath(
  message: Message,
  conversationState: ConversationState
): ColdPathTrigger | null {
  const content = message.content.toLowerCase();
 
  // 명시적 트리거: 사용자가 과거 정보를 요청
  if (
    content.includes("이전에") ||
    content.includes("지난번") ||
    content.includes("기억") ||
    content.includes("전에 말한")
  ) {
    return { type: "explicit", condition: "user_requested_recall" };
  }
 
  // 암묵적 트리거: 현재 맥락으로는 답변이 어려운 경우
  if (conversationState.confidenceScore < 0.5) {
    return { type: "implicit", condition: "low_confidence" };
  }
 
  // 주기적 트리거: 새 주제가 시작될 때
  if (conversationState.topicChanged) {
    return { type: "periodic", condition: "topic_change" };
  }
 
  return null;
}

하이브리드 검색

Cold Path에서는 여러 소스를 병렬로 검색하고 결과를 통합합니다.

hybrid-retrieval.ts
typescript
interface RetrievalSource {
  name: string;
  search: (query: string, userId: string) => Promise<SearchResult[]>;
  weight: number;
  timeout: number; // 밀리초
}
 
async function hybridRetrieval(
  query: string,
  userId: string,
  sources: RetrievalSource[],
  maxResults: number = 10
): Promise<SearchResult[]> {
  // 모든 소스에서 병렬 검색 (타임아웃 적용)
  const searchPromises = sources.map((source) =>
    Promise.race([
      source.search(query, userId).then((results) =>
        results.map((r) => ({ ...r, sourceWeight: source.weight }))
      ),
      new Promise<SearchResult[]>((resolve) =>
        setTimeout(() => resolve([]), source.timeout)
      ),
    ])
  );
 
  const allResults = await Promise.all(searchPromises);
  const flatResults = allResults.flat();
 
  // 가중 점수로 통합 순위 결정
  return flatResults
    .map((r) => ({
      ...r,
      finalScore: r.similarity * r.sourceWeight,
    }))
    .sort((a, b) => b.finalScore - a.finalScore)
    .slice(0, maxResults);
}
 
// 사용 예시
const sources: RetrievalSource[] = [
  {
    name: "vector_db",
    search: vectorMemory.search,
    weight: 0.5,
    timeout: 2000,
  },
  {
    name: "knowledge_graph",
    search: graphMemory.search,
    weight: 0.3,
    timeout: 3000,
  },
  {
    name: "episodic",
    search: episodicMemory.search,
    weight: 0.2,
    timeout: 2000,
  },
];
Warning

Cold Path 검색에는 반드시 타임아웃을 설정하세요. 외부 서비스 장애 시 전체 응답이 지연되는 것을 방지합니다. 타임아웃이 발생하면 해당 소스 없이 Hot Path 정보만으로 응답을 생성해야 합니다.


메모리 라우터

메모리 라우터(Memory Router)는 사용자 메시지를 분석하여 어떤 메모리 소스를 검색할지 결정하는 구성 요소입니다.

memory-router.ts
typescript
type MemorySourceType = "vector" | "graph" | "episodic" | "none";
 
interface RoutingDecision {
  sources: MemorySourceType[];
  query: string;           // 검색에 사용할 쿼리 (원본과 다를 수 있음)
  priority: "high" | "normal" | "low";
}
 
async function routeMemoryRequest(
  message: Message,
  conversationContext: string,
  llm: LLMClient
): Promise<RoutingDecision> {
  const response = await llm.complete({
    messages: [{
      role: "system",
      content: `사용자 메시지를 분석하여 어떤 메모리 소스를 검색해야 하는지 결정하세요.
 
사용 가능한 소스:
- vector: 사실, 선호도, 일반 지식 검색
- graph: 엔티티 간 관계, 조직 구조, 프로젝트 연관성
- episodic: 과거 상호작용 경험, 이전 해결 사례
- none: 메모리 검색 불필요 (일반 대화, 인사 등)
 
JSON 형식으로 응답:
{"sources": [...], "query": "최적화된 검색 쿼리", "priority": "high|normal|low"}`,
    }, {
      role: "user",
      content: `현재 대화 맥락: ${conversationContext}\n\n사용자 메시지: ${message.content}`,
    }],
  });
 
  return JSON.parse(response.content);
}

경량 라우터

LLM 기반 라우터는 정확하지만 매 턴마다 추가 호출이 발생합니다. 비용을 줄이기 위해 규칙 기반 경량 라우터를 1차로 사용하고, 불확실한 경우에만 LLM 라우터를 사용하는 2단계 방식이 효과적입니다.

lightweight-router.ts
typescript
function lightweightRoute(message: Message): RoutingDecision | null {
  const content = message.content;
 
  // 인사, 감사 등 -> 메모리 불필요
  if (isGreetingOrAck(content)) {
    return { sources: [], query: "", priority: "low" };
  }
 
  // "누가", "어느 팀" 등 관계 질문 -> 그래프
  if (containsRelationalQuery(content)) {
    return { sources: ["graph"], query: content, priority: "high" };
  }
 
  // "지난번에", "이전에" 등 회고 -> 에피소딕
  if (containsRecallIndicator(content)) {
    return { sources: ["episodic"], query: content, priority: "high" };
  }
 
  // 판단 불가 -> null 반환하여 LLM 라우터로 위임
  return null;
}

캐싱 전략

반복적으로 접근되는 메모리에 대한 캐싱은 비용과 지연시간을 크게 줄입니다.

다계층 캐시

memory-cache.ts
typescript
interface MemoryCache {
  l1: Map<string, CachedResult>;  // 인메모리 (세션 내)
  l2: RedisClient;                 // Redis (세션 간, TTL 적용)
}
 
class CachedMemoryStore {
  private cache: MemoryCache;
  private coldStore: ColdPathStore;
 
  async search(
    query: string,
    userId: string
  ): Promise<SearchResult[]> {
    const cacheKey = this.buildCacheKey(query, userId);
 
    // L1 캐시 확인
    const l1Result = this.cache.l1.get(cacheKey);
    if (l1Result && !this.isExpired(l1Result)) {
      return l1Result.data;
    }
 
    // L2 캐시 확인
    const l2Result = await this.cache.l2.get(cacheKey);
    if (l2Result) {
      this.cache.l1.set(cacheKey, { data: l2Result, timestamp: Date.now() });
      return l2Result;
    }
 
    // Cold Path 검색
    const freshResult = await this.coldStore.search(query, userId);
 
    // 캐시 갱신
    this.cache.l1.set(cacheKey, { data: freshResult, timestamp: Date.now() });
    await this.cache.l2.set(cacheKey, freshResult, { ex: 3600 }); // 1시간 TTL
 
    return freshResult;
  }
 
  private buildCacheKey(query: string, userId: string): string {
    // 쿼리의 임베딩 해시를 키로 사용하여 유사 쿼리도 캐시 히트
    return `mem:${userId}:${hashEmbedding(query)}`;
  }
}
Info

캐시 무효화 정책이 중요합니다. 사용자가 새로운 선호도를 표현하면 관련 캐시를 즉시 무효화해야 합니다. 그렇지 않으면 오래된 정보가 계속 제공될 수 있습니다.


비용-지연시간 최적화

프로덕션에서 메모리 시스템의 비용과 지연시간은 핵심 운영 지표입니다.

비용 최적화 전략

cost-optimization.txt
text
1. 선택적 검색
   - 모든 턴에 Cold Path 검색을 실행하지 않음
   - 메모리 라우터로 필요한 경우만 검색
   - 예상 절감: 40-60% API 호출 감소
 
2. 임베딩 캐싱
   - 동일/유사 쿼리의 임베딩을 캐싱
   - 임베딩 API 호출 절감
   - 예상 절감: 30-50% 임베딩 비용 감소
 
3. 계층적 압축 (6장)
   - 오래된 메모리를 주기적으로 압축
   - 벡터 DB 저장 용량 절감
   - 검색 대상 감소로 쿼리 비용 절감
 
4. 토큰 예산 엄격 관리
   - Hot/Cold Path별 토큰 상한 설정
   - 예산 초과 시 우선순위 기반 절단

지연시간 최적화

Hot Path 구성과 Cold Path 검색을 병렬로 실행하면 전체 지연시간을 줄일 수 있습니다. Cold Path에 타임아웃을 설정하여 느린 소스가 전체를 지연시키지 않도록 합니다.


전체 아키텍처 통합

지금까지의 모든 요소를 통합한 전체 아키텍처입니다.

dual-layer-architecture.ts
typescript
class DualLayerMemorySystem {
  private hotPath: HotPathManager;
  private coldPath: ColdPathManager;
  private router: MemoryRouter;
  private cache: CachedMemoryStore;
  private compressor: MemoryCompressor;
 
  async processMessage(
    message: Message,
    session: Session
  ): Promise<UnifiedContext> {
    // 1. Hot Path 구성 (항상 실행)
    const hotContext = this.hotPath.build(session);
 
    // 2. 메모리 라우팅 결정
    const routing = await this.router.route(message, session);
 
    // 3. Cold Path 검색 (필요 시)
    let coldContext: SearchResult[] = [];
    if (routing.sources.length > 0) {
      coldContext = await this.cache.searchWithFallback(
        routing.query,
        session.userId,
        routing.sources
      );
    }
 
    // 4. 컨텍스트 통합 및 토큰 예산 조정
    const unified = this.assembleContext(
      hotContext, coldContext, session.tokenBudget
    );
 
    // 5. 백그라운드: 메모리 저장 및 압축
    this.backgroundTasks(message, session);
 
    return unified;
  }
 
  private backgroundTasks(message: Message, session: Session): void {
    // 비동기: 새 메모리 저장
    this.coldPath.storeIfRelevant(message, session);
 
    // 비동기: 주기적 압축 확인
    this.compressor.checkAndConsolidate(session.userId);
  }
}

정리

이번 장에서 다룬 듀얼 레이어 아키텍처의 핵심 내용을 정리합니다.

  • Hot Path는 항상 컨텍스트에 포함되는 즉시 가용 메모리(시스템 프롬프트, 요약, 최근 메시지, 사용자 프로필)입니다
  • Cold Path는 필요 시에만 외부 저장소에서 검색하는 메모리(벡터 DB, 지식 그래프, 에피소딕)입니다
  • 메모리 라우터는 어떤 소스를 검색할지 결정하여 불필요한 검색을 줄입니다
  • 하이브리드 검색은 여러 소스를 병렬로 검색하고 가중 점수로 결과를 통합합니다
  • 캐싱과 병렬 처리로 비용과 지연시간을 최적화합니다

다음 장 미리보기

9장에서는 프로젝트 메모리와 코딩 에이전트를 다룹니다. CLAUDE.md 기반 프로젝트 메모리, 코드베이스 컨텍스트 지속, 세션 간 학습, 팀 메모리 등 코딩 에이전트에 특화된 메모리 패턴을 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#ai#llm#ai-agent

관련 글

AI / ML

9장: 프로젝트 메모리와 코딩 에이전트

CLAUDE.md 기반 프로젝트 메모리, 코드베이스 컨텍스트 지속, 세션 간 학습, 팀 메모리 설계 패턴 등 코딩 에이전트에 특화된 메모리 시스템을 다룹니다.

2026년 3월 14일·18분
AI / ML

7장: 메모리 프레임워크 비교와 선택

Mem0, Zep, Letta, LangChain/LangGraph의 메모리 시스템을 상세 비교하고, 프로젝트 요구사항에 맞는 프레임워크 선택 의사결정 트리를 제시합니다.

2026년 3월 10일·16분
AI / ML

10장: 실전 프로젝트 — 에이전트 메모리 시스템 구축

Mem0와 Zep을 활용한 듀얼 레이어 메모리 시스템 구축, 메모리 압축 파이프라인, 성능 벤치마킹, 프로덕션 운영 체크리스트까지 실전 가이드를 제공합니다.

2026년 3월 16일·19분
이전 글7장: 메모리 프레임워크 비교와 선택
다음 글9장: 프로젝트 메모리와 코딩 에이전트

댓글

목차

약 16분 남음
  • 학습 목표
  • 듀얼 레이어 아키텍처란
  • Hot Path 설계
    • 구성 요소
    • Hot Path의 토큰 예산
  • Cold Path 설계
    • 검색 트리거
    • 하이브리드 검색
  • 메모리 라우터
    • 경량 라우터
  • 캐싱 전략
    • 다계층 캐시
  • 비용-지연시간 최적화
    • 비용 최적화 전략
    • 지연시간 최적화
  • 전체 아키텍처 통합
  • 정리
  • 다음 장 미리보기