AI가 드디어 기억한다 — LangGraph 메모리 완벽 가이드 [2026 실전 튜토리얼]

Written in

by

이 글은 시리즈 4편입니다.

  • 1편: 왜 Python이 2026년에도 압도적 1위인가
  • 2편: 나만의 AI 챗봇 만들기 — RAG 시스템 처음부터 배포까지
  • 3편: 혼자 일하는 AI는 이제 옛날 — LangGraph 멀티 에이전트
  • 4편: AI가 드디어 기억한다 — LangGraph 메모리 완벽 가이드 ← 지금 여기

📌 난이도: 중급 (3편까지 따라왔다면 충분합니다) ⏱️ 읽는 시간: 약 12분 / 실습 시간: 약 2시간 🛠️ 완성물: 대화를 기억하고 사용자 맞춤형으로 진화하는 AI 어시스턴트


지난 편에서 3인조 AI 팀을 만들었습니다.

리서처, 라이터, 팩트체커가 협력해서 꽤 쓸만한 결과물을 냈죠. 그런데 다음 날 다시 켜면 어떻게 될까요?

완전히 처음으로 돌아갑니다.

“어제 제 이름은 지수라고 했잖아요?” → “죄송합니다, 저는 그런 정보를 알지 못합니다.”

“지난번에 작성한 Python 글 스타일로 써줘” → “어떤 스타일을 원하시나요?”

사용자 입장에서 이건 그냥 검색창과 다를 게 없습니다. 진짜 어시스턴트라면 기억해야 합니다.

오늘은 LangGraph에 메모리를 붙여서, 진짜 의미의 “나만의 AI”를 만드는 방법을 다룹니다.


📊 목차

  1. 기억이 왜 중요한가 — 메모리 없는 AI의 한계
  2. LangGraph 메모리의 두 종류 — 단기 vs 장기
  3. 단기 메모리 구현 — MemorySaver (세션 내 기억)
  4. 중기 메모리 구현 — SQLiteSaver (로컬 영구 저장)
  5. 장기 메모리 구현 — PostgreSQL (프로덕션 수준)
  6. 장기 메모리 Store — 사용자 선호도 학습
  7. 멀티 유저 처리 — thread_id로 사용자 분리
  8. 실전 패턴: 맥락 요약으로 비용 절감
  9. 메모리 설계 체크리스트

1. 기억이 왜 중요한가

메모리가 없는 AI와 있는 AI의 차이를 실제로 느껴봅시다.

메모리 없는 AI:

[1일차] 사용자: "저는 백엔드 개발자고 Python을 주로 씁니다."
AI: "네, 알겠습니다!"
[2일차] 사용자: "제 수준에 맞게 설명해줘."
AI: "어떤 분야를 하시는지, 어떤 언어를 쓰시는지 알려주시면..."

메모리 있는 AI:

[1일차] 사용자: "저는 백엔드 개발자고 Python을 주로 씁니다."
AI: "기억해두겠습니다!"
[2일차] 사용자: "제 수준에 맞게 설명해줘."
AI: "Python 백엔드 개발자 기준으로 설명드릴게요. asyncio를 이미
아실 테니 그 맥락에서..."

차이가 느껴지시나요? 두 번째 AI가 비서처럼 동작하는 이유는 딱 하나입니다. 기억하기 때문입니다.

LangGraph는 기억을 두 층으로 나눠서 관리합니다.


2. LangGraph 메모리의 두 종류

LangGraph의 공식 메모리 설계는 명확합니다.

┌─────────────────────────────────────────────┐
│ LangGraph 메모리 구조 │
├─────────────────────┬───────────────────────┤
│ 단기 메모리 │ 장기 메모리 │
│ (Short-term) │ (Long-term) │
├─────────────────────┼───────────────────────┤
│ 현재 대화 스레드 내 │ 여러 세션에 걸쳐 유지 │
│ 자동으로 관리됨 │ 직접 저장/불러와야 함 │
│ 체크포인터가 담당 │ Store가 담당 │
│ 세션 끝나면 사라짐 │ 영구 보존 │
├─────────────────────┼───────────────────────┤
│ 예시: 이번 대화 내용 │ 예시: 사용자 이름, │
│ 현재 작업 상태 │ 선호 스타일, │
│ 에러 히스토리 │ 이전 프로젝트 │
└─────────────────────┴───────────────────────┘

체크포인터(Checkpointer) = 단기 메모리 담당. 현재 대화 흐름을 저장. 스토어(Store) = 장기 메모리 담당. 세션을 넘어서 사실과 선호도를 저장.

실제 프로덕션에서는 둘 다 써야 합니다. 지금부터 단계별로 구현합니다.


3. 단기 메모리 — MemorySaver (세션 내 기억)

가장 간단한 것부터 시작합니다. MemorySaver는 메모리(RAM)에 저장하는 체크포인터입니다. 서버가 꺼지면 사라지지만, 한 대화 내에서는 모든 것을 기억합니다.

python

# memory_basic.py
import os
from dotenv import load_dotenv
from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.memory import MemorySaver
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
load_dotenv()
# ── 상태 정의 ──────────────────────────────────────
class ChatState(TypedDict):
messages: Annotated[list, add_messages]
# add_messages: 메시지를 덮어쓰지 않고 누적시키는 리듀서
# ── LLM ──────────────────────────────────────────
llm = ChatAnthropic(
model="claude-sonnet-4-20250514",
api_key=os.getenv("ANTHROPIC_API_KEY"),
max_tokens=1024
)
# ── 챗봇 노드 ────────────────────────────────────
def chatbot_node(state: ChatState) -> dict:
system = SystemMessage(content="""
당신은 친절하고 기억력이 좋은 AI 어시스턴트입니다.
이전 대화 내용을 항상 참고해서 맥락에 맞게 대답하세요.
""")
response = llm.invoke([system] + state["messages"])
return {"messages": [response]}
# ── 그래프 조립 ──────────────────────────────────
builder = StateGraph(ChatState)
builder.add_node("chatbot", chatbot_node)
builder.set_entry_point("chatbot")
builder.add_edge("chatbot", END)
# 핵심: MemorySaver 연결
memory = MemorySaver()
graph = builder.compile(checkpointer=memory)
# ── 대화 함수 ────────────────────────────────────
def chat(user_input: str, thread_id: str = "default") -> str:
"""
thread_id: 대화 세션을 구분하는 ID
같은 thread_id면 이전 대화를 기억합니다.
"""
config = {"configurable": {"thread_id": thread_id}}
result = graph.invoke(
{"messages": [HumanMessage(content=user_input)]},
config=config
)
return result["messages"][-1].content
# ── 테스트 ──────────────────────────────────────
if __name__ == "__main__":
tid = "user-test-001" # ID 대화 이력 묶임
print("=" * 50)
r1 = chat("안녕하세요! 저는 지수라고 해요. Python 개발자입니다.", tid)
print(f"AI: {r1}\n")
r2 = chat("제 이름이 뭐였죠?", tid)
print(f"AI: {r2}\n")
r3 = chat("제 직업도 기억하고 있나요?", tid)
print(f"AI: {r3}\n")

실행하면 AI가 이름과 직업을 같은 세션 내에서 기억합니다.

⚠️ MemorySaver의 한계 서버를 재시작하면 모든 대화가 사라집니다. 개발·테스트에는 충분하지만 실제 서비스에는 쓸 수 없습니다.


4. 중기 메모리 — SQLiteSaver (로컬 영구 저장)

서버를 껐다 켜도 기억이 유지돼야 한다면 SQLite를 씁니다. 파일 하나(.db)에 저장하기 때문에 설치도 필요 없고, 개인 프로젝트나 단일 서버에 딱 맞습니다.

bash

pip install langgraph-checkpoint-sqlite

python

# memory_sqlite.py
from langgraph.checkpoint.sqlite import SqliteSaver
# 나머지 import는 위와 동일...
# MemorySaver 대신 SqliteSaver 사용
# ":memory:" → 인메모리(테스트용)
# "chat_memory.db" → 파일에 영구 저장
with SqliteSaver.from_conn_string("chat_memory.db") as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
# 1번 실행
config = {"configurable": {"thread_id": "user-jisu"}}
graph.invoke(
{"messages": [HumanMessage("저는 Python으로 FastAPI를 주로 만들어요.")]},
config=config
)
print("✅ 1번 실행 완료. 이제 프로그램을 껐다 켜보세요.")

프로그램을 껐다가 다시 실행해도 chat_memory.db 파일이 남아있기 때문에 이전 대화를 이어갈 수 있습니다.

python

# 나중에 다시 실행 — 이전 대화가 복원됨
with SqliteSaver.from_conn_string("chat_memory.db") as checkpointer:
graph = builder.compile(checkpointer=checkpointer)
# 같은 thread_id를 쓰면 이전 대화 이어짐
config = {"configurable": {"thread_id": "user-jisu"}}
result = graph.invoke(
{"messages": [HumanMessage("제가 주로 어떤 프레임워크 쓴다고 했죠?")]},
config=config
)
print(result["messages"][-1].content)
# → "FastAPI라고 하셨어요!"

5. 장기 메모리 — PostgreSQL (프로덕션 수준)

여러 서버에서 동시에 접근해야 하거나, 수백만 건의 대화를 관리해야 한다면 PostgreSQL입니다. LangGraph가 공식적으로 프로덕션 용도로 추천하는 백엔드입니다.

bash

pip install langgraph-checkpoint-postgres psycopg psycopg-pool

python

# memory_postgres.py
from langgraph.checkpoint.postgres import PostgresSaver
import os
DB_URI = os.getenv("DATABASE_URL")
# 예: "postgresql://user:password@localhost:5432/mydb"
def get_graph_with_postgres():
"""PostgreSQL 체크포인터를 연결한 그래프 반환"""
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
# 최초 1회만 실행 — 필요한 테이블 자동 생성
checkpointer.setup()
graph = builder.compile(checkpointer=checkpointer)
return graph, checkpointer
# FastAPI와 함께 사용하는 패턴
from fastapi import FastAPI
from contextlib import asynccontextmanager
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
app = FastAPI()
graph_instance = None
@asynccontextmanager
async def lifespan(app: FastAPI):
"""서버 시작 시 DB 연결, 종료 시 해제"""
global graph_instance
async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
await checkpointer.setup() # 테이블 초기화 (최초 1회)
graph_instance = builder.compile(checkpointer=checkpointer)
print("✅ PostgreSQL 체크포인터 연결 완료")
yield # 서버 실행 중
# 서버 종료 시 자동으로 연결 해제
app = FastAPI(lifespan=lifespan)
@app.post("/chat/{user_id}")
async def chat_endpoint(user_id: str, message: str):
config = {"configurable": {"thread_id": user_id}}
result = await graph_instance.ainvoke(
{"messages": [HumanMessage(content=message)]},
config=config
)
return {"reply": result["messages"][-1].content}
환경권장 체크포인터특징
개발 / 테스트MemorySaver설치 불필요, 빠름, 재시작 시 초기화
로컬 서비스SqliteSaver파일 기반, 영구 저장, 단일 서버용
프로덕션PostgresSaver다중 서버, 고가용성, 암호화 지원

6. 장기 메모리 Store — 사용자 선호도 학습

체크포인터가 “대화 흐름”을 기억한다면, Store는 “사실”을 기억합니다.

사용자가 좋아하는 글쓰기 스타일, 자주 쓰는 언어, 직업 같은 정보를 여러 세션에 걸쳐 축적합니다. 이게 있어야 진짜 개인화가 됩니다.

python

# memory_store.py
from langgraph.store.memory import InMemoryStore
from langgraph.graph import StateGraph, END
from langgraph.checkpoint.memory import MemorySaver
import json
# ── 메모리 스토어 초기화 ────────────────────────────
# 개발: InMemoryStore
# 프로덕션: 별도 영구 저장소 (DB, Redis 등) 연결
store = InMemoryStore()
def save_user_preference(user_id: str, key: str, value: str):
"""사용자 선호도를 Store에 저장"""
namespace = ("user_prefs", user_id)
store.put(namespace, key, {"value": value})
print(f"💾 저장: [{user_id}] {key} = {value}")
def load_user_preferences(user_id: str) -> dict:
"""사용자의 모든 선호도 불러오기"""
namespace = ("user_prefs", user_id)
items = store.search(namespace)
return {item.key: item.value["value"] for item in items}
# ── 선호도를 반영하는 챗봇 노드 ──────────────────────
def smart_chatbot_node(state: ChatState, config: dict) -> dict:
"""
사용자 ID로 저장된 선호도를 불러와 시스템 프롬프트에 반영
"""
user_id = config.get("configurable", {}).get("user_id", "anonymous")
prefs = load_user_preferences(user_id)
# 선호도를 시스템 프롬프트에 주입
pref_text = ""
if prefs:
pref_text = "\n\n[이 사용자에 대해 알고 있는 정보]\n"
for k, v in prefs.items():
pref_text += f"- {k}: {v}\n"
system = SystemMessage(content=f"""
당신은 개인화된 AI 어시스턴트입니다.
사용자의 정보를 기반으로 맞춤형 답변을 제공하세요.{pref_text}
""")
response = llm.invoke([system] + state["messages"])
# 대화에서 새로운 선호도를 감지하면 자동 저장
last_msg = state["messages"][-1].content.lower()
if "python" in last_msg and "좋아" in last_msg:
save_user_preference(user_id, "선호언어", "Python")
if "개발자" in last_msg or "엔지니어" in last_msg:
save_user_preference(user_id, "직업", "개발자")
return {"messages": [response]}

실전 사용 예시

python

# 사용자 정보 미리 저장
save_user_preference("user-jisu", "이름", "지수")
save_user_preference("user-jisu", "직업", "Python 백엔드 개발자")
save_user_preference("user-jisu", "선호스타일", "기술적이고 간결하게")
save_user_preference("user-jisu", "경력", "3년차")
# 저장된 정보 확인
prefs = load_user_preferences("user-jisu")
print(prefs)
# → {'이름': '지수', '직업': 'Python 백엔드 개발자', ...}
# 이제 이 사용자와 대화할 때마다 이 정보가 자동으로 반영됨
config = {
"configurable": {
"thread_id": "session-001",
"user_id": "user-jisu" # Store 조회에 사용
}
}

7. 멀티 유저 처리 — thread_id로 사용자 분리

실제 서비스에서는 여러 사용자가 동시에 접속합니다. thread_id를 올바르게 설계해야 사용자 간 대화가 섞이지 않습니다.

python

# thread_id 설계 패턴
# ✅ 좋은 예 — 사용자별, 목적별로 구분
config_jisu = {"configurable": {"thread_id": "user-jisu-general"}}
config_minho = {"configurable": {"thread_id": "user-minho-general"}}
# 한 사용자가 여러 대화 스레드를 가질 수도 있음
config_jisu_work = {"configurable": {"thread_id": "user-jisu-work-project-a"}}
config_jisu_study = {"configurable": {"thread_id": "user-jisu-study-python"}}
# ❌ 나쁜 예 — 모든 사용자가 같은 thread_id 사용
# 모든 대화가 섞여버림
config_bad = {"configurable": {"thread_id": "global"}}

python

# FastAPI에서 사용자별 thread_id 자동 생성
from fastapi import FastAPI, Header
from typing import Optional
app = FastAPI()
@app.post("/chat")
async def chat(
message: str,
session_id: str, # 클라이언트가 관리하는 세션 ID
x_user_id: Optional[str] = Header(None) # JWT에서 추출한 유저 ID
):
# thread_id = 사용자ID + 세션ID 조합으로 고유성 보장
thread_id = f"{x_user_id}-{session_id}"
config = {"configurable": {
"thread_id": thread_id,
"user_id": x_user_id
}}
result = await graph_instance.ainvoke(
{"messages": [HumanMessage(content=message)]},
config=config
)
return {"reply": result["messages"][-1].content, "thread_id": thread_id}

8. 실전 패턴: 맥락 요약으로 비용 절감

메모리를 쓰면 문제가 하나 생깁니다. 대화가 길어질수록 토큰 비용이 폭증합니다.

100턴 대화라면 매번 100개의 메시지를 LLM에 보내는 셈입니다. 이건 현실적이지 않습니다.

해결책은 오래된 대화를 요약으로 압축하는 것입니다.

python

# memory_summary.py — 대화 요약 패턴
def summarize_if_too_long(state: ChatState) -> dict:
"""
메시지가 20개를 넘으면 오래된 것들을 요약으로 압축합니다.
최근 5개 메시지 + 이전 대화 요약 형태로 유지합니다.
"""
messages = state["messages"]
THRESHOLD = 20 # 이 숫자를 넘으면 요약 실행
KEEP_RECENT = 5 # 최근 N개는 원본 유지
if len(messages) <= THRESHOLD:
return {} # 아직 짧으면 아무것도 안 함
# 요약할 대상: 최근 5개를 제외한 나머지
to_summarize = messages[:-KEEP_RECENT]
recent = messages[-KEEP_RECENT:]
# LLM으로 요약 생성
summary_prompt = f"""
다음 대화를 3~5문장으로 핵심만 요약해주세요.
사용자 이름, 직업, 선호사항, 논의한 주요 주제를 반드시 포함하세요.
대화:
{chr(10).join([f"{m.type}: {m.content}" for m in to_summarize])}
"""
summary_response = llm.invoke([HumanMessage(content=summary_prompt)])
# 요약을 첫 번째 시스템 메시지로, 나머지는 최근 메시지만 유지
summary_message = SystemMessage(
content=f"[이전 대화 요약]\n{summary_response.content}"
)
print(f"🗜️ 메모리 압축: {len(to_summarize)}개 메시지 → 요약 1개")
return {"messages": [summary_message] + recent}

python

# 그래프에 요약 노드 추가
builder = StateGraph(ChatState)
builder.add_node("chatbot", chatbot_node)
builder.add_node("summarize", summarize_if_too_long)
builder.set_entry_point("chatbot")
# 대화 후 자동으로 길이 체크
builder.add_edge("chatbot", "summarize")
builder.add_edge("summarize", END)

이 패턴을 쓰면 대화가 아무리 길어져도 토큰 비용이 일정 수준으로 유지됩니다.

방식100턴 후 컨텍스트 크기비용
요약 없음100개 메시지 전체💸💸💸
요약 압축요약 1개 + 최근 5개💸

9. 메모리 설계 체크리스트

프로젝트에서 메모리를 설계할 때 이 질문들을 먼저 답해보세요.

환경 선택

  • 로컬 개발 / 프로토타입 → MemorySaver
  • 단일 서버 소규모 서비스 → SqliteSaver
  • 멀티 서버 / 프로덕션 → PostgresSaver

thread_id 설계

  • 사용자별로 고유한 ID인가?
  • 대화 목적(주제)별로 분리했는가?
  • 로그인 사용자는 user_id + session_id 조합을 사용하는가?

장기 메모리 Store

  • 세션 간 기억해야 할 정보가 있는가? (이름, 직업, 선호 등)
  • 자동 감지 vs 수동 저장 중 어느 방식이 적합한가?

비용 최적화

  • 긴 대화를 요약하는 로직이 있는가?
  • 최근 N개 메시지만 활성 컨텍스트로 유지하는가?

에러 처리

  • DB 연결 실패 시 폴백(fallback)이 있는가?
  • MemorySaver로 자동 전환되는 로직이 있는가?

마치며 — 기억하는 AI와 기억 못하는 AI의 간극

메모리를 붙이기 전과 후, AI의 느낌은 완전히 달라집니다.

기억 못하는 AI는 매번 처음부터 시작하는 인턴입니다. 기억하는 AI는 나를 아는 동료입니다.

오늘 배운 내용을 정리하면 이렇습니다.

  • MemorySaver → 세션 내 기억. 개발·테스트 전용
  • SqliteSaver → 파일 기반 영구 기억. 로컬·소규모 서비스
  • PostgresSaver → DB 기반. 프로덕션 표준
  • Store → 세션을 넘는 사실·선호도 저장. 진짜 개인화
  • 요약 패턴 → 긴 대화의 비용 최적화

이 네 가지를 조합하면 “나를 알고, 기억하고, 성장하는 AI 어시스턴트”가 완성됩니다.

다음 5편에서는 이 모든 것을 합쳐서 완전한 AI 어시스턴트 서비스를 Docker로 패키징하고 클라우드에 배포하는 방법을 다룹니다. 1편부터 4편까지 만든 모든 것이 하나로 합쳐지는 편입니다.


🔖 이 시리즈의 다른 글

  • 1편: 왜 Python이 2026년에도 압도적 1위인가
  • 2편: 나만의 AI 챗봇 만들기 — RAG 처음부터 배포까지
  • 3편: 혼자 일하는 AI는 이제 옛날 — LangGraph 멀티 에이전트
  • 4편: AI가 드디어 기억한다 — LangGraph 메모리 완벽 가이드 ← 지금 여기
  • 5편: 1~4편 통합 — Docker로 패키징하고 클라우드 배포하기 (예정)

태그: #Python #LangGraph #AI메모리 #챗봇 #LangChain #PostgreSQL #SQLite #AI개발 #개발튜토리얼 #2026


데이터 출처: LangGraph 공식 문서 · DigitalOcean LangGraph+Mem0 튜토리얼 · Markaicode LangGraph Memory Guide · LangChain Checkpoint Docs

Tags

Categories

Discover more from

Subscribe now to keep reading and get access to the full archive.

Continue reading